Coverage for fastblocks/_health_integration.py: 0%

202 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

1"""ACB HealthService integration for FastBlocks. 

2 

3This module bridges FastBlocks components with ACB's comprehensive health monitoring system. 

4It registers FastBlocks-specific health checks while maintaining existing MCP health checks. 

5 

6Author: lesleslie <les@wedgwoodwebworks.com> 

7Created: 2025-10-01 

8""" 

9# type: ignore # ACB health service API stub - graceful degradation 

10 

11import typing as t 

12from contextlib import suppress 

13from uuid import UUID 

14 

15from acb.adapters import AdapterStatus 

16from acb.depends import Inject, depends 

17 

18# Optional ACB health imports (graceful degradation if not available) 

19try: 

20 from acb.services.health import ( 

21 HealthCheckMixin, 

22 HealthCheckResult, 

23 HealthCheckType, 

24 HealthStatus, 

25 ) 

26 

27 ACB_HEALTH_AVAILABLE = True 

28except ImportError: 

29 ACB_HEALTH_AVAILABLE = False 

30 HealthCheckMixin = object # Fallback base class 

31 HealthCheckResult = None 

32 HealthCheckType = None 

33 HealthStatus = None 

34 

35 

36class FastBlocksHealthCheck(HealthCheckMixin): # type: ignore[misc] 

37 """Base health check implementation for FastBlocks components.""" 

38 

39 @depends.inject # type: ignore[misc] 

40 def __init__( 

41 self, 

42 config: Inject[t.Any] = depends(), 

43 component_id: str | None = None, 

44 component_name: str | None = None, 

45 ) -> None: 

46 if ACB_HEALTH_AVAILABLE: 

47 super().__init__() 

48 self.config = config 

49 self._component_id: str = component_id or self.__class__.__name__.lower() 

50 self._component_name: str = component_name or self.__class__.__name__ 

51 

52 @property 

53 def component_id(self) -> str: 

54 """Get unique identifier for this component.""" 

55 return self._component_id 

56 

57 @property 

58 def component_name(self) -> str: 

59 """Get human-readable name for this component.""" 

60 return self._component_name 

61 

62 async def _perform_health_check( 

63 self, 

64 check_type: t.Any, # HealthCheckType when available 

65 ) -> t.Any: # HealthCheckResult when available 

66 """Default health check - override in subclasses.""" 

67 if not ACB_HEALTH_AVAILABLE: 

68 return None 

69 

70 return HealthCheckResult( 

71 component_id=self.component_id, 

72 component_name=self.component_name, 

73 status=HealthStatus.HEALTHY, 

74 check_type=check_type, 

75 message=f"{self.component_name} is operational", 

76 ) 

77 

78 

79class TemplatesHealthCheck(FastBlocksHealthCheck): 

80 """Health check for FastBlocks template system.""" 

81 

82 def __init__(self) -> None: 

83 super().__init__( 

84 component_id="templates", 

85 component_name="Template System", 

86 ) 

87 

88 async def _perform_health_check( 

89 self, 

90 check_type: t.Any, 

91 ) -> t.Any: 

92 """Check template system health.""" 

93 if not ACB_HEALTH_AVAILABLE: 

94 return None 

95 

96 details: dict[str, t.Any] = {} 

97 status = HealthStatus.HEALTHY 

98 message = "Template system operational" 

99 

100 try: 

101 # Try to get templates adapter 

102 templates = depends.get("templates") 

103 

104 if templates is None: 

105 status = HealthStatus.DEGRADED 

106 message = "Templates adapter not initialized" 

107 else: 

108 # Check if templates has required attributes 

109 if hasattr(templates, "app") and templates.app is not None: 

110 details["jinja_env_initialized"] = True 

111 

112 # Check template directory accessibility 

113 if hasattr(templates.app, "env") and templates.app.env.loader: 

114 details["loader_available"] = True 

115 else: 

116 status = HealthStatus.DEGRADED 

117 message = "Template loader not configured" 

118 else: 

119 status = HealthStatus.DEGRADED 

120 message = "Template app not initialized" 

121 

122 # Check cache availability 

123 try: 

124 cache = depends.get("cache") 

125 details["cache_available"] = cache is not None 

126 except Exception: 

127 details["cache_available"] = False 

128 

129 except Exception as e: 

130 status = HealthStatus.UNHEALTHY 

131 message = f"Template health check failed: {e}" 

132 details["error"] = str(e) 

133 

134 return HealthCheckResult( 

135 component_id=self.component_id, 

136 component_name=self.component_name, 

137 status=status, 

138 check_type=check_type, 

139 message=message, 

140 details=details, 

141 ) 

142 

143 

144class CacheHealthCheck(FastBlocksHealthCheck): 

145 """Health check for FastBlocks cache system.""" 

146 

147 def __init__(self) -> None: 

148 super().__init__( 

149 component_id="cache", 

150 component_name="Cache System", 

151 ) 

