import json import threading from datetime import datetime, timezone from http.server import HTTPServer, BaseHTTPRequestHandler from io import StringIO class HealthServer: def __init__(self, port: int = 8081): self.port = port self.lock = threading.Lock() self.last_sync = None self.last_sync_duration = 0.0 self.last_sync_success = None self.event_count = 0 self.syncs_total = 0 self.syncs_failed = 0 self.server = None self.thread = None def start(self) -> None: handler = self._make_handler() self.server = HTTPServer(("0.0.0.0", self.port), handler) self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) self.thread.start() def stop(self) -> None: if self.server: self.server.shutdown() self.server = None self.thread = None def update_status(self, last_sync: datetime, duration: float, success: bool, event_count: int) -> None: with self.lock: self.last_sync = last_sync self.last_sync_duration = duration self.last_sync_success = success self.event_count = event_count self.syncs_total += 1 if not success: self.syncs_failed += 1 def _make_handler(self): server_self = self class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/health": self._handle_health(server_self) elif self.path == "/metrics": self._handle_metrics(server_self) else: self.send_response(404) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "not found"}).encode()) def _handle_health(self, srv): with srv.lock: status = "ok" result = "none" if srv.last_sync_success is not None: if srv.last_sync_success: result = "success" else: result = "failure" status = "error" payload = { "status": status, "last_sync": srv.last_sync.isoformat() if srv.last_sync else None, "last_sync_duration_sec": srv.last_sync_duration, "last_sync_result": result, "event_count": srv.event_count, "syncs_total": srv.syncs_total, "syncs_failed": srv.syncs_failed, } body = json.dumps(payload).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(body) def _handle_metrics(self, srv): with srv.lock: last_success = 1 if srv.last_sync_success else 0 duration = srv.last_sync_duration evt_count = srv.event_count total = srv.syncs_total failed = srv.syncs_failed buf = StringIO() buf.write(f"baikal_sync_last_duration_seconds {duration}\n") buf.write(f"baikal_sync_events_total {evt_count}\n") buf.write(f"baikal_sync_total {total}\n") buf.write(f"baikal_sync_failures_total {failed}\n") buf.write(f"baikal_sync_last_success {last_success}\n") body = buf.getvalue().encode() self.send_response(200) self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8") self.end_headers() self.wfile.write(body) def log_message(self, format, *args): pass return Handler