Getting started with logging in FastAPI
A quick guide on setting up request logging and correlating application logs in FastAPI for effective troubleshooting in production.


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 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, especially with high request volumes.
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 this reference for all available parameters and default masking patterns.
With this configuration, the Apitally SDK automatically captures request and application logs, stores them in a temporary file, and periodically flushes them to Apitally’s servers. All this happens asynchronously to not affect your app’s performance.
You can then find and inspect the logs in the Apitally dashboard, with various filtering options. For example, you can filter by:
- Consumer or client IP address
- HTTP method, request URL or response status code
- Request and response body (free text search)
- Response time (find slow requests)
Clicking on a request in the logs brings up a modal with rich details, including:
- Path and query parameters
- Headers (with sensitive headers masked automatically)
- Request and response bodies (supports text and JSON up to 50 KB)
- Correlated application logs
- List of related requests from same client
Apitally stores log data in a ClickHouse database hosted in the US and retains it for 15 days. Log volume limits apply based on the pricing tier, with the lowest tier allowing 1 million requests per month. The highest tier includes 25 million requests and users can enable usage-based pricing to go beyond that limit.
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.