forked from sagnik/Project_Velocity
Merge Conflicts (#41)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#41
This commit is contained in:
103
backend/observability.py
Normal file
103
backend/observability.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from collections import deque
|
||||
from dataclasses import asdict, dataclass
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
logger = logging.getLogger("velocity.observability")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RequestMetric:
|
||||
request_id: str
|
||||
method: str
|
||||
path: str
|
||||
status_code: int
|
||||
duration_ms: float
|
||||
|
||||
|
||||
class RequestObservabilityMiddleware(BaseHTTPMiddleware):
|
||||
def __init__(self, app, *, max_metrics: int = 500) -> None:
|
||||
super().__init__(app)
|
||||
self.max_metrics = max_metrics
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
|
||||
request.state.request_id = request_id
|
||||
started = time.perf_counter()
|
||||
status_code = 500
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status_code = response.status_code
|
||||
return self._finalize(request, response, request_id, started, status_code)
|
||||
except Exception:
|
||||
duration_ms = (time.perf_counter() - started) * 1000
|
||||
self._record_metric(request, request_id, status_code, duration_ms)
|
||||
logger.exception(
|
||||
"request_failed",
|
||||
extra={
|
||||
"request_id": request_id,
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"duration_ms": round(duration_ms, 2),
|
||||
},
|
||||
)
|
||||
raise
|
||||
|
||||
def _finalize(
|
||||
self,
|
||||
request: Request,
|
||||
response: Response,
|
||||
request_id: str,
|
||||
started: float,
|
||||
status_code: int,
|
||||
) -> Response:
|
||||
duration_ms = (time.perf_counter() - started) * 1000
|
||||
response.headers["X-Request-ID"] = request_id
|
||||
response.headers["X-Response-Time-Ms"] = f"{duration_ms:.2f}"
|
||||
self._record_metric(request, request_id, status_code, duration_ms)
|
||||
logger.info(
|
||||
"request_completed",
|
||||
extra={
|
||||
"request_id": request_id,
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status_code": status_code,
|
||||
"duration_ms": round(duration_ms, 2),
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
||||
def _record_metric(
|
||||
self,
|
||||
request: Request,
|
||||
request_id: str,
|
||||
status_code: int,
|
||||
duration_ms: float,
|
||||
) -> None:
|
||||
metrics = getattr(request.app.state, "request_metrics", None)
|
||||
if metrics is None:
|
||||
metrics = deque(maxlen=self.max_metrics)
|
||||
request.app.state.request_metrics = metrics
|
||||
metrics.append(
|
||||
RequestMetric(
|
||||
request_id=request_id,
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status_code=status_code,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def metrics_snapshot(app, *, limit: int = 50) -> list[dict]:
|
||||
metrics = getattr(app.state, "request_metrics", deque())
|
||||
return [asdict(metric) for metric in list(metrics)[-limit:]][::-1]
|
||||
|
||||
Reference in New Issue
Block a user