152 

153 async def _test_cache_operations( 

154 self, cache: t.Any, details: dict[str, t.Any] 

155 ) -> tuple[t.Any, str]: # Returns (status, message) 

156 """Test cache read/write operations and update details.""" 

157 test_key = "__fastblocks_health_check__" 

158 test_value = "health_check_ok" 

159 

160 try: 

161 # Test set operation 

162 await cache.set(test_key, test_value, ttl=10) 

163 details["write_test"] = "passed" 

164 

165 # Test get operation 

166 retrieved = await cache.get(test_key) 

167 if retrieved == test_value: 

168 details["read_test"] = "passed" 

169 await cache.delete(test_key) 

170 return HealthStatus.HEALTHY, "Cache system operational" 

171 

172 details["read_test"] = "failed" 

173 return HealthStatus.DEGRADED, "Cache read verification failed" 

174 

175 except Exception as e: 

176 details["operation_error"] = str(e) 

177 return HealthStatus.DEGRADED, f"Cache operations failed: {e}" 

178 

179 async def _collect_cache_stats( 

180 self, cache: t.Any, details: dict[str, t.Any] 

181 ) -> None: 

182 """Collect cache statistics if available.""" 

183 if hasattr(cache, "get_stats"): 

184 with suppress(Exception): # Stats not critical 

185 stats = await cache.get_stats() 

186 if hasattr(stats, "hit_ratio"): 

187 details["cache_hit_ratio"] = stats.hit_ratio 

188 

189 async def _perform_health_check( 

190 self, 

191 check_type: t.Any, 

192 ) -> t.Any: 

193 """Check cache system health.""" 

194 if not ACB_HEALTH_AVAILABLE: 

195 return None 

196 

197 details: dict[str, t.Any] = {} 

198 status = HealthStatus.HEALTHY 

199 message = "Cache system operational" 

200 

201 try: 

202 # Try to get cache adapter 

203 cache = depends.get("cache") 

204 

205 if cache is None: 

206 status = HealthStatus.DEGRADED 

207 message = "Cache adapter not available (degraded mode)" 

208 details["reason"] = "cache_disabled_or_not_configured" 

209 else: 

210 # Test cache operations 

211 status, message = await self._test_cache_operations(cache, details) 

212 

213 # Collect stats if available 

214 await self._collect_cache_stats(cache, details) 

215 

216 except Exception as e: 

217 status = HealthStatus.UNHEALTHY 

218 message = f"Cache health check failed: {e}" 

219 details["error"] = str(e) 

220 

221 return HealthCheckResult( 

222 component_id=self.component_id, 

223 component_name=self.component_name, 

224 status=status, 

225 check_type=check_type, 

226 message=message, 

227 details=details, 

228 ) 

229 

230 

231class RoutesHealthCheck(FastBlocksHealthCheck): 

232 """Health check for FastBlocks routing system.""" 

233 

234 def __init__(self) -> None: 

235 super().__init__( 

236 component_id="routes", 

237 component_name="Routing System", 

238 ) 

239 

240 def _check_routes_adapter( 

241 self, routes: t.Any, details: dict[str, t.Any] 

242 ) -> tuple[t.Any, str]: # Returns (status, message) 

243 """Check routes adapter status and update details.""" 

244 if not hasattr(routes, "routes"): 

245 return HealthStatus.DEGRADED, "Routes collection not available" 

246 

247 route_count = len(routes.routes) if routes.routes else 0 

248 details["route_count"] = route_count 

249 

250 if route_count == 0: 

251 return HealthStatus.DEGRADED, "No routes registered" 

252 

253 return HealthStatus.HEALTHY, f"{route_count} routes registered" 

254 

255 async def _perform_health_check( 

256 self, 

257 check_type: t.Any, 

258 ) -> t.Any: 

259 """Check routing system health.""" 

260 if not ACB_HEALTH_AVAILABLE: 

261 return None 

262 

263 details: dict[str, t.Any] = {} 

264 status = HealthStatus.HEALTHY 

265 message = "Routing system operational" 

266 

267 try: 

268 # Try to get routes adapter 

269 routes = depends.get("routes") 

270 

271 if routes is None: 

272 status = HealthStatus.DEGRADED 

273 message = "Routes adapter not initialized" 

274 else: 

275 status, message = self._check_routes_adapter(routes, details) 

276 

277 except Exception as e: 

278 status = HealthStatus.UNHEALTHY 

279 message = f"Routes health check failed: {e}" 

280 details["error"] = str(e) 

281 

282 return HealthCheckResult( 

283 component_id=self.component_id, 

284 component_name=self.component_name, 

285 status=status, 

286 check_type=check_type, 

287 message=message, 

288 details=details, 

289 ) 

290 

291 

292class DatabaseHealthCheck(FastBlocksHealthCheck): 

293 """Health check for database connectivity.""" 

294 

