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

1"""Health check and validation system for FastBlocks adapters.""" 

2 

3import asyncio 

4import time 

5from datetime import datetime 

6from typing import Any 

7 

8from .registry import AdapterRegistry 

9 

10 

11class HealthCheckResult: 

12 """Result of a health check operation.""" 

13 

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() 

29 

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 } 

40 

41 

42class HealthCheckSystem: 

43 """Health monitoring and validation system for adapters.""" 

44 

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] = {} 

51 

52 async def check_adapter_health(self, adapter_name: str) -> HealthCheckResult: 

53 """Perform comprehensive health check on an adapter.""" 

54 start_time = time.time() 

55 

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 ) 

62 

63 self._running_checks[adapter_name] = True 

64 

65 # Basic validation check 

66 validation_result = await self.registry.validate_adapter(adapter_name) 

67 

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) 

80 

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 ) 

94 

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 

105 

106 # Store result in history 

107 self._store_check_result(result) 

108 

109 return result 

110 

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 = [] 

117 

118 # Check if adapter has required methods based on its type 

119 adapter_info = await self.registry.get_adapter_info(adapter_name) 

120 

121 if adapter_info: 

122 category = adapter_info.category 

123 

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)) 

135 

136 # Check settings availability 

137 if hasattr(adapter, "settings"): 

138 checks.append("Settings available") 

139 else: 

140 warnings.append("No settings found") 

141 

142 # Check ACB registration 

143 try: 

144 from acb.depends import depends 

145 

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") 

153 

154 duration_ms = (time.time() - start_time) * 1000 

155 

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" 

165 

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 ) 

177 

178 async def _check_image_adapter(self, adapter: Any) -> list[str]: 

179 """Check image adapter functionality.""" 

180 checks = [] 

181 

182 if hasattr(adapter, "get_img_tag"): 

183 checks.append("get_img_tag method available") 

184 

185 if hasattr(adapter, "get_image_url"): 

186 checks.append("get_image_url method available") 

187 

188 if hasattr(adapter, "upload_image"): 

189 checks.append("upload_image method available") 

190 

191 return checks 

192 

193 async def _check_style_adapter(self, adapter: Any) -> list[str]: 

194 """Check style adapter functionality.""" 

195 checks = [] 

196 

197 if hasattr(adapter, "get_stylesheet_links"): 

198 checks.append("get_stylesheet_links method available") 

199 

200 if hasattr(adapter, "get_component_class"): 

201 checks.append("get_component_class method available") 

202 

203 if hasattr(adapter, "get_utility_classes"): 

204 checks.append("get_utility_classes method available") 

205 

206 return checks 

207 

208 async def _check_icon_adapter(self, adapter: Any) -> list[str]: 

209 """Check icon adapter functionality.""" 

210 checks = [] 

211 

212 if hasattr(adapter, "get_icon_tag"): 

213 checks.append("get_icon_tag method available") 

214 

215 if hasattr(adapter, "get_stylesheet_links"): 

216 checks.append("get_stylesheet_links method available") 

217 

218 if hasattr(adapter, "get_icon_class"): 

219 checks.append("get_icon_class method available") 

220 

221 return checks 

222 

223 async def _check_font_adapter(self, adapter: Any) -> list[str]: 

224 """Check font adapter functionality.""" 

225 checks = [] 

226 

227 if hasattr(adapter, "get_font_import"): 

228 checks.append("get_font_import method available") 

229 

230 if hasattr(adapter, "get_font_family"): 

231 checks.append("get_font_family method available") 

232 

233 return checks 

234 

235 async def _check_template_adapter(self, adapter: Any) -> list[str]: 

236 """Check template adapter functionality.""" 

237 checks = [] 

238 

239 if hasattr(adapter, "render_template"): 

240 checks.append("render_template method available") 

241 

242 if hasattr(adapter, "get_template"): 

243 checks.append("get_template method available") 

244 

245 if hasattr(adapter, "list_templates"): 

246 checks.append("list_templates method available") 

247 

248 return checks 

249 

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 = {} 

254 

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)) 

260 

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 ) 

269 

270 return results 

271 

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] = [] 

276 

277 self._check_history[result.adapter_name].append(result) 

278 

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:] 

284 

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:] 

290 

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 } 

302 

303 latest_results = {} 

304 

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] 

309 

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 } 

317 

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 

326 

327 summary["total_adapters"] = len(latest_results) 

328 

329 if latest_results: 

330 latest_time = max(result.timestamp for result in latest_results.values()) 

331 summary["last_check_time"] = latest_time.isoformat() 

332 

333 return summary 

334 

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 

340 

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, {}) 

344 

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