Spaces:
Running
Running
File size: 3,690 Bytes
fc2fe9f 167c070 fc2fe9f 167c070 fc2fe9f 167c070 fc2fe9f 167c070 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | """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
|