import logging import time import uuid from collections.abc import Awaitable, Callable from fastapi import FastAPI, Request from starlette.responses import Response logger = logging.getLogger("app.request") def install_request_logging_middleware(app: FastAPI) -> None: @app.middleware("http") async def request_logging_middleware( request: Request, call_next: Callable[[Request], Awaitable[Response]], ) -> Response: request_id = request.headers.get("X-Request-ID") or uuid.uuid4().hex request.state.request_id = request_id started_at = time.perf_counter() try: response = await call_next(request) except Exception: duration_ms = round((time.perf_counter() - started_at) * 1000, 2) logger.exception( "request failed", extra={ "event": "request", "request_id": request_id, "method": request.method, "path": request.url.path, "status_code": 500, "duration_ms": duration_ms, "client_ip": request.client.host if request.client else None, "user_id": getattr(request.state, "user_id", None), }, ) raise duration_ms = round((time.perf_counter() - started_at) * 1000, 2) status_code = response.status_code level = logging.INFO if status_code >= 500: level = logging.ERROR elif status_code >= 400: level = logging.WARNING logger.log( level, "request completed", extra={ "event": "request", "request_id": request_id, "method": request.method, "path": request.url.path, "status_code": status_code, "duration_ms": duration_ms, "client_ip": request.client.host if request.client else None, "user_id": getattr(request.state, "user_id", None), }, ) response.headers["X-Request-ID"] = request_id return response