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.
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…