Coverage for metrics_config.py: 69.23%
13 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 17:36 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 17:36 +0000
1"""
2Prometheus Metrics Configuration for FastAPI
4Exposes a /metrics endpoint with standard HTTP metrics:
5- http_requests_total: Counter of requests by method, path, status
6- http_request_duration_seconds: Histogram of request latency
7- http_requests_in_progress: Gauge of concurrent requests
9These metrics are scraped by Grafana Alloy and sent to Grafana Cloud.
11Usage:
12 from metrics_config import setup_metrics
13 setup_metrics(app)
14"""
16import re
18from prometheus_fastapi_instrumentator import Instrumentator
19from prometheus_fastapi_instrumentator.metrics import latency, requests
22def normalize_path(path: str) -> str:
23 """
24 Normalize dynamic path segments to prevent cardinality explosion.
26 Examples:
27 /api/users/123 -> /api/users/{id}
28 /api/matches/abc-def-123 -> /api/matches/{id}
29 /api/teams/5/players/10 -> /api/teams/{id}/players/{id}
30 """
31 # UUID pattern (with or without hyphens)
32 path = re.sub(r"/[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}", "/{id}", path)
33 # Numeric IDs
34 path = re.sub(r"/\d+", "/{id}", path)
35 # Short hex IDs (8+ chars)
36 path = re.sub(r"/[0-9a-f]{8,}", "/{id}", path)
37 return path
40def setup_metrics(app):
41 """
42 Configure Prometheus metrics instrumentation for FastAPI.
44 Adds a /metrics endpoint that exposes:
45 - Request counts by method, path, status code
46 - Request duration histograms
47 - In-progress request gauge
49 Args:
50 app: FastAPI application instance
51 """
52 instrumentator = Instrumentator(
53 should_group_status_codes=False, # Keep exact status codes (200, 201, 500, etc.)
54 should_ignore_untemplated=True, # Ignore requests without route templates
55 should_respect_env_var=False, # Always enable (don't check ENABLE_METRICS env)
56 should_instrument_requests_inprogress=True, # Track concurrent requests
57 excluded_handlers=[
58 "/health",
59 "/healthz",
60 "/ready",
61 "/livez",
62 "/metrics", # Don't instrument the metrics endpoint itself
63 ],
64 inprogress_name="http_requests_in_progress",
65 inprogress_labels=True,
66 )
68 # Add default metrics with path normalization
69 instrumentator.add(
70 requests(
71 metric_name="http_requests",
72 metric_doc="Total HTTP requests",
73 metric_namespace="",
74 metric_subsystem="",
75 should_include_handler=True,
76 should_include_method=True,
77 should_include_status=True,
78 )
79 ).add(
80 latency(
81 metric_name="http_request_duration_seconds",
82 metric_doc="HTTP request duration in seconds",
83 metric_namespace="",
84 metric_subsystem="",
85 should_include_handler=True,
86 should_include_method=True,
87 should_include_status=True,
88 buckets=(0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 10.0),
89 )
90 )
92 # Instrument the app and expose /metrics endpoint
93 instrumentator.instrument(app).expose(
94 app,
95 endpoint="/metrics",
96 include_in_schema=False, # Hide from OpenAPI docs
97 should_gzip=False, # Prometheus prefers uncompressed
98 )
100 return instrumentator