Coverage for src / lexigram / contracts / observability / metrics.py: 0%

40 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-15 18:57 +0800

1"""Monitoring protocols. 

2 

3Protocols for metrics, tracing, and health checking. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

9 

10from lexigram.contracts.core.health import HealthCheckCategory 

11 

12if TYPE_CHECKING: 

13 from collections.abc import Callable 

14 

15 

16@runtime_checkable 

17class AlertDispatcherProtocol(Protocol): 

18 """Protocol for dispatching operational alerts. 

19 

20 Implementations route alerts to notification channels (logging, 

21 PagerDuty, Slack, etc.). ``lexigram-monitor`` ships a built-in 

22 :class:`~lexigram.monitor.alerts.LoggingAlertDispatcher` that writes 

23 alerts to the structured logger. 

24 

25 Example:: 

26 

27 class SlackAlertDispatcher: 

28 async def send_alert( 

29 self, 

30 title: str, 

31 message: str, 

32 severity: str, 

33 context: dict[str, Any] | None = None, 

34 ) -> None: 

35 await self._slack.post( 

36 channel="#ops", 

37 text=f"[{severity}] {title}: {message}", 

38 ) 

39 """ 

40 

41 async def send_alert( 

42 self, 

43 title: str, 

44 message: str, 

45 severity: str, 

46 context: dict[str, Any] | None = None, 

47 ) -> None: 

48 """Dispatch a free-form operational alert. 

49 

50 Args: 

51 title: Short, human-readable alert title. 

52 message: Detailed alert message. 

53 severity: Severity level string, e.g. ``"low"``, ``"high"``, 

54 ``"critical"``. 

55 context: Optional free-form mapping of additional metadata. 

56 """ 

57 ... 

58 

59 async def send_metric_alert( 

60 self, 

61 metric_name: str, 

62 current_value: float, 

63 threshold: float, 

64 context: dict[str, Any] | None = None, 

65 ) -> None: 

66 """Dispatch an alert triggered by a metric threshold breach. 

67 

68 Args: 

69 metric_name: Name of the metric that breached its threshold. 

70 current_value: Observed metric value at the time of the alert. 

71 threshold: The configured threshold that was exceeded. 

72 context: Optional free-form mapping of additional metadata. 

73 """ 

74 ... 

75 

76 

77@runtime_checkable 

78class MetricsRecorderProtocol(Protocol): 

79 """Record pre-defined metrics. Minimal interface for resilience, events, etc.""" 

80 

81 def increment( 

82 self, 

83 name: str, 

84 value: float = 1.0, 

85 tags: dict[str, str] | None = None, 

86 ) -> None: 

87 """Increment a counter metric. 

88 

89 Args: 

90 name: MetricProtocol name. 

91 value: Value to increment by. 

92 tags: Optional tags/labels. 

93 """ 

94 ... 

95 

96 def gauge( 

97 self, 

98 name: str, 

99 value: float, 

100 tags: dict[str, str] | None = None, 

101 ) -> None: 

102 """Set a gauge metric. 

103 

104 Args: 

105 name: MetricProtocol name. 

106 value: Current value. 

107 tags: Optional tags/labels. 

108 """ 

109 ... 

110 

111 def histogram( 

112 self, 

113 name: str, 

114 value: float, 

115 tags: dict[str, str] | None = None, 

116 ) -> None: 

117 """Record a histogram value. 

118 

119 Args: 

120 name: MetricProtocol name. 

121 value: Value to record. 

122 tags: Optional tags/labels. 

123 """ 

124 ... 

125 

126 

127@runtime_checkable 

128class MetricsFactoryProtocol(Protocol): 

129 """Create metric instruments. Extended interface for lexigram-monitor.""" 

130 

131 def register_metric(self, metric: MetricProtocol) -> None: 

132 """Register an existing metric instrument. 

133 

134 Args: 

135 metric: Pre-defined metric instance. 

136 """ 

137 ... 

138 

139 def create_counter( 

140 self, 

141 name: str, 

142 description: str = "", 

143 labels: dict[str, str] | None = None, 

144 ) -> Any: 

145 """Create a counter metric. 

146 

147 Args: 

148 name: MetricProtocol name. 

149 description: MetricProtocol description. 

150 labels: Default labels. 

151 

152 Returns: 

153 Counter metric instance. 

154 """ 

155 ... 

156 

157 def create_gauge( 

158 self, 

159 name: str, 

160 description: str = "", 

161 labels: dict[str, str] | None = None, 

162 ) -> Any: 

163 """Create a gauge metric. 

164 

165 Args: 

166 name: MetricProtocol name. 

167 description: MetricProtocol description. 

168 labels: Default labels. 

169 

170 Returns: 

171 Gauge metric instance. 

172 """ 

173 ... 

174 

175 def create_histogram( 

176 self, 

177 name: str, 

178 description: str = "", 

179 labels: dict[str, str] | None = None, 

180 buckets: list[float] | None = None, 

181 ) -> Any: 

182 """Create a histogram metric. 

