Coverage for src/lumen/observability/metrics.py: 100%
26 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1"""Thin, library-agnostic wrapper over the OTel metrics API for domain metrics.
3These helpers are safe to call regardless of whether telemetry is enabled: when
4no ``MeterProvider`` has been configured the OpenTelemetry API hands back no-op
5instruments, so recording adds negligible overhead and never raises. Instruments
6are created lazily and cached; the OTel proxy meter re-binds them to the real
7provider once ``setup_telemetry`` runs.
9PII rule: only resource *types* and interaction *names* are used as attributes —
10never resource IDs, patient data or search parameter values.
11"""
13from __future__ import annotations
15from dataclasses import dataclass
16from functools import lru_cache
18from opentelemetry import metrics
19from opentelemetry.metrics import Counter, Histogram
22@dataclass(frozen=True)
23class _Instruments:
24 interactions: Counter
25 search_duration: Histogram
26 validation_errors: Counter
27 hook_duration: Histogram
28 hook_failures: Counter
31@lru_cache(maxsize=1)
32def _instruments() -> _Instruments:
33 meter = metrics.get_meter("lumen.domain")
34 return _Instruments(
35 interactions=meter.create_counter(
36 "lumen.fhir.interactions",
37 unit="1",
38 description="FHIR interactions by resource type and interaction",
39 ),
40 search_duration=meter.create_histogram(
41 "lumen.fhir.search.duration",
42 unit="ms",
43 description="FHIR search execution duration",
44 ),
45 validation_errors=meter.create_counter(
46 "lumen.fhir.validation.errors",
47 unit="1",
48 description="Inbound resource validation failures by resource type",
49 ),
50 hook_duration=meter.create_histogram(
51 "lumen.cortex.hook.duration",
52 unit="ms",
53 description="Cortex write-time hook execution duration",
54 ),
55 hook_failures=meter.create_counter(
56 "lumen.cortex.hook.failures",
57 unit="1",
58 description="Cortex hook failures by plugin and reason (timeout, error)",
59 ),
60 )
63def record_interaction(resource_type: str, interaction: str) -> None:
64 """Count one FHIR interaction (e.g. read, create, update, delete, search)."""
65 _instruments().interactions.add(1, {"resource_type": resource_type, "interaction": interaction})
68def record_search_duration(resource_type: str, duration_ms: float) -> None:
69 """Record the wall-clock duration of a search execution in milliseconds."""
70 _instruments().search_duration.record(duration_ms, {"resource_type": resource_type})
73def record_validation_error(resource_type: str) -> None:
74 """Count one inbound validation failure for a resource type."""
75 _instruments().validation_errors.add(1, {"resource_type": resource_type})
78def record_hook_duration(plugin: str, event: str, duration_ms: float) -> None:
79 """Record how long one Cortex hook invocation took."""
80 _instruments().hook_duration.record(duration_ms, {"plugin": plugin, "event": event})
83def record_hook_failure(plugin: str, event: str, reason: str) -> None:
84 """Count one Cortex hook that timed out or raised."""
85 _instruments().hook_failures.add(1, {"plugin": plugin, "event": event, "reason": reason})