Getting started with logging in FastAPI

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

Simon GurckeSimon Gurcke//5 min read

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.

Request logs in Apitally dashboard
Request logs in Apitally

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

Response body in request log item
Response body in request logs

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

Correlated application logs in request log item
Correlated application logs

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.