"""Correlation ID middleware for distributed tracing. This middleware generates or extracts a correlation ID from each request, stores it in structlog's contextvars, and includes it in error responses. This enables correlating logs across frontend and backend for a single user action or request flow. Correlation IDs flow through the request lifecycle: 1. Frontend generates/passes via `X-Correlation-ID` header 2. Middleware extracts or generates a UUID4 3. Middleware stores in structlog.contextvars 4. All log entries include the correlation ID automatically 5. Error responses include the correlation ID for client-side correlation Processing order ----------------- This middleware must be the outermost in the security-critical chain so it executes first on incoming requests (outermost = first to see request, last to see response). In the required chain: CorrelationIdMiddleware → CsrfMiddleware → RateLimitMiddleware The registration order in ``main.py`` must be: RateLimitMiddleware, CsrfMiddleware, CorrelationIdMiddleware (last registered = outermost in Starlette's reverse application). """ from __future__ import annotations import uuid from typing import TYPE_CHECKING import structlog from starlette.middleware.base import BaseHTTPMiddleware if TYPE_CHECKING: from collections.abc import Awaitable, Callable from starlette.requests import Request from starlette.responses import Response as StarletteResponse log: structlog.stdlib.BoundLogger = structlog.get_logger() # Standard header name for correlation IDs (follows W3C Trace Context conventions) _CORRELATION_ID_HEADER: str = "X-Correlation-ID" # Key name for storing correlation ID in structlog context CORRELATION_ID_CONTEXT_KEY: str = "correlation_id" class CorrelationIdMiddleware(BaseHTTPMiddleware): """Extract or generate correlation ID and inject into structlog context. For each request, this middleware: 1. Checks for `X-Correlation-ID` header (trusted from frontend) 2. Generates a new UUID4 if header not present 3. Stores in structlog.contextvars so all logs for this request include it 4. Makes available via request.state for error handlers The correlation ID enables tracing a single user action or request flow across both frontend and backend systems using structured logs. """ async def dispatch( self, request: Request, call_next: Callable[[Request], Awaitable[StarletteResponse]], ) -> StarletteResponse: """Intercept requests to extract or generate correlation ID. Args: request: The incoming HTTP request. call_next: The next middleware / router handler. Returns: The response from the next middleware / router, with correlation ID in the request state for use by exception handlers. """ # Extract correlation ID from request header, or generate a new one correlation_id: str = request.headers.get( _CORRELATION_ID_HEADER, str(uuid.uuid4()), ) # Store in structlog context so all logs for this request include it structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars( **{CORRELATION_ID_CONTEXT_KEY: correlation_id} ) # Also store on request.state for use by exception handlers request.state.correlation_id = correlation_id log.debug( "request_received", method=request.method, path=request.url.path, ) response: StarletteResponse = await call_next(request) # Add correlation ID to response header so frontend can correlate errors response.headers[_CORRELATION_ID_HEADER] = correlation_id return response