|
2 | 2 | from collections import deque |
3 | 3 | from threading import Lock |
4 | 4 | from datetime import datetime, timedelta |
| 5 | +from typing import Optional |
| 6 | + |
| 7 | +from flask import g, has_request_context, request |
5 | 8 |
|
6 | 9 | class MetricsService: |
7 | 10 | _instance = None |
@@ -79,14 +82,62 @@ def _build_traffic_series(self, recent_requests, hours=24, now=None): |
79 | 82 |
|
80 | 83 | return buckets |
81 | 84 |
|
82 | | - def track_request(self, provider, status_code, response_time, timestamp=None): |
| 85 | + def _request_context_metadata(self, provider): |
| 86 | + if not has_request_context(): |
| 87 | + return {} |
| 88 | + |
| 89 | + user = getattr(g, "authenticated_user", None) or {} |
| 90 | + rate_limit = getattr(g, "rate_limit", None) or {} |
| 91 | + payload = request.get_json(silent=True) if request.is_json else None |
| 92 | + model = payload.get("model") if isinstance(payload, dict) else None |
| 93 | + if model and ":" not in str(model) and provider: |
| 94 | + model = f"{provider}:{model}" |
| 95 | + |
| 96 | + return { |
| 97 | + "request_id": getattr(g, "request_id", None), |
| 98 | + "user_id": user.get("username") or user.get("id"), |
| 99 | + "api_key_prefix": user.get("api_key_prefix"), |
| 100 | + "model": model, |
| 101 | + "input_tokens": rate_limit.get("input_tokens"), |
| 102 | + "output_tokens": rate_limit.get("output_tokens"), |
| 103 | + "estimated_tokens": rate_limit.get("estimated_tokens"), |
| 104 | + } |
| 105 | + |
| 106 | + def track_request( |
| 107 | + self, |
| 108 | + provider, |
| 109 | + status_code, |
| 110 | + response_time, |
| 111 | + timestamp=None, |
| 112 | + request_id: Optional[str] = None, |
| 113 | + user_id: Optional[str] = None, |
| 114 | + api_key_prefix: Optional[str] = None, |
| 115 | + model: Optional[str] = None, |
| 116 | + input_tokens: Optional[int] = None, |
| 117 | + output_tokens: Optional[int] = None, |
| 118 | + estimated_tokens: Optional[int] = None, |
| 119 | + estimated_cost: Optional[float] = None, |
| 120 | + actual_cost: Optional[float] = None, |
| 121 | + ttft_ms: Optional[float] = None, |
| 122 | + ): |
83 | 123 | """Track a new request""" |
84 | 124 | now = timestamp if timestamp is not None else time.time() |
| 125 | + context_metadata = self._request_context_metadata(provider) |
85 | 126 | self.requests.append({ |
86 | 127 | 'timestamp': now, |
87 | 128 | 'provider': provider, |
88 | 129 | 'status_code': status_code, |
89 | | - 'response_time': response_time |
| 130 | + 'response_time': response_time, |
| 131 | + 'request_id': request_id or context_metadata.get("request_id"), |
| 132 | + 'user_id': user_id or context_metadata.get("user_id"), |
| 133 | + 'api_key_prefix': api_key_prefix or context_metadata.get("api_key_prefix"), |
| 134 | + 'model': model or context_metadata.get("model"), |
| 135 | + 'input_tokens': input_tokens if input_tokens is not None else context_metadata.get("input_tokens"), |
| 136 | + 'output_tokens': output_tokens if output_tokens is not None else context_metadata.get("output_tokens"), |
| 137 | + 'estimated_tokens': estimated_tokens if estimated_tokens is not None else context_metadata.get("estimated_tokens"), |
| 138 | + 'estimated_cost': estimated_cost, |
| 139 | + 'actual_cost': actual_cost, |
| 140 | + 'ttft_ms': ttft_ms, |
90 | 141 | }) |
91 | 142 |
|
92 | 143 | def get_stats(self, hours=24, now=None): |
@@ -248,13 +299,44 @@ def get_recent_failures(self, limit=5, now=None, hours=24): |
248 | 299 | return [ |
249 | 300 | { |
250 | 301 | "time": datetime.fromtimestamp(request["timestamp"]).strftime('%Y-%m-%d %H:%M:%S'), |
| 302 | + "request_id": request.get("request_id"), |
251 | 303 | "provider": request["provider"], |
| 304 | + "model": request.get("model"), |
252 | 305 | "status_code": request["status_code"], |
253 | 306 | "response_time": round(request["response_time"], 2), |
254 | 307 | "status_bucket": self._status_code_bucket(request["status_code"]), |
255 | 308 | } |
256 | 309 | for request in failures |
257 | 310 | ] |
| 311 | + |
| 312 | + def get_request_records(self, limit=100, now=None, hours=24): |
| 313 | + """Return recent request-level records without prompt or response bodies.""" |
| 314 | + current_time = now if now is not None else time.time() |
| 315 | + records = sorted( |
| 316 | + self._get_recent_requests(hours=hours, now=current_time), |
| 317 | + key=lambda item: item["timestamp"], |
| 318 | + reverse=True, |
| 319 | + )[:limit] |
| 320 | + |
| 321 | + return [ |
| 322 | + { |
| 323 | + "time": datetime.fromtimestamp(record["timestamp"]).strftime('%Y-%m-%d %H:%M:%S'), |
| 324 | + "request_id": record.get("request_id"), |
| 325 | + "user_id": record.get("user_id"), |
| 326 | + "api_key_prefix": record.get("api_key_prefix"), |
| 327 | + "provider": record["provider"], |
| 328 | + "model": record.get("model"), |
| 329 | + "status_code": record["status_code"], |
| 330 | + "input_tokens": record.get("input_tokens"), |
| 331 | + "output_tokens": record.get("output_tokens"), |
| 332 | + "estimated_tokens": record.get("estimated_tokens"), |
| 333 | + "estimated_cost": record.get("estimated_cost"), |
| 334 | + "actual_cost": record.get("actual_cost"), |
| 335 | + "response_time": round(record["response_time"], 2), |
| 336 | + "ttft_ms": record.get("ttft_ms"), |
| 337 | + } |
| 338 | + for record in records |
| 339 | + ] |
258 | 340 |
|
259 | 341 | def get_recent_activity(self, limit=10): |
260 | 342 | """Get recent activity for the status page""" |
|
0 commit comments