Getting started with logging in FastAPI
Learn how to implement request logging, capture application logs, and correlate them in your FastAPI application.


Logging is critical for understanding and debugging FastAPI apps in production. When things go wrong, logs are usually the best starting point for any investigation.
For APIs, logging can be broken down into two categories: request logs, which record individual API requests and responses, and application logs, which contain detailed messages from your application’s code. Ideally, both types of logs are correlated, meaning each log record is linked to the API request.
In this article, we’ll cover the basics of logging and log correlation in FastAPI. We’ll also introduce Apitally as a simple solution for capturing request logs and correlated application logs with minimal effort.
Basic request logging
Request logs are a chronological record of every API request your application handles. A typical entry contains at least a timestamp, the HTTP method, request path, and response status code.
If you’re using uvicorn to run your FastAPI app, you actually get basic request logs in your console out of the box. However, these don’t include timestamps or correlation IDs, which are necessary to link other log messages with requests.
We can address these shortcomings with a custom logging middleware. That way we’ll have more control over what is included in the logs. For example, we could add the response time to identify slow requests.
Let’s implement this in a simple FastAPI app:
import logging
import time
from fastapi import FastAPI, Request
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
app = FastAPI(title="Example API")
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
response_time = time.perf_counter() - start_time
logger.info(f"{request.method} {request.url.path} {response.status_code} {response_time:.3f}s")
return response
@app.get("/hello")
async def say_hello():
logger.info("Saying hello")
return {"message": "Hello!"}
You can disable uvicorn’s built-in logs with the --no-access-logs
option.
The middleware logs the request details after the route handler returns, while the route handler also logs during its execution. When two requests are processed concurrently, the log output could look like this:
2025-09-30 14:29:00,123 [main] INFO: Saying hello
2025-09-30 14:29:00,123 [main] INFO: Saying hello
2025-09-30 14:29:00,124 [main] INFO: GET /hello 200 0.001s
2025-09-30 14:29:00,124 [main] INFO: GET /hello 200 0.001s
As you can see there is no way to tell which “Saying hello” message belongs to which request. Not a big deal in this simple example, but essential when debugging production issues.
Log correlation
To link log messages with requests we need a correlation ID. I highly recommend using the asgi-correlation-id package for this purpose.
pip install asgi-correlation-id
The package provides a middleware we can add to our application. It automatically generates a correlation ID for each incoming request. Additionally, we need to configure a log filter and include the correlation ID in our log format. This can’t be done using basicConfig
, so we need to use the more verbose dictConfig
instead.
import logging
from asgi_correlation_id import CorrelationIdMiddleware
from fastapi import FastAPI
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"filters": {
"correlation_id": {
"()": "asgi_correlation_id.CorrelationIdFilter",
"uuid_length": 32,
"default_value": "-",
},
},
"formatters": {
"standard": {
"format": "%(asctime)s [%(correlation_id)s] [%(name)s] %(levelname)s: %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"filters": ["correlation_id"],
"formatter": "standard",
},
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
})
logger = logging.getLogger(__name__)
app = FastAPI()
app.add_middleware(CorrelationIdMiddleware)
# ...
If we made two concurrent requests to our API again, we’d get the following output. You can see how the correlation IDs allow us to tell which messages belong together.
2025-09-30 14:36:00,123 [main] [50e2646d5e594cb197ae9f3d21de7a57] INFO: Saying hello
2025-09-30 14:36:00,123 [main] [e986f5451c4b4e0fb7fea90dda32608f] INFO: Saying hello
2025-09-30 14:36:00,124 [main] [50e2646d5e594cb197ae9f3d21de7a57] INFO: GET /hello 200 0.001s
2025-09-30 14:36:00,124 [main] [e986f5451c4b4e0fb7fea90dda32608f] INFO: GET /hello 200 0.001s
Beyond log files
We now have correlated logs being written to the console or log files. That’s a big step up. But if you’ve ever tried to debug a production issue by sifting through large log files, you know this approach doesn’t scale. For many APIs, the volume of logs can quickly become too large to inspect manually.
On top of that, request and response metadata from logs often isn’t enough. You may need to inspect the full request and response headers and payloads to get to the bottom of an issue. These are impractical to capture in log files.
Introducing Apitally
Apitally is designed to solve these exact challenges. It provides a user-friendly and searchable interface for request logs and automatically handles log correlation. You can configure exactly what gets logged, including headers and payloads, while masking rules make it easy to avoid capturing sensitive information.
To get started, let’s install the Apitally SDK for FastAPI.
pip install "apitally[fastapi]"
Then, add the Apitally middleware to your FastAPI app. This replaces the need for asgi-correlation-id and any custom middleware for request logging.
from fastapi import FastAPI
from apitally.fastapi import ApitallyMiddleware
app = FastAPI()
app.add_middleware(
ApitallyMiddleware,
client_id="your-client-id",
env="dev", # or "prod" etc.
enable_request_logging=True,
capture_logs=True,
# Configure what's included in logs
log_request_headers=True,
log_request_body=True,
log_response_body=True,
# Mask headers or body fields using regex
mask_headers=[r"^X-Sensitive-Header$"],
mask_body_fields=[r"^sensitive_field$"],
)
Check out the docs to learn about all available parameters and default masking patterns.
With this configuration, logs are sent to the Apitally dashboard, where you can easily filter and search them.

You can inspect individual requests and responses in detail, including the full payloads.

Captured log messages emitted by your application during request handling can be viewed alongside the request details.

Conclusion
With request logging and correlation IDs in place, we can trace through our logs effectively when troubleshooting issues. Our custom middleware and the asgi-correlation-id package provided a straightforward way to implement this in FastAPI.
When logs are written only to the console or files, searching and filtering can become inefficient as the volume grows. Apitally solves this by indexing log data, making it easy to find and inspect requests, responses, and related log messages. Its SDK for FastAPI automates the entire logging pipeline and is ready for production with minimal configuration.