import time import os from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware from app.core.logger import logger # Paths that are too noisy to log at INFO — only log if they error _QUIET_PATHS = frozenset(["/health", "/favicon.ico", "/metrics", "/docs", "/openapi.json", "/redoc"]) class APILoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): start_time = time.perf_counter() path = request.url.path method = request.method try: response = await call_next(request) except Exception as exc: duration_ms = round((time.perf_counter() - start_time) * 1000, 1) logger.bind( method=method, path=path, status=500, duration_ms=duration_ms, ).error(f"Unhandled exception on {method} {path} — {type(exc).__name__}: {exc}") raise duration_ms = round((time.perf_counter() - start_time) * 1000, 1) status = response.status_code # Suppress noisy health-check / static paths unless they error if path in _QUIET_PATHS and status < 400: return response # Route logs by severity so production dashboards can filter cleanly bound = logger.bind(method=method, path=path, status=status, duration_ms=duration_ms) if status >= 500: bound.error(f"{method} {path} → {status} ({duration_ms}ms)") elif status >= 400: # 401 on /auth/me is expected background noise — demote to DEBUG if path == "/auth/me" and status == 401: bound.debug(f"{method} {path} → {status} ({duration_ms}ms)") else: bound.warning(f"{method} {path} → {status} ({duration_ms}ms)") else: bound.info(f"{method} {path} → {status} ({duration_ms}ms)") return response