feat: add pipeline health checks module and CLI runner

- Create health/ package with HealthStatus, HealthCheck, HealthReport
- Add check_motion_freshness, check_embedding_coverage, check_llm_coverage
- Add scripts/health_check.py CLI with text/JSON output and exit codes
- Add comprehensive tests for core, checks, and CLI

P4-005: Pipeline health checks
This commit is contained in:
2026-05-01 00:02:45 +02:00
parent 04cc62ea06
commit e352d7c7bc
6 changed files with 495 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
from dataclasses import dataclass
from enum import Enum
from typing import List
class HealthStatus(Enum):
OK = "ok"
WARNING = "warning"
CRITICAL = "critical"
@dataclass
class HealthCheck:
name: str
status: HealthStatus
message: str
details: dict
@dataclass
class HealthReport:
checks: List[HealthCheck]
@property
def status(self) -> HealthStatus:
if any(c.status == HealthStatus.CRITICAL for c in self.checks):
return HealthStatus.CRITICAL
if any(c.status == HealthStatus.WARNING for c in self.checks):
return HealthStatus.WARNING
return HealthStatus.OK
@property
def exit_code(self) -> int:
if self.status == HealthStatus.CRITICAL:
return 2
if self.status == HealthStatus.WARNING:
return 1
return 0
def run_checks(checks: List[HealthCheck]) -> HealthReport:
return HealthReport(checks=checks)
+140
View File
@@ -0,0 +1,140 @@
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from health import HealthCheck, HealthStatus
def check_motion_freshness(
conn: Any,
max_age_days: int = 7,
min_motions: int = 100,
) -> HealthCheck:
try:
result = conn.execute(
"SELECT COUNT(*) FROM motions WHERE date >= ?",
[datetime.now() - timedelta(days=max_age_days)],
).fetchone()
count = result[0] if result else 0
except Exception as e:
return HealthCheck(
name="motion_freshness",
status=HealthStatus.CRITICAL,
message=f"Could not query motion freshness: {e}",
details={},
)
if count == 0:
return HealthCheck(
name="motion_freshness",
status=HealthStatus.CRITICAL,
message=f"No motions in last {max_age_days} days",
details={"count": 0, "threshold": max_age_days},
)
if count < min_motions:
return HealthCheck(
name="motion_freshness",
status=HealthStatus.WARNING,
message=f"Only {count} motions in last {max_age_days} days (expected >= {min_motions})",
details={"count": count, "threshold": max_age_days, "min_expected": min_motions},
)
return HealthCheck(
name="motion_freshness",
status=HealthStatus.OK,
message=f"{count} motions in last {max_age_days} days",
details={"count": count, "threshold": max_age_days},
)
def check_embedding_coverage(
conn: Any,
min_coverage: float = 0.95,
) -> HealthCheck:
try:
total_result = conn.execute("SELECT COUNT(*) FROM motions").fetchone()
total = total_result[0] if total_result else 0
if total == 0:
return HealthCheck(
name="embedding_coverage",
status=HealthStatus.CRITICAL,
message="No motions in database",
details={"total": 0, "with_embeddings": 0, "coverage": 0.0},
)
embed_result = conn.execute(
"SELECT COUNT(DISTINCT motion_id) FROM fused_embeddings"
).fetchone()
with_embeddings = embed_result[0] if embed_result else 0
coverage = with_embeddings / total
except Exception as e:
return HealthCheck(
name="embedding_coverage",
status=HealthStatus.CRITICAL,
message=f"Could not query embedding coverage: {e}",
details={},
)
if coverage < min_coverage:
return HealthCheck(
name="embedding_coverage",
status=HealthStatus.WARNING,
message=f"Embedding coverage {coverage:.1%} (expected >= {min_coverage:.0%})",
details={"total": total, "with_embeddings": with_embeddings, "coverage": coverage},
)
return HealthCheck(
name="embedding_coverage",
status=HealthStatus.OK,
message=f"Embedding coverage {coverage:.1%}",
details={"total": total, "with_embeddings": with_embeddings, "coverage": coverage},
)
def check_llm_coverage(
conn: Any,
max_missing_ratio: float = 0.15,
) -> HealthCheck:
try:
total_result = conn.execute("SELECT COUNT(*) FROM motions").fetchone()
total = total_result[0] if total_result else 0
if total == 0:
return HealthCheck(
name="llm_coverage",
status=HealthStatus.CRITICAL,
message="No motions in database",
details={"total": 0, "missing": 0, "missing_ratio": 0.0},
)
missing_result = conn.execute(
"SELECT COUNT(*) FROM motions WHERE layman_explanation IS NULL OR layman_explanation = ''"
).fetchone()
missing = missing_result[0] if missing_result else 0
missing_ratio = missing / total
except Exception as e:
return HealthCheck(
name="llm_coverage",
status=HealthStatus.CRITICAL,
message=f"Could not query LLM coverage: {e}",
details={},
)
if missing_ratio > max_missing_ratio:
return HealthCheck(
name="llm_coverage",
status=HealthStatus.CRITICAL,
message=f"{missing_ratio:.1%} missing layman explanations ({missing}/{total})",
details={"total": total, "missing": missing, "missing_ratio": missing_ratio},
)
if missing_ratio > 0.05:
return HealthCheck(
name="llm_coverage",
status=HealthStatus.WARNING,
message=f"{missing_ratio:.1%} missing layman explanations ({missing}/{total})",
details={"total": total, "missing": missing, "missing_ratio": missing_ratio},
)
return HealthCheck(
name="llm_coverage",
status=HealthStatus.OK,
message=f"{missing_ratio:.1%} missing layman explanations ({missing}/{total})",
details={"total": total, "missing": missing, "missing_ratio": missing_ratio},
)