183 

184 Args: 

185 name: MetricProtocol name. 

186 description: MetricProtocol description. 

187 labels: Default labels. 

188 buckets: Histogram buckets. 

189 

190 Returns: 

191 Histogram metric instance. 

192 """ 

193 ... 

194 

195 

196@runtime_checkable 

197class MetricProtocol(Protocol): 

198 """Protocol for metric implementations. 

199 

200 Metrics track numeric measurements over time. 

201 """ 

202 

203 @property 

204 def name(self) -> str: 

205 """MetricProtocol name.""" 

206 ... 

207 

208 @property 

209 def description(self) -> str: 

210 """MetricProtocol description.""" 

211 ... 

212 

213 def record(self, value: float, labels: dict[str, str] | None = None) -> None: 

214 """Record a metric value. 

215 

216 Args: 

217 value: Numeric value to record. 

218 labels: Optional labels/tags. 

219 """ 

220 ... 

221 

222 

223@runtime_checkable 

224class MetricsBackendProtocol(Protocol): 

225 """Protocol for metrics backend implementations (metrics only). 

226 

227 Backends export metrics to external systems. 

228 """ 

229 

230 async def initialize(self) -> None: 

231 """Initialize the metrics backend.""" 

232 ... 

233 

234 async def shutdown(self) -> None: 

235 """Shutdown the metrics backend.""" 

236 ... 

237 

238 def record_metric( 

239 self, 

240 name: str, 

241 value: Any, 

242 metric_type: str, 

243 labels: dict[str, str] | None = None, 

244 ) -> None: 

245 """Record a metric value. 

246 

247 Args: 

248 name: MetricProtocol name. 

249 value: MetricProtocol value. 

250 metric_type: Type of metric (counter, gauge, histogram). 

251 labels: Optional labels. 

252 """ 

253 ... 

254 

255 

256@runtime_checkable 

257class MetricsCollectorProtocol( 

258 MetricsRecorderProtocol, MetricsFactoryProtocol, Protocol 

259): 

260 """Full metrics capability. Implemented by lexigram-monitor. 

261 

262 Combines recording capabilities (increment, gauge, histogram) with 

263 factory capabilities (create_counter, create_gauge, create_histogram). 

264 """ 

265 

266 

267@runtime_checkable 

268class HealthCheckRegistryProtocol(Protocol): 

269 """Protocol for a categorised health check registry. 

270 

271 Implementations (e.g. ``HealthChecker``) store checks tagged with a 

272 :class:`~lexigram.contracts.core.health.HealthCheckCategory` so that 

273 callers can query subsets independently: 

274 

275 * ``run_liveness`` — is the process alive and not deadlocked? 

276 * ``run_readiness`` — is the process ready to accept traffic? 

277 * ``run_startup`` — has initial startup completed? 

278 

279 This maps directly to the three Kubernetes probe types. 

280 """ 

281 

282 def add( 

283 self, 

284 name: str, 

285 check: Callable[[], Any], 

286 *, 

287 timeout: float | None = None, 

288 critical: bool = True, 

289 category: HealthCheckCategory = HealthCheckCategory.READINESS, 

290 ) -> None: 

291 """Register a categorised health check. 

292 

293 Args: 

294 name: Unique identifier for the check. 

295 check: Callable that performs the check. 

296 timeout: Optional per-check timeout in seconds. 

297 critical: Whether a non-healthy result should make the aggregate 

298 readiness status ``UNHEALTHY``. Defaults to ``True``. 

299 category: :class:`~lexigram.contracts.core.health.HealthCheckCategory` 

300 value. Defaults to ``READINESS``. 

301 """ 

302 ... 

303 

304 async def run_all(self) -> tuple[Any, dict[str, Any]]: 

305 """Run all registered checks regardless of category. 

306 

307 Returns: 

308 ``(aggregate_status, per_check_results)`` tuple. 

309 """ 

310 ... 

311 

312 async def run_liveness(self) -> tuple[Any, dict[str, Any]]: 

313 """Run only LIVENESS checks. 

314 

315 Returns: 

316 ``(aggregate_status, per_check_results)`` tuple. 

317 """ 

318 ... 

319 

320 async def run_readiness(self) -> tuple[Any, dict[str, Any]]: 

321 """Run only READINESS checks. 

322 

323 Returns: 

324 ``(aggregate_status, per_check_results)`` tuple. 

325 """ 

326 ... 

327 

328 async def run_startup(self) -> tuple[Any, dict[str, Any]]: 

329 """Run only STARTUP checks. 

330 

331 Returns: 

332 ``(aggregate_status, per_check_results)`` tuple. 

333 """ 

334 ... 

335 

336 

337__all__ = [ 

338 "AlertDispatcherProtocol", 

339 "HealthCheckRegistryProtocol", 

340 "MetricProtocol", 

341 "MetricsBackendProtocol", 

342 "MetricsCollectorProtocol", 

343 "MetricsFactoryProtocol", 

344 "MetricsRecorderProtocol", 

345]