bifroest/app/core/health.py
2026-10-27 08:34:17 +01:00

71 lines
2.4 KiB
Python

"""Health-Checks für Web und Worker (→ Betrieb).
live: der Prozess antwortet (im Worker: die Schleife läuft). Bewusst ohne Datenbank,
damit ein DB-Ausfall nicht zu Neustarts aller Prozesse führt.
ready: Datenbank erreichbar, Migrationen auf dem neuesten Stand, Upload-Verzeichnis
beschreibbar, Master-Key gesetzt. Sonst 503: kein Traffic bzw. keine Runs.
"""
import logging
import tempfile
from dataclasses import dataclass, field
from functools import cache
from pathlib import Path
from alembic.script import ScriptDirectory
from sqlalchemy import text
from app.core.config import Settings
from app.core.database import Database
log = logging.getLogger(__name__)
MIGRATIONS = Path(__file__).resolve().parent.parent / "migrations"
@dataclass
class Report:
checks: dict[str, str] = field(default_factory=dict) # Name → "ok" oder Grund
@property
def ok(self) -> bool:
return all(v == "ok" for v in self.checks.values())
def body(self) -> dict:
return {"status": "ok" if self.ok else "unavailable", "checks": self.checks}
@cache
def head_revision() -> str:
from alembic.config import Config
config = Config()
config.set_main_option("script_location", str(MIGRATIONS))
return ScriptDirectory.from_config(config).get_current_head()
def ready(db: Database, settings: Settings) -> Report:
report = Report()
try:
with db.engine.connect() as connection:
connection.execute(text("SELECT 1"))
current = connection.execute(text("SELECT version_num FROM alembic_version")).scalar()
report.checks["database"] = "ok"
report.checks["migrations"] = "ok" if current == head_revision() else \
f"Stand {current}, erwartet {head_revision()} (aiw migrate)"
except Exception as e:
report.checks["database"] = f"nicht erreichbar: {type(e).__name__}"
report.checks["uploads"] = writable(Path(settings.upload_dir))
report.checks["secret_key"] = "ok" if settings.secret_key else "AIW_SECRET_KEY fehlt"
if not report.ok:
log.warning("Nicht bereit", extra={"checks": report.checks})
return report
def writable(directory: Path) -> str:
try:
directory.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=directory, prefix=".ready-", delete=True):
pass
return "ok"
except OSError as e:
return f"nicht beschreibbar: {e.strerror or type(e).__name__}"