"""请求指标收集中间件:计时、慢请求告警、响应时间响应头。""" import time import logging from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response logger = logging.getLogger("geo.metrics") # 慢请求阈值(秒) SLOW_REQUEST_THRESHOLD = 1.0 # 跳过指标收集的路径前缀(健康检查等高频低价值路径) _SKIP_PATHS = {"/health", "/ready", "/docs", "/openapi.json", "/favicon.ico"} class MetricsMiddleware(BaseHTTPMiddleware): """记录每个 HTTP 请求的耗时,并: - 在响应头写入 X-Response-Time - 对超过阈值的慢请求输出 WARNING 日志(携带结构化字段) - 预留 Sentry / Prometheus 集成点(TODO 注释标注) """ async def dispatch(self, request: Request, call_next) -> Response: # 跳过健康检查等低价值路径,避免日志噪音 if request.url.path in _SKIP_PATHS: return await call_next(request) start_time = time.perf_counter() response = await call_next(request) duration = time.perf_counter() - start_time duration_ms = round(duration * 1000, 2) # 写回响应时间响应头 response.headers["X-Response-Time"] = f"{duration:.3f}s" # 从 request.state 获取 request_id(由 RequestIdMiddleware 注入) request_id = getattr(request.state, "request_id", None) log_extra: dict = { "path": request.url.path, "method": request.method, "duration_ms": duration_ms, "status_code": response.status_code, } if request_id: log_extra["request_id"] = request_id if duration >= SLOW_REQUEST_THRESHOLD: logger.warning("Slow request detected", extra=log_extra) else: logger.debug("Request completed", extra=log_extra) # TODO: 集成 Prometheus Counter/Histogram # metrics_registry.http_request_duration.observe(duration, labels={...}) # TODO: 集成 Sentry 性能监控 # if sentry_sdk: sentry_sdk.set_measurement("response_time_ms", duration_ms) return response