"""Middleware that logs every HTTP request with user context and timing.""" import logging import time import uuid from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request logger = logging.getLogger("pedshub.requests") # Paths to skip logging (noisy or uninteresting) SKIP_PATHS = {"/api/health", "/favicon.ico", "/robots.txt"} SKIP_PREFIXES = ("/uploads/",) class RequestLoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): path = request.url.path # Skip noisy paths if path in SKIP_PATHS or any(path.startswith(p) for p in SKIP_PREFIXES): return await call_next(request) request_id = uuid.uuid4().hex[:8] request.state.request_id = request_id # Extract user from JWT (best-effort, never block) user_id = None user_email = None auth = request.headers.get("authorization", "") if auth.startswith("Bearer "): try: from jose import jwt from app.config import settings payload = jwt.decode( auth[7:], settings.SECRET_KEY, algorithms=[settings.ALGORITHM], options={"verify_exp": False}, ) user_email = payload.get("sub") except Exception: pass start = time.perf_counter() response = await call_next(request) duration_ms = round((time.perf_counter() - start) * 1000, 1) status = response.status_code method = request.method client_ip = request.client.host if request.client else "-" log_data = { "request_id": request_id, "method": method, "path": path, "status": status, "duration_ms": duration_ms, "client_ip": client_ip, } if user_email: log_data["user"] = user_email if status >= 500: logger.error("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data) elif status >= 400: logger.warning("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data) else: logger.info("%(method)s %(path)s %(status)s %(duration_ms)sms", log_data, extra=log_data) return response