Skip to content

Commit df0d9aa

Browse files
authored
Fix configuration caching and telemetry exporters (#150)
1 parent 998a5c6 commit df0d9aa

File tree

2 files changed

+61
-4
lines changed

2 files changed

+61
-4
lines changed

backend/api/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@
2323
from api.routes_sync import router as sync_router
2424
from api.routes_observability import router as observability_router
2525
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
26+
from prometheus_fastapi_instrumentator import Instrumentator
2627
from api.metrics import observe_http_request
2728

2829
setup_instrumentation()
2930

31+
if bootstrap_instrumentation is not None: # pragma: no branch - simple guard
32+
try:
33+
bootstrap_instrumentation()
34+
except Exception: # pragma: no cover - defensive guard
35+
logger.exception("Failed to bootstrap OpenTelemetry instrumentation")
36+
3037
# Initialize settings
3138
settings = get_settings()
3239

@@ -201,4 +208,4 @@ async def metrics() -> Dict[str, str]:
201208

202209
return {
203210
"detail": "Metrics are exported via OpenTelemetry OTLP; no local payload is available.",
204-
}
211+
}

backend/api/metrics.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
from __future__ import annotations
2+
3+
from typing import Any
4+
25
from prometheus_client import Counter, Histogram
36

7+
_otel_meter: Any | None = None
8+
_http_request_counter: Any | None = None
9+
_http_request_duration_histogram: Any | None = None
10+
_web_vitals_lcp_histogram: Any | None = None
11+
12+
try: # pragma: no cover - optional dependency guard
13+
from opentelemetry import metrics as otel_metrics
14+
except ImportError: # pragma: no cover - optional dependency guard
15+
otel_metrics = None # type: ignore[assignment]
16+
else:
17+
_otel_meter = otel_metrics.get_meter(__name__)
18+
_http_request_counter = _otel_meter.create_counter(
19+
"http.server.request.count",
20+
description="HTTP requests processed by the backend",
21+
unit="1",
22+
)
23+
_http_request_duration_histogram = _otel_meter.create_histogram(
24+
"http.server.duration",
25+
description="Duration of HTTP requests handled by the backend",
26+
unit="s",
27+
)
28+
_web_vitals_lcp_histogram = _otel_meter.create_histogram(
29+
"frontend.web_vitals.lcp",
30+
description="Largest Contentful Paint reported from the frontend",
31+
unit="s",
32+
)
33+
434
REQUEST_LATENCY_BUCKETS = (
535
0.005,
636
0.01,
@@ -86,21 +116,41 @@ def observe_http_request(
86116
service: str,
87117
route: str,
88118
method: str,
89-
status: str,
119+
status_code: int | str,
90120
duration_seconds: float,
91121
) -> None:
92122
"""Record a single HTTP request observation."""
93123

124+
status_label = str(status_code)
94125
http_requests_total.labels(
95-
service=service, route=route, method=method, status=status
126+
service=service, route=route, method=method, status=status_label
96127
).inc()
97128
http_request_duration_seconds.labels(
98-
service=service, route=route, method=method, status=status
129+
service=service, route=route, method=method, status=status_label
99130
).observe(duration_seconds)
100131

132+
if _http_request_counter is not None or _http_request_duration_histogram is not None:
133+
attributes: dict[str, str | int] = {
134+
"service.name": service,
135+
"http.route": route,
136+
"http.method": method,
137+
}
138+
try:
139+
attributes["http.status_code"] = int(status_code)
140+
except (TypeError, ValueError):
141+
# Skip the status code attribute if it cannot be coerced to an int.
142+
pass
143+
144+
if _http_request_counter is not None:
145+
_http_request_counter.add(1, attributes)
146+
if _http_request_duration_histogram is not None:
147+
_http_request_duration_histogram.record(duration_seconds, attributes)
148+
101149

102150
def observe_lcp(*, app: str, seconds: float) -> None:
103151
"""Record a Largest Contentful Paint measurement in seconds."""
104152

105153
web_vitals_lcp.labels(app=app).observe(seconds)
154+
if _web_vitals_lcp_histogram is not None:
155+
_web_vitals_lcp_histogram.record(seconds, {"app": app})
106156

0 commit comments

Comments
 (0)