"""Infrastructure : client ClickHouse, health check HTTP, arrêt propre. Exécute le serveur de santé en thread daemon dès l'import. """ import signal import sys import threading from http.server import HTTPServer, BaseHTTPRequestHandler from ja4_common.clickhouse import get_client as _ja4_get_client from .config import HEALTH_PORT from .log import log_info, log_decision # ─── Arrêt propre ─────────────────────────────────────────────────────────── def _shutdown(sig, frame): """Gestionnaire de signal SIGTERM/SIGINT : journalise l'arrêt et quitte proprement.""" log_info(f"Signal {sig} reçu — arrêt propre.") log_decision('SERVICE_STOP', 'shutdown', '', {'signal': sig}) sys.exit(0) signal.signal(signal.SIGTERM, _shutdown) signal.signal(signal.SIGINT, _shutdown) # ─── Health check ─────────────────────────────────────────────────────────── _service_healthy = True _health_lock = threading.Lock() def set_healthy(healthy: bool): """Met à jour l'état de santé du service (thread-safe).""" global _service_healthy with _health_lock: _service_healthy = healthy def is_healthy() -> bool: """Retourne l'état de santé courant.""" with _health_lock: return _service_healthy class _HealthHandler(BaseHTTPRequestHandler): """Gestionnaire HTTP minimal pour le point de santé du service.""" def do_GET(self): """Répond à la requête GET : renvoie 200 OK ou 503 DEGRADED selon l'état du service.""" healthy = is_healthy() code = 200 if healthy else 503 self.send_response(code) self.end_headers() self.wfile.write(b'OK' if healthy else b'DEGRADED') def log_message(self, *args): """Supprime les logs HTTP internes pour ne pas polluer la sortie standard.""" pass threading.Thread( target=lambda: HTTPServer(('', HEALTH_PORT), _HealthHandler).serve_forever(), daemon=True ).start() # ─── Client ClickHouse ────────────────────────────────────────────────────── def get_client(): """Return the shared ja4_common ClickHouse client, reconnecting on ping failure.""" return _ja4_get_client().connect() def score_to_threat_level(score: float) -> str: """Convertit un score d'anomalie brut IsolationForest en niveau de menace textuel. Seuils : CRITICAL < −0.30 | HIGH < −0.15 | MEDIUM < −0.05 | LOW < 0 | NORMAL ≥ 0. """ if score < -0.30: return 'CRITICAL' if score < -0.15: return 'HIGH' if score < -0.05: return 'MEDIUM' if score < 0: return 'LOW' return 'NORMAL'