"""Author RAG Chatbot SaaS — Structured Logging Middleware (Pure ASGI). Logs every request with: method, path, status, duration_ms, client_ip. Uses structlog for JSON-formatted structured logging. Uses pure ASGI to avoid BaseHTTPMiddleware request body consumption issues. """ import time import structlog from starlette.types import ASGIApp, Receive, Scope, Send logger = structlog.get_logger(__name__) class LoggingMiddleware: """Log all HTTP requests with timing and status (pure ASGI).""" def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self.app(scope, receive, send) return start_time = time.monotonic() status_code = 500 # Default in case of unhandled error async def send_with_logging(message): nonlocal status_code if message["type"] == "http.response.start": status_code = message["status"] await send(message) try: await self.app(scope, receive, send_with_logging) finally: duration_ms = int((time.monotonic() - start_time) * 1000) path = scope.get("path", "") # Skip health check noise in logs if path != "/health": client = scope.get("client") ip = client[0] if client else "unknown" log = logger.bind( method=scope.get("method", ""), path=path, status=status_code, duration_ms=duration_ms, ip=ip, ) if status_code >= 500: log.error("Request failed") elif status_code >= 400: log.warning("Client error") else: log.info("Request completed")