Coverage for fastblocks / mcp / health.py: 72%
186 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-19 12:49 -0800
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-19 12:49 -0800
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 def _determine_health_status(
112 self, checks: list[str], warnings: list[str]
113 ) -> tuple[str, str]:
114 """Determine health status and message based on checks and warnings."""
115 if checks:
116 status = "warning" if warnings else "healthy"
117 message = f"Passed {len(checks)} checks"
118 if warnings:
119 message += f", {len(warnings)} warnings"
120 else:
121 status = "error"
122 message = "No functional checks passed"
123 return status, message
125 async def _perform_category_specific_checks(
126 self, adapter_info: Any, adapter: Any
127 ) -> list[str]:
128 """Perform category-specific health checks."""
129 checks = []
130 category = adapter_info.category
132 # Category-specific health checks
133 if category == "images":
134 checks.extend(await self._check_image_adapter(adapter))
135 elif category == "styles":
136 checks.extend(await self._check_style_adapter(adapter))
137 elif category == "icons":
138 checks.extend(await self._check_icon_adapter(adapter))
139 elif category == "fonts":
140 checks.extend(await self._check_font_adapter(adapter))
141 elif category == "templates":
142 checks.extend(await self._check_template_adapter(adapter))
144 return checks
146 async def _check_acb_registration(
147 self, adapter_name: str
148 ) -> tuple[list[str], list[str]]:
149 """Check ACB registration for the adapter."""
150 checks = []
151 warnings = []
153 try:
154 from acb.depends import depends
156 registered_adapter = depends.get_sync(adapter_name)
157 if registered_adapter:
158 checks.append("Registered with ACB")
159 else:
160 warnings.append("Not registered with ACB")
161 except Exception:
162 warnings.append("ACB registration check failed")
164 return checks, warnings
166 async def _perform_functional_checks(
167 self, adapter_name: str, adapter: Any, start_time: float
168 ) -> HealthCheckResult:
169 """Perform functional health checks on an adapter."""
170 checks = []
171 warnings = []
173 # Check if adapter has required methods based on its type
174 adapter_info = await self.registry.get_adapter_info(adapter_name)
176 if adapter_info:
177 checks.extend(
178 await self._perform_category_specific_checks(adapter_info, adapter)
179 )
181 # Check settings availability
182 if hasattr(adapter, "settings"):
183 checks.append("Settings available")
184 else:
185 warnings.append("No settings found")
187 # Check ACB registration
188 acb_checks, acb_warnings = await self._check_acb_registration(adapter_name)
189 checks.extend(acb_checks)
190 warnings.extend(acb_warnings)
192 duration_ms = (time.time() - start_time) * 1000
194 # Determine overall status
195 status, message = self._determine_health_status(checks, warnings)
197 return HealthCheckResult(
198 adapter_name,
199 status,
200 message,
201 {
202 "checks_passed": checks,
203 "warnings": warnings,
204 "category": adapter_info.category if adapter_info else "unknown",
205 },
206 duration_ms,
207 )
209 async def _check_image_adapter(self, adapter: Any) -> list[str]:
210 """Check image adapter functionality."""
211 checks = []
213 if hasattr(adapter, "get_img_tag"):
214 checks.append("get_img_tag method available")
216 if hasattr(adapter, "get_image_url"):
217 checks.append("get_image_url method available")
219 if hasattr(adapter, "upload_image"):
220 checks.append("upload_image method available")
222 return checks
224 async def _check_style_adapter(self, adapter: Any) -> list[str]:
225 """Check style adapter functionality."""
226 checks = []
228 if hasattr(adapter, "get_stylesheet_links"):
229 checks.append("get_stylesheet_links method available")
231 if hasattr(adapter, "get_component_class"):
232 checks.append("get_component_class method available")
234 if hasattr(adapter, "get_utility_classes"):
235 checks.append("get_utility_classes method available")
237 return checks
239 async def _check_icon_adapter(self, adapter: Any) -> list[str]:
240 """Check icon adapter functionality."""
241 checks = []
243 if hasattr(adapter, "get_icon_tag"):
244 checks.append("get_icon_tag method available")
246 if hasattr(adapter, "get_stylesheet_links"):
247 checks.append("get_stylesheet_links method available")
249 if hasattr(adapter, "get_icon_class"):
250 checks.append("get_icon_class method available")
252 return checks
254 async def _check_font_adapter(self, adapter: Any) -> list[str]:
255 """Check font adapter functionality."""
256 checks = []
258 if hasattr(adapter, "get_font_import"):
259 checks.append("get_font_import method available")
261 if hasattr(adapter, "get_font_family"):
262 checks.append("get_font_family method available")
264 return checks
266 async def _check_template_adapter(self, adapter: Any) -> list[str]:
267 """Check template adapter functionality."""
268 checks = []
270 if hasattr(adapter, "render_template"):
271 checks.append("render_template method available")
273 if hasattr(adapter, "get_template"):
274 checks.append("get_template method available")
276 if hasattr(adapter, "list_templates"):
277 checks.append("list_templates method available")
279 return checks
281 async def check_all_adapters(self) -> dict[str, HealthCheckResult]:
282 """Perform health checks on all available adapters."""
283 available_adapters = await self.registry.list_available_adapters()
284 results = {}
286 # Run checks concurrently
287 tasks = []
288 for adapter_name in available_adapters.keys():
289 task = asyncio.create_task(self.check_adapter_health(adapter_name))
290 tasks.append((adapter_name, task))
292 for adapter_name, task in tasks:
293 try:
294 result = await task
295 results[adapter_name] = result
296 except Exception as e:
297 results[adapter_name] = HealthCheckResult(
298 adapter_name, "error", f"Check failed: {e}"
299 )
301 return results
303 def _store_check_result(self, result: HealthCheckResult) -> None:
304 """Store health check result in history."""
305 if result.adapter_name not in self._check_history:
306 self._check_history[result.adapter_name] = []
308 self._check_history[result.adapter_name].append(result)
310 # Keep only last 100 results per adapter
311 if len(self._check_history[result.adapter_name]) > 100:
312 self._check_history[result.adapter_name] = self._check_history[
313 result.adapter_name
314 ][-100:]
316 def get_check_history(
317 self, adapter_name: str, limit: int = 10
318 ) -> list[HealthCheckResult]:
319 """Get health check history for an adapter."""
320 return self._check_history.get(adapter_name, [])[-limit:]
322 def get_system_health_summary(self) -> dict[str, Any]:
323 """Get overall system health summary."""
324 summary: dict[str, Any] = {
325 "healthy_adapters": 0,
326 "warning_adapters": 0,
327 "error_adapters": 0,
328 "unknown_adapters": 0,
329 "total_adapters": 0,
330 "last_check_time": None,
331 "adapter_status": {},
332 }
334 latest_results = {}
336 # Get latest result for each adapter
337 for adapter_name, history in self._check_history.items():
338 if history:
339 latest_results[adapter_name] = history[-1]
341 # Count statuses
342 for adapter_name, result in latest_results.items():
343 summary["adapter_status"][adapter_name] = {
344 "status": result.status,
345 "last_check": result.timestamp.isoformat(),
346 "message": result.message,
347 }
349 if result.status == "healthy":
350 summary["healthy_adapters"] += 1
351 elif result.status == "warning":
352 summary["warning_adapters"] += 1
353 elif result.status == "error":
354 summary["error_adapters"] += 1
355 else:
356 summary["unknown_adapters"] += 1
358 summary["total_adapters"] = len(latest_results)
360 if latest_results:
361 latest_time = max(result.timestamp for result in latest_results.values())
362 summary["last_check_time"] = latest_time.isoformat()
364 return summary
366 def configure_health_checks(
367 self, adapter_name: str, config: dict[str, Any]
368 ) -> None:
369 """Configure health check parameters for an adapter."""
370 self._check_config[adapter_name] = config
372 def get_health_check_config(self, adapter_name: str) -> dict[str, Any]:
373 """Get health check configuration for an adapter."""
374 return self._check_config.get(adapter_name, {})
376 async def schedule_periodic_checks(self, interval_minutes: int = 30) -> None:
377 """Schedule periodic health checks for all adapters."""
378 while True:
379 try:
380 await self.check_all_adapters()
381 await asyncio.sleep(interval_minutes * 60)
382 except Exception:
383 # Log error but continue
384 await asyncio.sleep(60) # Wait 1 minute before retrying