FastAPI JSON logging to stdout (Uvicorn access logs included)

Container platforms like Docker, Kubernetes, and AWS ECS expect structured JSON logs on stdout. Without consistent formatting, log aggregation systems (CloudWatch, Datadog, ELK) can’t parse your logs reliably.

The Problem

A typical FastAPI application produces two distinct log streams:

Application logs (your code):

INFO:     Processing order 12345

Uvicorn access logs (HTTP requests):

INFO:     127.0.0.1:52486 - "GET /api/orders HTTP/1.1" 200 OK

These plain text formats cause problems:

  1. Inconsistent structure - App logs and access logs have different formats

  2. Color codes - Uvicorn adds ANSI escape sequences that break JSON parsers

  3. No machine-readable fields - Can’t filter by status code, latency, or request ID

The Solution

fapilog provides unified JSON output for both application and access logs:

from fastapi import FastAPI, Depends
from fapilog.fastapi import FastAPIBuilder, get_request_logger

app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .build()
)

@app.get("/api/orders/{order_id}")
async def get_order(order_id: str, logger=Depends(get_request_logger)):
    await logger.info("Processing order", order_id=order_id)
    return {"order_id": order_id, "status": "shipped"}

The fastapi preset configuration:

  • Outputs JSON to stdout (no files)

  • Disables color codes

  • Includes request context (request_id) automatically

  • Masks sensitive fields (passwords, API keys, tokens)

Sample Output

Application log:

{"timestamp": "2026-01-21T10:30:00.123Z", "level": "INFO", "message": "Processing order", "order_id": "12345", "request_id": "550e8400-e29b-41d4-a716-446655440000"}

Access log:

{"timestamp": "2026-01-21T10:30:00.456Z", "level": "INFO", "message": "HTTP request", "method": "GET", "path": "/api/orders/12345", "status_code": 200, "duration_ms": 15.2, "request_id": "550e8400-e29b-41d4-a716-446655440000"}

Both share consistent field names (timestamp, level, message, request_id), making log aggregation straightforward.

Including Uvicorn Access Logs

fapilog’s FastAPIBuilder automatically installs middleware that captures HTTP requests. The LoggingMiddleware handles:

  • Request method, path, and query parameters

  • Response status codes

  • Request duration

  • Client IP address

  • Request ID correlation

No additional Uvicorn configuration is needed. The default Uvicorn access log is effectively replaced by fapilog’s structured output.

Disabling Default Uvicorn Logs

If you want to suppress Uvicorn’s default access logging entirely (recommended to avoid duplicate logs), configure Uvicorn:

import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        access_log=False,  # Disable Uvicorn's access log
    )

Or via CLI:

uvicorn main:app --host 0.0.0.0 --port 8000 --no-access-log

Docker Configuration

A minimal Dockerfile for JSON logging:

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Use --no-access-log to avoid duplicate access logs
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]

Docker Compose

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - FAPILOG_CORE__LOG_LEVEL=INFO
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Explicit Format Control

If you’re not using the FastAPI integration but still want JSON output:

from fapilog import get_async_logger

async def main():
    # format="json" ensures JSON output regardless of environment
    logger = await get_async_logger(format="json")
    await logger.info("Starting service", version="1.0.0")

For automatic format detection (JSON in containers, pretty in terminals):

logger = await get_async_logger(format="auto")

Going Deeper