Source code for alchemiq.health.checks
"""Async health probes for configured alchemiq backends (Postgres, ClickHouse, cache)."""
from __future__ import annotations
import asyncio
import time
from collections.abc import Awaitable
from typing import TYPE_CHECKING
from sqlalchemy import text
from alchemiq.cache import get_cache
from alchemiq.health.report import ComponentHealth, HealthReport
from alchemiq.runtime.engine import is_configured, require_engine
if TYPE_CHECKING:
from alchemiq.cache import CacheBackend
def _short(exc: BaseException) -> str:
return f"{type(exc).__name__}: {exc}"[:200]
def _clickhouse_configured() -> bool:
try:
from alchemiq.clickhouse.connection import is_clickhouse_configured
except ImportError:
return False
return is_clickhouse_configured()
def _ms(start: float) -> float:
return (time.perf_counter() - start) * 1000.0
async def _probe_postgres(timeout: float) -> ComponentHealth:
start = time.perf_counter()
try:
engine = require_engine()
async with asyncio.timeout(timeout):
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
return ComponentHealth("postgres", True, _ms(start))
except Exception as exc:
return ComponentHealth("postgres", False, None, _short(exc))
async def _probe_clickhouse(timeout: float) -> ComponentHealth:
start = time.perf_counter()
try:
from alchemiq.clickhouse.connection import get_clickhouse_client
async with asyncio.timeout(timeout):
client = await get_clickhouse_client()
ok = await client.ping()
if not ok:
raise RuntimeError("clickhouse ping returned False")
return ComponentHealth("clickhouse", True, _ms(start))
except Exception as exc:
return ComponentHealth("clickhouse", False, None, _short(exc))
async def _probe_cache(backend: CacheBackend, timeout: float) -> ComponentHealth:
start = time.perf_counter()
try:
async with asyncio.timeout(timeout):
await backend.get("aq:__health__")
return ComponentHealth("cache", True, _ms(start))
except Exception as exc:
return ComponentHealth("cache", False, None, _short(exc))
[docs]
async def check_health(*, timeout: float = 5.0) -> HealthReport:
"""Run health probes for all configured backends concurrently.
Returns a :class:`.HealthReport` aggregating all component results.
Probes are skipped for backends that are not configured. :attr:`.HealthReport.healthy`
is ``True`` only if **every** component probe succeeds. Each probe is guarded by
``timeout`` seconds; a probe that times out or errors is recorded as unhealthy.
E.g.::
from alchemiq.health import check_health
report = await check_health()
if not report.healthy:
print(report.to_dict())
# FastAPI integration:
from alchemiq.fastapi import health_router
app.include_router(health_router())
:param timeout: per-probe timeout in seconds (default 5.0).
:return: a :class:`.HealthReport` aggregating all component results.
.. note::
When no backends are configured, returns a trivially healthy report with an empty
``components`` tuple - useful in unit tests that haven't called ``configure``.
.. seealso:: :class:`.HealthReport` - the returned aggregate report.
"""
probes: list[Awaitable[ComponentHealth]] = []
if is_configured():
probes.append(_probe_postgres(timeout))
if _clickhouse_configured():
probes.append(_probe_clickhouse(timeout))
backend = get_cache()
if backend is not None:
probes.append(_probe_cache(backend, timeout))
if not probes:
return HealthReport(healthy=True, components=())
components = tuple(await asyncio.gather(*probes))
return HealthReport(healthy=all(c.healthy for c in components), components=components)