Structured Logging in Prod
Scenario
A user complains that checkout failed at 3:14 PM. You search your text logs, but hundreds of concurrent requests are intermixed. You can't figure out which log lines belong to their request.
Mental model
Standard logs are strings designed for human eyes. Structured logs are JSON objects designed for machines (like Datadog or Splunk). By attaching a unique `request_id` to every log emitted during a request, you can instantly filter and trace a single user's journey.
Deep dive
In production, `print()` and standard Python `logging` text strings are insufficient. They cannot be easily queried or filtered in centralized log aggregators.
Structured logging outputs a JSON object for every log line. Each object contains standard fields: timestamp, severity level, module, and a context payload.
A critical backend pattern is injecting a correlation ID (`request_id`). A middleware generates a UUID when the request starts, stores it in context variables (`contextvars`), and the logger automatically appends it to every log emitted during that request lifecycle.
Code examples
Bad: Text logging
import logging
logging.info(f"User {user_id} bought item {item_id}")
# Output: 2026-05-12 12:00:00 INFO User 42 bought item 99
If you want to find all purchases of item 99, you have to write a brittle regex parser.
Good: Structured logging with context
import structlog
from contextvars import ContextVar
request_id_var = ContextVar("request_id")
logger = structlog.get_logger()
# In a FastAPI Middleware:
request_id_var.set("uuid-1234")
# Anywhere in the app:
logger.info("item_purchased", user_id=42, item_id=99)
# Output: {"event": "item_purchased", "user_id": 42, "item_id": 99, "request_id": "uuid-1234"}
The output is JSON. Log aggregators automatically parse it, allowing you to instantly search `request_id=uuid-1234` and see everything that happened.
Common mistakes
- Leaking context across requests: If you use global variables or thread locals for the `request_id` in an async app, requests will overwrite each other's IDs. You MUST use Python's `contextvars`, which are natively async-safe.
Recall questions
- Why is JSON preferred over plain text for production logging?
- What is a correlation ID or request ID?
- Which Python module must be used to store request context safely in `async` applications?
Questions & answers
How would you trace a request that starts in a FastAPI service, calls a microservice, and then updates a database?
I would generate a `request_id` in the API gateway/middleware. I'd include it in structured logs locally, and inject it into the HTTP headers when calling the microservice so the downstream service logs with the exact same ID.
Why shouldn't you use standard Python threading locals for context in FastAPI?
FastAPI uses `asyncio`. Multiple concurrent requests share the same thread (the event loop). Thread locals would leak state between requests; `contextvars` natively handle async task separation.
Continue learning
Previous: Middleware & CORS