import time import socket import logging import requests import concurrent.futures from health_agent.uptime_kuma import push from health_agent import config logger = logging.getLogger(__name__) def check_single_actuator(ip, port, path, timeout): try: url = f"http://{ip}:{port}{path}" resp = requests.get(url, timeout=timeout) if resp.status_code != 200: return False, f"HTTP {resp.status_code}" try: data = resp.json() if data.get("status") == "UP": return True, "" return False, f"status: {data.get('status', 'unknown')}" except ValueError: return True, "" # JSON parse failed, but HTTP 200 is sufficient except Exception as e: return False, str(e) def check_service(svc): start_time = time.time() name = svc["name"] service = svc["service"] port = svc["port"] path = svc.get("path", "/actuator/health") timeout = svc.get("timeout", 5) expected_replicas = svc.get("expected_replicas") try: # Resolve A records try: addr_info = socket.getaddrinfo(service, port, socket.AF_INET, socket.SOCK_STREAM) ips = list(set(item[4][0] for item in addr_info)) except socket.gaierror as e: ping_ms = int((time.time() - start_time) * 1000) push(name, "down", f"DNS resolution failed: {e}", ping_ms) return if not ips: ping_ms = int((time.time() - start_time) * 1000) push(name, "down", "DNS resolution failed: no IP returned", ping_ms) return healthy_count = 0 unhealthy_ips = [] # Check all IPs for ip in ips: is_up, err = check_single_actuator(ip, port, path, timeout) if is_up: healthy_count += 1 else: unhealthy_ips.append(f"{ip} {err}") ping_ms = int((time.time() - start_time) * 1000) if unhealthy_ips: total = len(ips) unhealthy_count = len(unhealthy_ips) msg = f"{unhealthy_count}/{total} unhealthy: {', '.join(unhealthy_ips)}" push(name, "down", msg, ping_ms) return if expected_replicas is not None and healthy_count < expected_replicas: resolved = len(ips) msg = f"{healthy_count}/{expected_replicas} replicas healthy (expected {expected_replicas}, resolved {resolved})" push(name, "down", msg, ping_ms) return total = expected_replicas if expected_replicas is not None else len(ips) push(name, "up", f"{healthy_count}/{total} replicas healthy", ping_ms) except Exception as e: ping_ms = int((time.time() - start_time) * 1000) logger.error(f"Actuator check failed for {name}: {e}") push(name, "down", str(e), ping_ms) def run_all_actuator_checks(): monitors = config.load_microservice_monitors() if not monitors: return with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: futures = [executor.submit(check_service, m) for m in monitors] concurrent.futures.wait(futures)