Structured Logging in Prod

RoadmapsPython Backend

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

Recall questions

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

Return to Python Backend Roadmap