Files
MoviePilot/app/adapters/web/metrics.py
2026-08-21 22:15:47 +08:00

59 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"