vector-auditor / src /api /middleware.py
AkshayM
production hardening: circuit breakers, graceful degradation, health checks, parallel uploads, HF Spaces deploy config
fc2fe9f
Raw
History Blame Contribute Delete
3.69 kB
"""HTTP middleware: request ID, structured logging, security, shutdown gate, metrics."""
import logging
import time
import uuid
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from ..observability import get_metrics
from ..shutdown import get_shutdown_manager
logger = logging.getLogger("rga_auditor.middleware")
class RequestLoggingMiddleware(BaseHTTPMiddleware):
"""Log every request with structured fields: method, path, status, duration, request_id."""
async def dispatch(self, request: Request, call_next):
t0 = time.time()
response = await call_next(request)
dt = time.time() - t0
rid = getattr(request.state, "request_id", "?")
logger.info("method=%s path=%s status=%d duration=%.3fs request_id=%s",
request.method, request.url.path, response.status_code, dt, rid)
return response
class RequestContextMiddleware(BaseHTTPMiddleware):
"""Attach a request ID and start time to every request."""
async def dispatch(self, request: Request, call_next):
rid = request.headers.get("X-Request-ID") or uuid.uuid4().hex[:16]
request.state.request_id = rid
t0 = time.time()
response = await call_next(request)
response.headers["X-Request-ID"] = rid
duration_ms = int((time.time() - t0) * 1000)
response.headers["X-Response-Time-Ms"] = str(duration_ms)
return response
class ShutdownGateMiddleware(BaseHTTPMiddleware):
"""During shutdown: 503 on new requests, exempt /health, /readyz, /metrics."""
EXEMPT = {"/health", "/readyz", "/metrics"}
async def dispatch(self, request: Request, call_next):
mgr = get_shutdown_manager()
if mgr.is_shutting_down and request.url.path not in self.EXEMPT:
return JSONResponse({"detail": "server shutting down"}, status_code=503)
return await call_next(request)
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "DENY")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
return response
class MetricsMiddleware(BaseHTTPMiddleware):
"""Count requests and latency. Use route template (not full path) for cardinality control."""
async def dispatch(self, request: Request, call_next):
# resolve route template after dispatch; before we don't know it
mgr = get_shutdown_manager()
if mgr.is_shutting_down and request.url.path not in ShutdownGateMiddleware.EXEMPT:
# Don't count pre-shutdown-rejected requests
return await call_next(request)
try:
await mgr.begin_request()
except RuntimeError:
return JSONResponse({"detail": "server shutting down"}, status_code=503)
try:
t0 = time.time()
response = await call_next(request)
dt = time.time() - t0
endpoint = _route_template(request)
get_metrics().observe_request(request.method, endpoint, response.status_code, dt)
return response
finally:
await mgr.end_request()
def _route_template(request: Request) -> str:
"""Return the route template (e.g. /docs/{doc_id}) to keep cardinality low."""
route = request.scope.get("route")
if route is not None and getattr(route, "path", None):
return route.path
return request.url.path