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

1"""Thin, library-agnostic wrapper over the OTel metrics API for domain metrics. 

2 

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. 

8 

9PII rule: only resource *types* and interaction *names* are used as attributes — 

10never resource IDs, patient data or search parameter values. 

11""" 

12 

13from __future__ import annotations 

14 

15from dataclasses import dataclass 

16from functools import lru_cache 

17 

18from opentelemetry import metrics 

19from opentelemetry.metrics import Counter, Histogram 

20 

21 

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 

29 

30 

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 ) 

61 

62 

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}) 

66 

67 

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}) 

71 

72 

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}) 

76 

77 

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}) 

81 

82 

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})