""" Prometheus metrics middleware for FastAPI. Exposes /metrics endpoint in Prometheus text format. Tracks HTTP requests, translations, and file uploads. """ import time import logging from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST logger = logging.getLogger(__name__) # ---- Metrics definitions ---- http_requests_total = Counter( "http_requests_total", "Total HTTP requests", ["method", "path", "status"], ) translation_total = Counter( "translation_total", "Total translations processed", ["provider", "file_type", "status"], ) translation_duration_seconds = Histogram( "translation_duration_seconds", "Translation processing duration in seconds", ["provider", "file_type"], buckets=(0.5, 1, 2, 5, 10, 30, 60, 120, 300), ) file_size_bytes = Histogram( "file_size_bytes", "Uploaded file size in bytes", ["file_type"], buckets=(100_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 25_000_000, 50_000_000), ) # Paths to skip from metrics (noisy health checks) _SKIP_PATHS = {"/health", "/ready", "/metrics", "/favicon.ico"} def record_translation(provider: str, file_type: str, duration: float, status: str = "success"): translation_total.labels(provider=provider, file_type=file_type, status=status).inc() translation_duration_seconds.labels(provider=provider, file_type=file_type).observe(duration) def record_file_size(file_type: str, size_bytes: int): file_size_bytes.labels(file_type=file_type).observe(size_bytes) class PrometheusMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): if request.url.path in _SKIP_PATHS: return await call_next(request) start = time.time() response: Response = await call_next(request) duration = time.time() - start path = request.url.path # Group dynamic paths to avoid label explosion if path.startswith("/api/v1/translations/"): path = "/api/v1/translations/{id}" elif path.startswith("/api/v1/download/"): path = "/api/v1/download/{id}" http_requests_total.labels( method=request.method, path=path, status=str(response.status_code), ).inc() return response def get_metrics() -> Response: body = generate_latest() return Response(content=body, media_type=CONTENT_TYPE_LATEST)