61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""请求指标收集中间件:计时、慢请求告警、响应时间响应头。"""
|
||
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
|