"""FastAPI middleware — request ID, structured logging, timing.""" from __future__ import annotations import time import uuid from collections.abc import Awaitable, Callable import structlog from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from core.logging import get_logger logger = get_logger(__name__) class RequestIDMiddleware(BaseHTTPMiddleware): """Attach a unique request ID to each request and bind it to logs.""" async def dispatch( self, request: Request, call_next: Callable[[Request], Awaitable[Response]], ) -> Response: request_id = request.headers.get("X-Request-ID") or uuid.uuid4().hex[:12] structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars( request_id=request_id, method=request.method, path=request.url.path, ) start = time.perf_counter() try: response = await call_next(request) except Exception as e: logger.exception("request_unhandled_error", error=str(e)) raise duration_ms = (time.perf_counter() - start) * 1000 response.headers["X-Request-ID"] = request_id logger.info( "request_completed", status_code=response.status_code, duration_ms=round(duration_ms, 2), ) return response