"""接口指标埋点:有界事件队列 + 纯 ASGI 中间件。 每个 HTTP 请求测总耗时、抓路由模板 + 状态码,非阻塞塞进有界队列;由 observe_worker 后台批量上报到 OpenObserve。请求路径上无任何 I/O。未配置观测时中间件直接透传。 """ from __future__ import annotations import asyncio import os import time from starlette.routing import Match from app.core.config import settings # 不采集的路径(纯噪音):健康检查。 _SKIP_PATHS = frozenset({"/health"}) # 未匹配路由(404/扫描器)归一到此,防维度爆炸。 _UNMATCHED = "__unmatched__" # service 字段:与 logging.py 同源(LOG_SERVICE_NAME),默认 app-server。 _SERVICE = os.getenv("LOG_SERVICE_NAME", "app-server") # 有界事件队列(懒创建,见 get_queue):首次取用时在运行中的 loop 里建,避免 import 期 # 无 loop 的边角问题;put_nowait/get_nowait 不需运行中的 loop → 可在无 loop 下测试。 _queue: asyncio.Queue[dict] | None = None # 队列满时的丢弃计数,worker 定期取出打日志。 _dropped = 0 def get_queue() -> asyncio.Queue[dict]: """返回全局有界事件队列(懒创建)。测试可 monkeypatch 模块级 _queue 换成小队列。""" global _queue if _queue is None: _queue = asyncio.Queue(maxsize=settings.OBSERVE_QUEUE_MAX) return _queue def take_dropped() -> int: """取出并清零累计丢弃数(供 worker 打点)。""" global _dropped n, _dropped = _dropped, 0 return n def record_event(event: dict) -> None: """非阻塞入队;队列满则丢弃当前事件并计数。永不抛异常、永不阻塞请求。""" global _dropped try: get_queue().put_nowait(event) except asyncio.QueueFull: _dropped += 1 def _resolve_route(scope) -> str: """从 scope 取路由模板(如 /things/{tid})。优先 scope['route'](现代 Starlette 路由后写入);取不到则手动匹配一次(老版本兜底);仍无 → __unmatched__(404/扫描器)。""" route = scope.get("route") path = getattr(route, "path", None) if path: return path app_ = scope.get("app") router = getattr(app_, "router", None) for candidate in getattr(router, "routes", []): try: match, _ = candidate.matches(scope) except Exception: # noqa: BLE001 - 匹配兜底,任一路由异常不影响整体 continue if match == Match.FULL and getattr(candidate, "path", None): return candidate.path return _UNMATCHED class RequestMetricsMiddleware: """纯 ASGI 中间件:测每个 http 请求耗时,记 method/route/status/duration。 放在最外层(main.py 里 CORS 之后 add),测到含 CORS 的完整耗时。未配置观测 → 透传。 """ def __init__(self, app) -> None: self.app = app async def __call__(self, scope, receive, send) -> None: if scope["type"] != "http" or not settings.observe_configured: await self.app(scope, receive, send) return if scope.get("path") in _SKIP_PATHS: await self.app(scope, receive, send) return start = time.perf_counter() status_holder = {"status": 500} # 下游异常未产出 response 时兜底 500 async def send_wrapper(message) -> None: if message["type"] == "http.response.start": status_holder["status"] = message["status"] await send(message) try: await self.app(scope, receive, send_wrapper) finally: duration_ms = (time.perf_counter() - start) * 1000.0 record_event({ "_timestamp": int(time.time() * 1_000_000), # µs,OpenObserve 时间列 "service": _SERVICE, "env": settings.APP_ENV, "method": scope.get("method", ""), "route": _resolve_route(scope), "status": status_holder["status"], "duration_ms": round(duration_ms, 3), })