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
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Pipeline health check CLI runner.
Exit codes:
0 = all healthy
1 = any warning
2 = any critical
"""
import argparse
import json
import sys
import duckdb
from config import config
from health import run_checks
from health.checks import check_embedding_coverage, check_llm_coverage, check_motion_freshness
def main() -> int:
parser = argparse.ArgumentParser(description="Check pipeline health")
parser.add_argument(
"--format",
choices=["text", "json"],
default="text",
help="Output format",
)
parser.add_argument(
"--threshold-days",
type=int,
default=7,
help="Max age in days for motion freshness check",
)
parser.add_argument(
"--min-embed-coverage",
type=float,
default=0.95,
help="Minimum embedding coverage ratio",
)
parser.add_argument(
"--max-missing-llm",
type=float,
default=0.15,
help="Maximum missing layman explanation ratio",
)
args = parser.parse_args()
try:
conn = duckdb.connect(config.DATABASE_PATH)
except Exception as e:
print(f"CRITICAL: Could not connect to database: {e}", file=sys.stderr)
return 2
checks = [
check_motion_freshness(conn, max_age_days=args.threshold_days),
check_embedding_coverage(conn, min_coverage=args.min_embed_coverage),
check_llm_coverage(conn, max_missing_ratio=args.max_missing_llm),
]
report = run_checks(checks)
if args.format == "json":
output = {
"status": report.status.value,
"exit_code": report.exit_code,
"checks": [
{
"name": c.name,
"status": c.status.value,
"message": c.message,
"details": c.details,
}
for c in report.checks
],
}
print(json.dumps(output, indent=2))
else:
status_emoji = {"ok": "", "warning": "⚠️", "critical": ""}
print(f"Health Report: {status_emoji.get(report.status.value, '?')} {report.status.value.upper()}")
print("-" * 50)
for check in report.checks:
emoji = status_emoji.get(check.status.value, "?")
print(f"{emoji} {check.name}: {check.message}")
if check.details:
for key, value in check.details.items():
print(f" {key}: {value}")
try:
conn.close()
except Exception:
pass
return report.exit_code
if __name__ == "__main__":
sys.exit(main())