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
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""ACB HealthService integration for FastBlocks.
3This module bridges FastBlocks components with ACB's comprehensive health monitoring system.
4It registers FastBlocks-specific health checks while maintaining existing MCP health checks.
6Author: lesleslie <les@wedgwoodwebworks.com>
7Created: 2025-10-01
8"""
9# type: ignore # ACB health service API stub - graceful degradation
11import typing as t
12from contextlib import suppress
13from uuid import UUID
15from acb.adapters import AdapterStatus
16from acb.depends import Inject, depends
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 )
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
36class FastBlocksHealthCheck(HealthCheckMixin): # type: ignore[misc]
37 """Base health check implementation for FastBlocks components."""
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__
52 @property
53 def component_id(self) -> str:
54 """Get unique identifier for this component."""
55 return self._component_id
57 @property
58 def component_name(self) -> str:
59 """Get human-readable name for this component."""
60 return self._component_name
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
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 )
79class TemplatesHealthCheck(FastBlocksHealthCheck):
80 """Health check for FastBlocks template system."""
82 def __init__(self) -> None:
83 super().__init__(
84 component_id="templates",
85 component_name="Template System",
86 )
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
96 details: dict[str, t.Any] = {}
97 status = HealthStatus.HEALTHY
98 message = "Template system operational"
100 try:
101 # Try to get templates adapter
102 templates = depends.get("templates")
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
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"
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
129 except Exception as e:
130 status = HealthStatus.UNHEALTHY
131 message = f"Template health check failed: {e}"
132 details["error"] = str(e)
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 )
144class CacheHealthCheck(FastBlocksHealthCheck):
145 """Health check for FastBlocks cache system."""
147 def __init__(self) -> None:
148 super().__init__(
149 component_id="cache",
150 component_name="Cache System",
151 )
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"
160 try:
161 # Test set operation
162 await cache.set(test_key, test_value, ttl=10)
163 details["write_test"] = "passed"
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"
172 details["read_test"] = "failed"
173 return HealthStatus.DEGRADED, "Cache read verification failed"
175 except Exception as e:
176 details["operation_error"] = str(e)
177 return HealthStatus.DEGRADED, f"Cache operations failed: {e}"
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
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
197 details: dict[str, t.Any] = {}
198 status = HealthStatus.HEALTHY
199 message = "Cache system operational"
201 try:
202 # Try to get cache adapter
203 cache = depends.get("cache")
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)
213 # Collect stats if available
214 await self._collect_cache_stats(cache, details)
216 except Exception as e:
217 status = HealthStatus.UNHEALTHY
218 message = f"Cache health check failed: {e}"
219 details["error"] = str(e)
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 )
231class RoutesHealthCheck(FastBlocksHealthCheck):
232 """Health check for FastBlocks routing system."""
234 def __init__(self) -> None:
235 super().__init__(
236 component_id="routes",
237 component_name="Routing System",
238 )
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"
247 route_count = len(routes.routes) if routes.routes else 0
248 details["route_count"] = route_count
250 if route_count == 0:
251 return HealthStatus.DEGRADED, "No routes registered"
253 return HealthStatus.HEALTHY, f"{route_count} routes registered"
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
263 details: dict[str, t.Any] = {}
264 status = HealthStatus.HEALTHY
265 message = "Routing system operational"
267 try:
268 # Try to get routes adapter
269 routes = depends.get("routes")
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)
277 except Exception as e:
278 status = HealthStatus.UNHEALTHY
279 message = f"Routes health check failed: {e}"
280 details["error"] = str(e)
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 )
292class DatabaseHealthCheck(FastBlocksHealthCheck):
293 """Health check for database connectivity."""
295 def __init__(self) -> None:
296 super().__init__(
297 component_id="database",
298 component_name="Database",
299 )
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
309 details: dict[str, t.Any] = {}
310 status = HealthStatus.HEALTHY
311 message = "Database operational"
313 try:
314 # Try to get sql adapter
315 sql = depends.get("sql")
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"
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
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)
340 except Exception as e:
341 status = HealthStatus.DEGRADED
342 message = f"Database health check failed: {e}"
343 details["error"] = str(e)
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 )
355async def register_fastblocks_health_checks() -> bool:
356 """Register all FastBlocks components with ACB HealthService.
358 Returns:
359 True if registration successful, False if ACB HealthService unavailable
360 """
361 if not ACB_HEALTH_AVAILABLE:
362 return False
364 try:
365 # Get ACB HealthService from the service registry
366 health_service = depends.get("health_service")
368 if health_service is None:
369 return False
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())
377 return True
379 except Exception:
380 # Graceful degradation if registration fails
381 return False
384async def get_fastblocks_health_summary() -> dict[str, t.Any]:
385 """Get comprehensive health summary for all FastBlocks components.
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 }
397 try:
398 health_service = depends.get("health_service")
400 if health_service is None:
401 return {
402 "status": "unknown",
403 "message": "ACB HealthService not initialized",
404 "components": {},
405 }
407 # Get health status for all registered components
408 component_ids = ["templates", "cache", "routes", "database"]
409 results = {}
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 }
422 # Determine overall status
423 statuses = [r.get("status", "unknown") for r in results.values()]
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"
434 return {
435 "status": overall_status,
436 "message": f"FastBlocks health status: {overall_status}",
437 "components": results,
438 }
440 except Exception as e:
441 return {
442 "status": "error",
443 "message": f"Health check failed: {e}",
444 "components": {},
445 }
448# Module metadata for ACB discovery
449MODULE_ID = UUID("01937d88-0000-7000-8000-000000000001")
450MODULE_STATUS = AdapterStatus.STABLE
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