feat: add Prometheus metrics + fix CI/CD health check port
- Add prometheus-client dependency - Create middleware/metrics.py with PrometheusMiddleware - Expose /metrics endpoint in Prometheus text format - Track http_requests_total, translation_total, translation_duration_seconds, file_size_bytes - Instrument translate routes with record_translation() and record_file_size() - Fix deploy.yml health check: localhost:8000 -> localhost:8001 (Portainer conflict) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
85
middleware/metrics.py
Normal file
85
middleware/metrics.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
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_LSP
|
||||
|
||||
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_LSP)
|
||||
Reference in New Issue
Block a user