71 lines
2.4 KiB
Python
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__}"
|
|
|