Coverage for fastblocks/mcp/health.py: 24%
172 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"""Health check and validation system for FastBlocks adapters."""
3import asyncio
4import time
5from datetime import datetime
6from typing import Any
8from .registry import AdapterRegistry
11class HealthCheckResult:
12 """Result of a health check operation."""
14 def __init__(
15 self,
16 adapter_name: str,
17 status: str,
18 message: str = "",
19 details: dict[str, Any] | None = None,
20 duration_ms: float = 0.0,
21 timestamp: datetime | None = None,
22 ):
23 self.adapter_name = adapter_name
24 self.status = status # 'healthy', 'warning', 'error', 'unknown'
25 self.message = message
26 self.details = details or {}
27 self.duration_ms = duration_ms
28 self.timestamp = timestamp or datetime.now()
30 def to_dict(self) -> dict[str, Any]:
31 """Convert result to dictionary."""
32 return {
33 "adapter_name": self.adapter_name,
34 "status": self.status,
35 "message": self.message,
36 "details": self.details,
37 "duration_ms": self.duration_ms,
38 "timestamp": self.timestamp.isoformat(),
39 }
42class HealthCheckSystem:
43 """Health monitoring and validation system for adapters."""
45 def __init__(self, registry: AdapterRegistry):
46 """Initialize health check system."""
47 self.registry = registry
48 self._check_history: dict[str, list[HealthCheckResult]] = {}
49 self._check_config: dict[str, dict[str, Any]] = {}
50 self._running_checks: dict[str, bool] = {}
52 async def check_adapter_health(self, adapter_name: str) -> HealthCheckResult:
53 """Perform comprehensive health check on an adapter."""
54 start_time = time.time()
56 try:
57 # Prevent concurrent checks on same adapter
58 if self._running_checks.get(adapter_name, False):
59 return HealthCheckResult(
60 adapter_name, "warning", "Health check already in progress"
61 )
63 self._running_checks[adapter_name] = True
65 # Basic validation check
66 validation_result = await self.registry.validate_adapter(adapter_name)
68 if not validation_result["valid"]:
69 duration_ms = (time.time() - start_time) * 1000
70 result = HealthCheckResult(
71 adapter_name,
72 "error",
73 f"Validation failed: {', '.join(validation_result['errors'])}",
74 validation_result,
75 duration_ms,
76 )
77 else:
78 # Get adapter instance for functional tests
79 adapter = await self.registry.get_adapter(adapter_name)
81 if not adapter:
82 duration_ms = (time.time() - start_time) * 1000
83 result = HealthCheckResult(
84 adapter_name,
85 "error",
86 "Could not instantiate adapter",
87 duration_ms=duration_ms,
88 )
89 else:
90 # Perform functional health checks
91 result = await self._perform_functional_checks(
92 adapter_name, adapter, start_time
93 )
95 except Exception as e:
96 duration_ms = (time.time() - start_time) * 1000
97 result = HealthCheckResult(
98 adapter_name,
99 "error",
100 f"Health check failed: {e}",
101 duration_ms=duration_ms,
102 )
103 finally:
104 self._running_checks[adapter_name] = False
106 # Store result in history
107 self._store_check_result(result)
109 return result
111 async def _perform_functional_checks(
112 self, adapter_name: str, adapter: Any, start_time: float
113 ) -> HealthCheckResult:
114 """Perform functional health checks on an adapter."""
115 checks = []
116 warnings = []
118 # Check if adapter has required methods based on its type
119 adapter_info = await self.registry.get_adapter_info(adapter_name)
121 if adapter_info:
122 category = adapter_info.category
124 # Category-specific health checks
125 if category == "images":
126 checks.extend(await self._check_image_adapter(adapter))
127 elif category == "styles":
128 checks.extend(await self._check_style_adapter(adapter))
129 elif category == "icons":
130 checks.extend(await self._check_icon_adapter(adapter))
131 elif category == "fonts":
132 checks.extend(await self._check_font_adapter(adapter))
133 elif category == "templates":
134 checks.extend(await self._check_template_adapter(adapter))
136 # Check settings availability
137 if hasattr(adapter, "settings"):
138 checks.append("Settings available")
139 else:
140 warnings.append("No settings found")
142 # Check ACB registration
143 try:
144 from acb.depends import depends
146 registered_adapter = depends.get(adapter_name)
147 if registered_adapter:
148 checks.append("Registered with ACB")
149 else:
150 warnings.append("Not registered with ACB")
151 except Exception:
152 warnings.append("ACB registration check failed")
154 duration_ms = (time.time() - start_time) * 1000
156 # Determine overall status
157 if checks:
158 status = "warning" if warnings else "healthy"
159 message = f"Passed {len(checks)} checks"
160 if warnings:
161 message += f", {len(warnings)} warnings"
162 else:
163 status = "error"
164 message = "No functional checks passed"
166 return HealthCheckResult(
167 adapter_name,
168 status,
169 message,
170 {
171 "checks_passed": checks,
172 "warnings": warnings,
173 "category": adapter_info.category if adapter_info else "unknown",
174 },
175 duration_ms,
176 )
178 async def _check_image_adapter(self, adapter: Any) -> list[str]:
179 """Check image adapter functionality."""
180 checks = []
182 if hasattr(adapter, "get_img_tag"):
183 checks.append("get_img_tag method available")
185 if hasattr(adapter, "get_image_url"):
186 checks.append("get_image_url method available")
188 if hasattr(adapter, "upload_image"):
189 checks.append("upload_image method available")
191 return checks
193 async def _check_style_adapter(self, adapter: Any) -> list[str]:
194 """Check style adapter functionality."""
195 checks = []
197 if hasattr(adapter, "get_stylesheet_links"):
198 checks.append("get_stylesheet_links method available")
200 if hasattr(adapter, "get_component_class"):
201 checks.append("get_component_class method available")
203 if hasattr(adapter, "get_utility_classes"):
204 checks.append("get_utility_classes method available")
206 return checks
208 async def _check_icon_adapter(self, adapter: Any) -> list[str]:
209 """Check icon adapter functionality."""
210 checks = []
212 if hasattr(adapter, "get_icon_tag"):
213 checks.append("get_icon_tag method available")
215 if hasattr(adapter, "get_stylesheet_links"):
216 checks.append("get_stylesheet_links method available")
218 if hasattr(adapter, "get_icon_class"):
219 checks.append("get_icon_class method available")
221 return checks
223 async def _check_font_adapter(self, adapter: Any) -> list[str]:
224 """Check font adapter functionality."""
225 checks = []
227 if hasattr(adapter, "get_font_import"):
228 checks.append("get_font_import method available")
230 if hasattr(adapter, "get_font_family"):
231 checks.append("get_font_family method available")
233 return checks
235 async def _check_template_adapter(self, adapter: Any) -> list[str]:
236 """Check template adapter functionality."""
237 checks = []
239 if hasattr(adapter, "render_template"):
240 checks.append("render_template method available")
242 if hasattr(adapter, "get_template"):
243 checks.append("get_template method available")
245 if hasattr(adapter, "list_templates"):
246 checks.append("list_templates method available")
248 return checks
250 async def check_all_adapters(self) -> dict[str, HealthCheckResult]:
251 """Perform health checks on all available adapters."""
252 available_adapters = await self.registry.list_available_adapters()
253 results = {}
255 # Run checks concurrently
256 tasks = []
257 for adapter_name in available_adapters.keys():
258 task = asyncio.create_task(self.check_adapter_health(adapter_name))
259 tasks.append((adapter_name, task))
261 for adapter_name, task in tasks:
262 try:
263 result = await task
264 results[adapter_name] = result
265 except Exception as e:
266 results[adapter_name] = HealthCheckResult(
267 adapter_name, "error", f"Check failed: {e}"
268 )
270 return results
272 def _store_check_result(self, result: HealthCheckResult) -> None:
273 """Store health check result in history."""
274 if result.adapter_name not in self._check_history:
275 self._check_history[result.adapter_name] = []
277 self._check_history[result.adapter_name].append(result)
279 # Keep only last 100 results per adapter
280 if len(self._check_history[result.adapter_name]) > 100:
281 self._check_history[result.adapter_name] = self._check_history[
282 result.adapter_name
283 ][-100:]
285 def get_check_history(
286 self, adapter_name: str, limit: int = 10
287 ) -> list[HealthCheckResult]:
288 """Get health check history for an adapter."""
289 return self._check_history.get(adapter_name, [])[-limit:]
291 def get_system_health_summary(self) -> dict[str, Any]:
292 """Get overall system health summary."""
293 summary: dict[str, Any] = {
294 "healthy_adapters": 0,
295 "warning_adapters": 0,
296 "error_adapters": 0,
297 "unknown_adapters": 0,
298 "total_adapters": 0,
299 "last_check_time": None,
300 "adapter_status": {},
301 }
303 latest_results = {}
305 # Get latest result for each adapter
306 for adapter_name, history in self._check_history.items():
307 if history:
308 latest_results[adapter_name] = history[-1]
310 # Count statuses
311 for adapter_name, result in latest_results.items():
312 summary["adapter_status"][adapter_name] = {
313 "status": result.status,
314 "last_check": result.timestamp.isoformat(),
315 "message": result.message,
316 }
318 if result.status == "healthy":
319 summary["healthy_adapters"] += 1
320 elif result.status == "warning":
321 summary["warning_adapters"] += 1
322 elif result.status == "error":
323 summary["error_adapters"] += 1
324 else:
325 summary["unknown_adapters"] += 1
327 summary["total_adapters"] = len(latest_results)
329 if latest_results:
330 latest_time = max(result.timestamp for result in latest_results.values())
331 summary["last_check_time"] = latest_time.isoformat()
333 return summary
335 def configure_health_checks(
336 self, adapter_name: str, config: dict[str, Any]
337 ) -> None:
338 """Configure health check parameters for an adapter."""
339 self._check_config[adapter_name] = config
341 def get_health_check_config(self, adapter_name: str) -> dict[str, Any]:
342 """Get health check configuration for an adapter."""
343 return self._check_config.get(adapter_name, {})
345 async def schedule_periodic_checks(self, interval_minutes: int = 30) -> None:
346 """Schedule periodic health checks for all adapters."""
347 while True:
348 try:
349 await self.check_all_adapters()
350 await asyncio.sleep(interval_minutes * 60)
351 except Exception:
352 # Log error but continue
353 await asyncio.sleep(60) # Wait 1 minute before retrying