mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-15 04:44:39 +08:00
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""HTTP route/status/latency 指标 ASGI 适配器。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from typing import Any
|
||
|
||
from app.runtime.observability import record_metric
|
||
from starlette.routing import Match
|
||
|
||
|
||
class HttpMetricsMiddleware:
|
||
"""按路由模板、方法和状态码记录低基数 HTTP 时延。"""
|
||
|
||
def __init__(self, app: Any) -> None:
|
||
"""保存下游 ASGI 应用。"""
|
||
self._app = app
|
||
|
||
async def __call__(self, scope: dict, receive: Any, send: Any) -> None:
|
||
"""只治理 HTTP scope,并在响应开始后读取路由模板。"""
|
||
if scope.get("type") != "http":
|
||
await self._app(scope, receive, send)
|
||
return
|
||
started_at = time.perf_counter()
|
||
status = "500"
|
||
|
||
async def send_with_metrics(message: dict) -> None:
|
||
"""捕获响应状态并原样转发 ASGI 消息。"""
|
||
nonlocal status
|
||
if message.get("type") == "http.response.start":
|
||
status = str(message.get("status", 500))
|
||
await send(message)
|
||
|
||
try:
|
||
await self._app(scope, receive, send_with_metrics)
|
||
finally:
|
||
route_path = self._resolve_route_template(scope)
|
||
record_metric(
|
||
"http.server.duration",
|
||
time.perf_counter() - started_at,
|
||
route=route_path,
|
||
method=str(scope.get("method", "UNKNOWN")),
|
||
status=status,
|
||
)
|
||
|
||
def _resolve_route_template(self, scope: dict) -> str:
|
||
"""遍历 ASGI wrapper 找到匹配路由模板,绝不回退到具体请求 path。"""
|
||
route = scope.get("route")
|
||
if getattr(route, "path", None):
|
||
return route.path
|
||
candidate = self._app
|
||
while candidate is not None:
|
||
for registered_route in getattr(candidate, "routes", ()):
|
||
match, _ = registered_route.matches(scope)
|
||
if match == Match.FULL:
|
||
return getattr(registered_route, "path", "unmatched")
|
||
candidate = getattr(candidate, "app", None)
|
||
return "unmatched"
|