""" Middleware for request logging and tracking. Implements correlation ID tracking for requests and comprehensive logging following 2026 FastAPI best practices. """ import logging import time import uuid from typing import Callable from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware from starlette.types import ASGIApp logger = logging.getLogger(__name__) class RequestLoggingMiddleware(BaseHTTPMiddleware): """ Middleware to log all HTTP requests with timing and correlation IDs. Features: - Generates unique correlation ID for each request - Logs request method, path, and client IP - Measures and logs request processing time - Logs response status code - Adds correlation ID to response headers for tracing """ async def dispatch( self, request: Request, call_next: Callable ) -> Response: # Generate correlation ID for request tracking correlation_id = str(uuid.uuid4()) request.state.correlation_id = correlation_id # Get client IP client_host = request.client.host if request.client else "unknown" # Log incoming request logger.info( f"Request started - " f"method={request.method} " f"path={request.url.path} " f"client={client_host} " f"correlation_id={correlation_id}" ) # Process request and measure time start_time = time.time() try: response = await call_next(request) process_time = time.time() - start_time # Add correlation ID to response headers response.headers["X-Correlation-ID"] = correlation_id response.headers["X-Process-Time"] = str(process_time) # Log response logger.info( f"Request completed - " f"method={request.method} " f"path={request.url.path} " f"status={response.status_code} " f"duration={process_time:.3f}s " f"correlation_id={correlation_id}" ) return response except Exception as exc: process_time = time.time() - start_time # Log error logger.error( f"Request failed - " f"method={request.method} " f"path={request.url.path} " f"duration={process_time:.3f}s " f"correlation_id={correlation_id} " f"error={str(exc)}", exc_info=True ) # Return a proper response so it flows through CORSMiddleware. # Re-raising from BaseHTTPMiddleware bypasses CORS headers. from starlette.responses import JSONResponse from fastapi import HTTPException if isinstance(exc, HTTPException): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, headers={"X-Correlation-ID": correlation_id, "X-Process-Time": f"{process_time:.3f}"}, ) return JSONResponse( status_code=500, content={"detail": "Internal server error"}, headers={"X-Correlation-ID": correlation_id, "X-Process-Time": f"{process_time:.3f}"}, ) class ErrorLoggingMiddleware(BaseHTTPMiddleware): """ Middleware to catch and log unhandled exceptions. Ensures all exceptions are logged before being returned to the client, providing full stack traces for debugging. IMPORTANT: Returns a JSONResponse instead of re-raising so the response flows back through CORSMiddleware and gets proper CORS headers. Re-raising exceptions from BaseHTTPMiddleware bypasses CORS, causing browsers to report CORS errors instead of the actual error (e.g., 401). """ async def dispatch( self, request: Request, call_next: Callable ) -> Response: try: response = await call_next(request) return response except Exception as exc: correlation_id = getattr(request.state, "correlation_id", "unknown") logger.error( f"Unhandled exception - " f"method={request.method} " f"path={request.url.path} " f"correlation_id={correlation_id}", exc_info=True ) # Return a proper response so it flows through CORSMiddleware. # If we re-raise, the response never passes through CORS and # browsers see a CORS error instead of the actual error. from starlette.responses import JSONResponse from fastapi import HTTPException if isinstance(exc, HTTPException): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, ) return JSONResponse( status_code=500, content={"detail": "Internal server error"}, )