Engineering in practice

Structured Logging and Distributed Tracing in Production

14 October 2026 · 8 min read

The Fallacy of Grepping Plaintext Logs

In early-stage local development, unstructured plaintext logs feel intuitive:

2026-10-14 10:00:00 [INFO] order processed for user 42 total $150

However, the moment your backend moves behind a load balancer running across eight container replicas handling thousands of simultaneous requests per second, plaintext logs become an unmaintainable nightmare:

  • Coroutines from hundreds of concurrent HTTP requests interleave their output lines randomly.
  • Filtering logs by user_id or tenant_id requires slow, fragile regex parsing.
  • Modern log aggregators (Grafana Loki, Datadog, Elasticsearch) cannot extract actionable metrics or build automated latency alert thresholds from free-form strings.

1. Migrating to Single-Line Structured JSON

In production environments, the primary consumer of log streams is not human eyes, but indexing software. Every log entry should be emitted as a single, valid, strongly-typed JSON object:

{
  "timestamp": "2026-10-14T10:00:00.124Z",
  "level": "INFO",
  "message": "Order processed successfully",
  "service": "billing-service",
  "correlation_id": "a9f8c12b-e452-4f8a-9213-91b4091a1e84",
  "user_id": 42,
  "order_id": 10842,
  "duration_ms": 14.2,
  "status_code": 201
}

With structured fields, engineers can execute instant analytical queries in under a second:

  • “Show all requests for tenant 42 where duration exceeded 300ms.”
  • “Graph the 99th percentile response time grouped by endpoint.”

2. Distributed Tracing: Propagating Correlation IDs in Asyncio

In asynchronous Python applications, traditional thread-local storage (threading.local) completely fails because hundreds of simultaneous requests share the same operating system thread. Using global state results in correlation IDs leaking across tenants.

The correct, robust pattern is Python’s native contextvars.ContextVar:

import contextvars
import uuid
from starlette.middleware.base import BaseHTTPMiddleware

correlation_ctx: contextvars.ContextVar[str] = contextvars.ContextVar("correlation_id", default="")

class CorrelationMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        # Extract incoming header or mint a new cryptographic UUID
        cid = request.headers.get("X-Correlation-ID", str(uuid.uuid4()))
        token = correlation_ctx.set(cid)
        try:
            response = await call_next(request)
            response.headers["X-Correlation-ID"] = cid
            return response
        finally:
            correlation_ctx.reset(token)

The superpower of ContextVar is that when an HTTP handler spawns sub-coroutines or background jobs via asyncio.create_task(), the child task automatically inherits a snapshot of the parent context, guaranteeing that every downstream database call or Redis operation is stamped with the identical trace ID.


3. Automated Secret Masking at the Formatter Layer

Accidentally leaking sensitive credentials, session tokens, or API secrets into log aggregators represents a severe security risk and GDPR compliance failure.

A production-ready logging engine must intercept log dictionaries before formatting, redacting sensitive keys (password, authorization, secret, token, api_key) and regex patterns (JWT tokens, private keys) with [REDACTED].


Conclusion

Structured logging and end-to-end correlation propagation are not optional luxuries—they represent the bedrock of production observability. They make the difference between diagnosing an incident in five minutes or spending hours guessing amid hundreds of thousands of disconnected log entries.