Coverage for qreward / utils / scheduler / telemetry.py: 97.44%

68 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 19:32 +0000

1"""Optional OpenTelemetry integration for schedule metrics export. 

2 

3All OTel imports are guarded by try/except so this module works 

4even when opentelemetry packages are not installed. 

5""" 

6 

7import os 

8import logging 

9from typing import Any, Optional 

10 

11logger = logging.getLogger(__name__) 

12 

13_OTEL_AVAILABLE = False 

14_meter_mod: Any = None 

15_trace_mod: Any = None 

16 

17try: 

18 from opentelemetry import metrics as _meter_mod # type: ignore[no-redef] 

19 from opentelemetry import trace as _trace_mod # type: ignore[no-redef] 

20 _OTEL_AVAILABLE = True 

21except ImportError: 

22 pass 

23 

24_ENV_ENABLED_KEY = "QREWARD_OTEL_ENABLED" 

25 

26 

27class TelemetryExporter: 

28 """Exports ScheduleMetrics to OpenTelemetry counters, histograms and spans. 

29 

30 When OTel SDK is not installed or disabled via environment variable, 

31 all methods are no-ops. 

32 

33 Example: 

34 >>> exporter = TelemetryExporter() 

35 >>> exporter.record(metrics) # records counters + histogram 

36 >>> with exporter.span("my_func") as span: 

37 ... span.set_attribute("retry_count", 3) 

38 """ 

39 

40 def __init__( 

41 self, 

42 meter_name: str = "qreward.scheduler", 

43 tracer_name: str = "qreward.scheduler", 

44 ) -> None: 

45 self._enabled = self._check_enabled() 

46 self._meter: Any = None 

47 self._tracer: Any = None 

48 self._total_calls_counter: Any = None 

49 self._success_counter: Any = None 

50 self._failure_counter: Any = None 

51 self._latency_histogram: Any = None 

52 

53 if self._enabled: 

54 self._meter = _meter_mod.get_meter(meter_name) 

55 self._tracer = _trace_mod.get_tracer(tracer_name) 

56 self._total_calls_counter = self._meter.create_counter( 

57 "qreward.schedule.total_calls", 

58 description="Total number of schedule invocations", 

59 ) 

60 self._success_counter = self._meter.create_counter( 

61 "qreward.schedule.success_count", 

62 description="Number of successful schedule completions", 

63 ) 

64 self._failure_counter = self._meter.create_counter( 

65 "qreward.schedule.failure_count", 

66 description="Number of failed schedule attempts", 

67 ) 

68 self._latency_histogram = self._meter.create_histogram( 

69 "qreward.schedule.latency_ms", 

70 unit="ms", 

71 description="Schedule invocation latency in milliseconds", 

72 ) 

73 

74 @classmethod 

75 def is_available(cls) -> bool: 

76 """Check if OpenTelemetry SDK is installed and enabled.""" 

77 return _OTEL_AVAILABLE and cls._check_enabled() 

78 

79 @staticmethod 

80 def _check_enabled() -> bool: 

81 """Check if OTel is available and not disabled by env var.""" 

82 if not _OTEL_AVAILABLE: 

83 return False 

84 env_val = os.environ.get(_ENV_ENABLED_KEY, "true").lower() 

85 return env_val not in ("false", "0", "no", "off") 

86 

87 def record(self, metrics: Any) -> None: 

88 """Record schedule metrics to OTel counters and histogram. 

89 

90 Args: 

91 metrics: A ScheduleMetrics instance. 

92 """ 

93 if not self._enabled: 

94 return 

95 self._total_calls_counter.add(metrics.total_calls) 

96 self._success_counter.add(metrics.success_count) 

97 self._failure_counter.add(metrics.failure_count) 

98 self._latency_histogram.record(metrics.total_latency_ms) 

99 

100 def start_span( 

101 self, name: str, attributes: Optional[dict[str, Any]] = None 

102 ) -> Any: 

103 """Start a new OTel span. 

104 

105 Args: 

106 name: Span name (typically the decorated function name). 

107 attributes: Optional span attributes. 

108 

109 Returns: 

110 A span object, or a no-op context if OTel is disabled. 

111 """ 

112 if not self._enabled or self._tracer is None: 

113 return _NoOpSpan() 

114 return self._tracer.start_span(name, attributes=attributes) 

115 

116 def end_span(self, span: Any, metrics: Any) -> None: 

117 """End a span and attach metrics as attributes. 

118 

119 Args: 

120 span: The span to end. 

121 metrics: A ScheduleMetrics instance. 

122 """ 

123 if not self._enabled or isinstance(span, _NoOpSpan): 

124 return 

125 span.set_attribute("qreward.total_calls", metrics.total_calls) 

126 span.set_attribute( 

127 "qreward.success_count", metrics.success_count 

128 ) 

129 span.set_attribute( 

130 "qreward.failure_count", metrics.failure_count 

131 ) 

132 span.set_attribute("qreward.retry_count", metrics.retry_count) 

133 span.set_attribute( 

134 "qreward.total_latency_ms", metrics.total_latency_ms 

135 ) 

136 span.end() 

137 

138 

139class _NoOpSpan: 

140 """No-op span used when OTel is not available.""" 

141 

142 def set_attribute(self, key: str, value: Any) -> None: 

143 pass 

144 

145 def end(self) -> None: 

146 pass 

147 

148 def __enter__(self) -> "_NoOpSpan": 

149 return self 

150 

151 def __exit__(self, *args: Any) -> None: 

152 pass