"""HTTP request logging middleware.""" import asyncio import logging import time from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response logger = logging.getLogger(__name__) # Paths that should not be logged _SKIP_PREFIXES = ("/assets", "/ws") _SKIP_PATHS = ("/favicon.ico",) class LoggingMiddleware(BaseHTTPMiddleware): """Logs every HTTP request to the database via db_schema.insert_http_log. - Skips static assets, WebSocket, and favicon paths. - Measures request duration in milliseconds. - Extracts username from request.state.user when available. - Writes logs asynchronously (non-blocking). - Never lets logging failures break a request. """ async def dispatch(self, request: Request, call_next) -> Response: path = request.url.path # Skip paths that should not be logged if path in _SKIP_PATHS or any(path.startswith(p) for p in _SKIP_PREFIXES): return await call_next(request) start = time.perf_counter() status_code = 500 # default if call_next raises try: response = await call_next(request) status_code = response.status_code return response finally: duration_ms = (time.perf_counter() - start) * 1000 method = request.method user = getattr(request.state, "user", None) ip = request.client.host if request.client else None # Fire-and-forget: never block the response asyncio.create_task( self._write_log(method, path, status_code, duration_ms, user, ip) ) @staticmethod async def _write_log( method: str, path: str, status_code: int, duration_ms: float, user: str | None, ip: str | None, ) -> None: """Write the log entry in a thread executor to avoid blocking.""" try: loop = asyncio.get_event_loop() await loop.run_in_executor( None, lambda: _db_insert(method, path, status_code, duration_ms, user, ip), ) except Exception: # Logging must never break the request logger.debug("Failed to write HTTP log", exc_info=True) def _db_insert( method: str, path: str, status_code: int, duration_ms: float, user: str | None, ip: str | None, ) -> None: """Synchronous DB insert — called inside run_in_executor.""" try: from web.backend.services.db_schema import insert_http_log insert_http_log( method=method, path=path, status_code=status_code, duration_ms=duration_ms, user=user, ip=ip, ) except Exception: logger.debug("DB insert_http_log failed", exc_info=True)