Coverage for agentos/core/health.py: 0%
79 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
1"""
2Production health check with dependency liveness probes.
4Extends the basic health endpoint with:
5- Database connectivity check (async)
6- Redis connectivity check (async)
7- Component-level health status
9Usage:
10 from agentos.core.health import HealthChecker
11 checker = HealthChecker(db_url="...", redis_url="...")
12 status = await checker.check()
13"""
15from __future__ import annotations
17import asyncio
18import logging
19import time
20from dataclasses import dataclass, field
22logger = logging.getLogger(__name__)
25@dataclass
26class ComponentHealth:
27 name: str
28 status: str # "healthy" | "degraded" | "unhealthy"
29 latency_ms: float
30 error: str | None = None
33@dataclass
34class HealthReport:
35 status: str # "healthy" | "degraded" | "unhealthy"
36 uptime_seconds: float
37 components: dict[str, ComponentHealth] = field(default_factory=dict)
38 timestamp: float = field(default_factory=time.time)
41class HealthChecker:
42 """Async health checker with component-level probing."""
44 def __init__(
45 self,
46 start_time: float,
47 db_url: str | None = None,
48 redis_url: str | None = None,
49 ):
50 self.start_time = start_time
51 self.db_url = db_url
52 self.redis_url = redis_url
54 async def _probe(self, name: str, check_fn, timeout: float = 3.0) -> ComponentHealth:
55 """Run a single component health probe with timeout."""
56 t0 = time.perf_counter()
57 try:
58 await asyncio.wait_for(check_fn(), timeout=timeout)
59 latency = (time.perf_counter() - t0) * 1000
60 return ComponentHealth(name=name, status="healthy", latency_ms=latency)
61 except TimeoutError:
62 latency = (time.perf_counter() - t0) * 1000
63 return ComponentHealth(
64 name=name, status="unhealthy", latency_ms=latency, error=f"Timeout after {timeout}s"
65 )
66 except Exception as e:
67 latency = (time.perf_counter() - t0) * 1000
68 return ComponentHealth(name=name, status="unhealthy", latency_ms=latency, error=str(e))
70 async def check(self) -> HealthReport:
71 """Run a full health check across all configured components."""
72 probes = []
74 # DB probe
75 if self.db_url:
76 probes.append(self._probe("database", self._check_db))
78 # Redis probe
79 if self.redis_url:
80 probes.append(self._probe("redis", self._check_redis))
82 # Always probe disk (write test)
83 probes.append(self._probe("disk", self._check_disk))
85 results = await asyncio.gather(*probes, return_exceptions=True)
87 components: dict[str, ComponentHealth] = {}
88 overall = "healthy"
90 for r in results:
91 if isinstance(r, ComponentHealth):
92 components[r.name] = r
93 if r.status == "unhealthy":
94 if overall == "healthy":
95 overall = "degraded"
96 elif r.status == "degraded" and overall == "healthy":
97 overall = "degraded"
98 elif isinstance(r, Exception):
99 # Probe itself crashed
100 components["internal"] = ComponentHealth(
101 name="internal", status="unhealthy", latency_ms=0, error=str(r)
102 )
103 overall = "unhealthy"
105 return HealthReport(
106 status=overall,
107 uptime_seconds=time.time() - self.start_time,
108 components=components,
109 )
111 async def _check_db(self):
112 """Database connectivity probe."""
113 from sqlalchemy import text
114 from sqlalchemy.ext.asyncio import create_async_engine
116 engine = create_async_engine(self.db_url, echo=False)
117 async with engine.connect() as conn:
118 await conn.execute(text("SELECT 1"))
119 await engine.dispose()
121 async def _check_redis(self):
122 """Redis connectivity probe."""
123 import redis.asyncio as redis
125 r = redis.from_url(self.redis_url)
126 await r.ping()
127 await r.close()
129 async def _check_disk(self):
130 """Filesystem write test."""
131 import os
132 import tempfile
134 with tempfile.NamedTemporaryFile(delete=False, prefix="health_", suffix=".tmp") as f:
135 f.write(b"ok")
137 try:
138 os.unlink(f.name)
139 except OSError:
140 pass
143__all__ = ["HealthChecker", "HealthReport", "ComponentHealth"]