Request envelope and trace ID propagation

Once requests fan out across orchestrator, tools, retrieval, and memory, debugging without a trace ID is hopeless. The fix is small: wrap every request in an envelope at the transport layer, stamp a trace ID, and pass that envelope through every downstream call. Now any log line can be tied back to one user request.

transport/envelope.py
python
import uuid
from contextvars import ContextVar
from pydantic import BaseModel


trace_id_var: ContextVar[str] = ContextVar('trace_id', default='-')


class RequestEnvelope(BaseModel):
    trace_id: str
    user_id: str | None = None
    payload: dict


async def envelope_middleware(request, call_next):
    trace_id = request.headers.get('x-trace-id') or str(uuid.uuid4())
    token = trace_id_var.set(trace_id)
    try:
        response = await call_next(request)
        response.headers['x-trace-id'] = trace_id
        return response
    finally:
        trace_id_var.reset(token)

A ContextVar stores the trace ID for the lifetime of the request. The middleware accepts an inbound x-trace-id (from a calling service) or mints a new UUID. Every layer below reads from the ContextVar without threading the value through every function.

Two rules turn this into a real debugging tool. Every log line at every layer must include trace_id. And the trace ID must echo back in the response header so a frontend or upstream caller can quote it in a bug report. Without the echo, users cannot tell you which run broke.

Quiz: Quiz

Loading practice…