C.W.K.
Stream
Lesson 01 of 06 · published

Observability & Correlation IDs — The Thread Through the Distributed Maze

~10 min · production, observability, correlation-id, tracing

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"A user reports 'this didn't work.' Without a correlation ID, you spend an hour correlating timestamps across five service logs. With one, you paste it into log search and see the entire story in five seconds. The whole game."

The Problem Correlation IDs Solve

A single user request, in a modern app, can touch many services: the gateway, your application, an auth service, a database, a cache, an upstream provider (Stripe, OpenAI), maybe a queue and a worker. Each writes its own logs. Without a shared identifier, correlating which log line in service B corresponds to which request in service A requires matching on timestamps and best-guess heuristics — slow, error-prone, and impossible at scale.

The fix: assign a unique ID to each incoming request, propagate it through every downstream call, log it with every line. Search by ID, see everything for that one request, end-to-end.

The Three Layers of the Pattern

  1. Generation. The first service that touches the request (usually the gateway or the application's outermost middleware) generates a UUID and assigns it as the correlation ID. If the client supplied an X-Request-ID header (some clients do for client-driven tracing), trust and reuse it.
  2. Propagation. Every downstream HTTP call carries the ID in a header (X-Request-ID, X-Correlation-Id, or W3C standard traceparent). Background queues carry it as part of the job payload. Async workers carry it forward.
  3. Logging. Every log line includes the ID as a structured field. Every external response carries it back to the client (so customer support can paste it into log search).

The W3C Trace Context — One Standard to Rule Them All

W3C Trace Context (RFC-status standard) defines two headers that have become canonical:

  • traceparent00-{trace_id}-{span_id}-{flags}. The trace_id is the request-wide correlation ID; span_id identifies this specific operation; flags carry sample/debug bits.
  • tracestate — vendor-specific extension state.

OpenTelemetry, Honeycomb, DataDog, Jaeger, AWS X-Ray, GCP Cloud Trace all speak this. If you use any one of these for distributed tracing, you get correlation IDs for free.

Correlation IDs aren't optional in any distributed system you run for more than a week. They're the difference between debugging in minutes and debugging in hours. Generate them at the edge; propagate everywhere; log with every line. Cost: one UUID per request. Value: every incident, forever.

Beyond IDs — The Observability Triangle

Correlation IDs are the entry point to the wider observability story:

  • Logs — discrete events (with the correlation ID).
  • Metrics — aggregated counters and histograms (request rate, latency P50/P99, error rate by endpoint).
  • Traces — the structured shape of one request across services (using traceparent).

All three benefit from the correlation ID linking them. A spike in a metric leads to traces leads to logs of specific requests — the chain that turns 'something's wrong' into 'here's exactly what.'

cwkPippa's Observability Reality

cwkPippa generates a request_id (UUID) in middleware on every incoming HTTP request, stores it in request.state.request_id, includes it as X-Request-ID on every response, and logs it with every log line. The JSONL session log is keyed by conversation_id (a separate identifier — one conversation has many request_ids over its lifetime). Together: when Dad reports 'the chat got stuck on May 23 around 11pm,' I can grep JSONL for the conversation, find the affected request_ids, and pull every log line for those across all four brain adapters. No external tracing infrastructure; just disciplined logging.

Code

FastAPI middleware: generate/reuse ID, log it, echo it·python
# FastAPI — correlation ID middleware
import uuid, logging
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware

log = logging.getLogger(__name__)

class CorrelationIDMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Reuse client-supplied ID if present (some clients do client-driven tracing)
        request_id = request.headers.get('x-request-id') or str(uuid.uuid4())
        request.state.request_id = request_id

        # Attach to every log line (use structlog or contextvars in production)
        log.info('request.start', extra={'request_id': request_id, 'path': request.url.path})

        response = await call_next(request)

        # Echo on the response for the client to see
        response.headers['X-Request-ID'] = request_id
        log.info('request.end', extra={'request_id': request_id, 'status': response.status_code})
        return response

app = FastAPI()
app.add_middleware(CorrelationIDMiddleware)

@app.get('/api/anything')
async def anything(request: Request):
    rid = request.state.request_id
    log.info('handler.work', extra={'request_id': rid})
    return {'rid': rid}
Propagation: pass the ID through every downstream call·python
# Propagate to downstream services via httpx
import httpx
from fastapi import Request

async def call_upstream(request: Request, payload: dict):
    rid = request.state.request_id
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            'https://upstream.example.com/process',
            json=payload,
            headers={'X-Request-ID': rid},  # propagate downstream
        )
        return resp.json()

# Upstream service sees the same X-Request-ID header,
# logs with the same ID, returns it on its response — closing the loop.
W3C traceparent — what OpenTelemetry and modern tracers speak·python
# W3C Trace Context — the standard form (OpenTelemetry-compatible)
import uuid
from fastapi import Request

def make_traceparent(trace_id: str | None = None, span_id: str | None = None) -> str:
    trace_id = trace_id or uuid.uuid4().hex                      # 32 hex chars
    span_id  = span_id  or uuid.uuid4().hex[:16]                 # 16 hex chars
    return f'00-{trace_id}-{span_id}-01'                          # 00=version, 01=sampled

# Read incoming traceparent (if any); otherwise create one
def get_trace_context(request: Request) -> dict:
    incoming = request.headers.get('traceparent')
    if incoming:
        # parse: version-trace_id-span_id-flags
        _, trace_id, _, _ = incoming.split('-')
        return {'trace_id': trace_id, 'traceparent': make_traceparent(trace_id)}
    return {'trace_id': uuid.uuid4().hex, 'traceparent': make_traceparent()}

External links

Exercise

Add a correlation ID middleware to a FastAPI app: generate UUID on every request, store in request.state, include as X-Request-ID on the response, log it with every log line. Make 10 requests; verify the IDs appear in logs and response headers, are different across requests but consistent within a single request. Bonus: have the FastAPI app call a second FastAPI service via httpx; propagate the ID via X-Request-ID header; show that both services log lines tagged with the same ID for the same end-user request.
Hint
BaseHTTPMiddleware is the cleanest FastAPI pattern. For logging, the production-grade approach is structlog or contextvars to thread the ID through every log call without manual extra= everywhere. The two-service bonus is the canonical distributed tracing demo — same request_id appears in both services' logs, proving end-to-end correlation works.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.