295 def __init__(self) -> None: 

296 super().__init__( 

297 component_id="database", 

298 component_name="Database", 

299 ) 

300 

301 async def _perform_health_check( 

302 self, 

303 check_type: t.Any, 

304 ) -> t.Any: 

305 """Check database health.""" 

306 if not ACB_HEALTH_AVAILABLE: 

307 return None 

308 

309 details: dict[str, t.Any] = {} 

310 status = HealthStatus.HEALTHY 

311 message = "Database operational" 

312 

313 try: 

314 # Try to get sql adapter 

315 sql = depends.get("sql") 

316 

317 if sql is None: 

318 status = HealthStatus.DEGRADED 

319 message = "Database adapter not configured" 

320 details["reason"] = "sql_adapter_not_available" 

321 else: 

322 # Try a simple database query 

323 try: 

324 # Most databases support SELECT 1 as a ping query 

325 await sql.execute("SELECT 1") 

326 details["connectivity_test"] = "passed" 

327 

328 # Check if we have connection pool info 

329 if hasattr(sql, "get_connection_info"): 

330 with suppress(Exception): # Connection info not critical 

331 conn_info = await sql.get_connection_info() 

332 details["connection_info"] = conn_info 

333 

334 except Exception as e: 

335 status = HealthStatus.UNHEALTHY 

336 message = f"Database query failed: {e}" 

337 details["connectivity_test"] = "failed" 

338 details["error"] = str(e) 

339 

340 except Exception as e: 

341 status = HealthStatus.DEGRADED 

342 message = f"Database health check failed: {e}" 

343 details["error"] = str(e) 

344 

345 return HealthCheckResult( 

346 component_id=self.component_id, 

347 component_name=self.component_name, 

348 status=status, 

349 check_type=check_type, 

350 message=message, 

351 details=details, 

352 ) 

353 

354 

355async def register_fastblocks_health_checks() -> bool: 

356 """Register all FastBlocks components with ACB HealthService. 

357 

358 Returns: 

359 True if registration successful, False if ACB HealthService unavailable 

360 """ 

361 if not ACB_HEALTH_AVAILABLE: 

362 return False 

363 

364 try: 

365 # Get ACB HealthService from the service registry 

366 health_service = depends.get("health_service") 

367 

368 if health_service is None: 

369 return False 

370 

371 # Register all FastBlocks health checks 

372 await health_service.register_component(TemplatesHealthCheck()) 

373 await health_service.register_component(CacheHealthCheck()) 

374 await health_service.register_component(RoutesHealthCheck()) 

375 await health_service.register_component(DatabaseHealthCheck()) 

376 

377 return True 

378 

379 except Exception: 

380 # Graceful degradation if registration fails 

381 return False 

382 

383 

384async def get_fastblocks_health_summary() -> dict[str, t.Any]: 

385 """Get comprehensive health summary for all FastBlocks components. 

386 

387 Returns: 

388 Dictionary with health status for each component 

389 """ 

390 if not ACB_HEALTH_AVAILABLE: 

391 return { 

392 "status": "unknown", 

393 "message": "ACB HealthService not available", 

394 "components": {}, 

395 } 

396 

397 try: 

398 health_service = depends.get("health_service") 

399 

400 if health_service is None: 

401 return { 

402 "status": "unknown", 

403 "message": "ACB HealthService not initialized", 

404 "components": {}, 

405 } 

406 

407 # Get health status for all registered components 

408 component_ids = ["templates", "cache", "routes", "database"] 

409 results = {} 

410 

411 for component_id in component_ids: 

412 try: 

413 result = await health_service.get_component_health(component_id) 

414 if result: 

415 results[component_id] = result.to_dict() 

416 except Exception: 

417 results[component_id] = { 

418 "status": "unknown", 

419 "message": "Health check failed", 

420 } 

421 

422 # Determine overall status 

423 statuses = [r.get("status", "unknown") for r in results.values()] 

424 

425 if "unhealthy" in statuses or "critical" in statuses: 

426 overall_status = "unhealthy" 

427 elif "degraded" in statuses: 

428 overall_status = "degraded" 

429 elif all(s == "healthy" for s in statuses): 

430 overall_status = "healthy" 

431 else: 

432 overall_status = "unknown" 

433 

434 return { 

435 "status": overall_status, 

436 "message": f"FastBlocks health status: {overall_status}", 

437 "components": results, 

438 } 

439 

440 except Exception as e: 

441 return { 

442 "status": "error", 

443 "message": f"Health check failed: {e}", 

444 "components": {}, 

445 } 

446 

447 

448# Module metadata for ACB discovery 

449MODULE_ID = UUID("01937d88-0000-7000-8000-000000000001") 

450MODULE_STATUS = AdapterStatus.STABLE 

451 

452# Auto-register health checks on module import 

453# Note: Registration happens during application startup via depends.set() 

454# This ensures proper async context is available