| |
| """ |
| RMI Health Monitoring Cron |
| ============================ |
| Checks: backend health, worker health (base + solana), Redis connectivity. |
| POSTs alerts to a webhook if anything is down. |
| |
| Run via cron every 1-5 minutes: |
| */2 * * * * cd /root/backend && python3 scripts/health_monitor.py >> /var/log/rmi_health.log 2>&1 |
| |
| Environment variables: |
| HEALTH_WEBHOOK_URL β webhook URL for alerts (Slack, Discord, PagerDuty, etc.) |
| BACKEND_URL β backend health endpoint (default: http://localhost:8000/health) |
| REDIS_HOST β Redis host (default: localhost) |
| REDIS_PORT β Redis port (default: 6379) |
| REDIS_PASSWORD β Redis password (default: empty) |
| BASE_WORKER_URL β Base worker health endpoint |
| SOLANA_WORKER_URL β Solana worker health endpoint |
| |
| Author: RMI Development |
| Date: 2026-05-20 |
| """ |
|
|
| import json |
| import logging |
| import os |
| import sys |
| import time |
| import urllib.error |
| import urllib.request |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(message)s", |
| ) |
| logger = logging.getLogger("health_monitor") |
|
|
| |
|
|
| HEALTH_WEBHOOK_URL = os.getenv("HEALTH_WEBHOOK_URL", "") |
| BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000/health") |
| REDIS_HOST = os.getenv("REDIS_HOST", "localhost") |
| REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) |
| REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") |
| BASE_WORKER_URL = os.getenv("BASE_WORKER_URL", "http://localhost:8000/api/v1/x402/fallback/status") |
| SOLANA_WORKER_URL = os.getenv("SOLANA_WORKER_URL", "") |
| CHECK_TIMEOUT = 10 |
| ALERT_COOLDOWN = 300 |
|
|
| |
| _last_alerts: dict = {} |
|
|
|
|
| def _load_cooldown_state(): |
| """Load alert cooldown state from file (survives restarts).""" |
| try: |
| state_file = "/tmp/rmi_health_cooldown.json" |
| if os.path.exists(state_file): |
| with open(state_file) as f: |
| return json.load(f) |
| except Exception: |
| pass |
| return {} |
|
|
|
|
| def _save_cooldown_state(state: dict): |
| """Save alert cooldown state to file.""" |
| try: |
| state_file = "/tmp/rmi_health_cooldown.json" |
| with open(state_file, "w") as f: |
| json.dump(state, f) |
| except Exception: |
| pass |
|
|
|
|
| def _should_alert(service: str) -> bool: |
| """Check if we should send an alert (respects cooldown).""" |
| now = time.time() |
| state = _load_cooldown_state() |
| last = state.get(service, 0) |
| if now - last < ALERT_COOLDOWN: |
| return False |
| state[service] = now |
| _save_cooldown_state(state) |
| return True |
|
|
|
|
| |
|
|
|
|
| def check_backend() -> dict: |
| """Check backend /health endpoint.""" |
| try: |
| req = urllib.request.Request(BACKEND_URL, method="GET") |
| req.add_header("Accept", "application/json") |
| with urllib.request.urlopen(req, timeout=CHECK_TIMEOUT) as resp: |
| data = json.loads(resp.read().decode()) |
| return {"service": "backend", "status": "healthy", "details": data} |
| except Exception as e: |
| return {"service": "backend", "status": "down", "error": str(e)[:200]} |
|
|
|
|
| def check_redis() -> dict: |
| """Check Redis connectivity.""" |
| try: |
| import redis |
|
|
| r = redis.Redis( |
| host=REDIS_HOST, |
| port=REDIS_PORT, |
| password=REDIS_PASSWORD, |
| decode_responses=True, |
| ) |
| r.ping() |
| info = r.info("server") |
| return { |
| "service": "redis", |
| "status": "healthy", |
| "details": { |
| "version": info.get("redis_version", "unknown"), |
| "uptime_seconds": info.get("uptime_in_seconds", 0), |
| "connected_clients": info.get("connected_clients", 0), |
| "used_memory_human": info.get("used_memory_human", "unknown"), |
| }, |
| } |
| except Exception as e: |
| return {"service": "redis", "status": "down", "error": str(e)[:200]} |
|
|
|
|
| def check_base_worker() -> dict: |
| """Check Base x402 worker/gateway health.""" |
| try: |
| req = urllib.request.Request(BASE_WORKER_URL, method="GET") |
| req.add_header("Accept", "application/json") |
| with urllib.request.urlopen(req, timeout=CHECK_TIMEOUT) as resp: |
| data = json.loads(resp.read().decode()) |
| return {"service": "base_worker", "status": "healthy", "details": data} |
| except Exception as e: |
| return {"service": "base_worker", "status": "down", "error": str(e)[:200]} |
|
|
|
|
| def check_solana_worker() -> dict: |
| """Check Solana x402 worker/gateway health.""" |
| if not SOLANA_WORKER_URL: |
| return { |
| "service": "solana_worker", |
| "status": "not_configured", |
| "details": "SOLANA_WORKER_URL not set", |
| } |
| try: |
| req = urllib.request.Request(SOLANA_WORKER_URL, method="GET") |
| req.add_header("Accept", "application/json") |
| with urllib.request.urlopen(req, timeout=CHECK_TIMEOUT) as resp: |
| data = json.loads(resp.read().decode()) |
| return {"service": "solana_worker", "status": "healthy", "details": data} |
| except Exception as e: |
| return {"service": "solana_worker", "status": "down", "error": str(e)[:200]} |
|
|
|
|
| |
|
|
|
|
| def send_alert(results: list): |
| """POST alert to webhook for any down services.""" |
| if not HEALTH_WEBHOOK_URL: |
| logger.warning("HEALTH_WEBHOOK_URL not set β skipping alert dispatch") |
| return |
|
|
| down_services = [r for r in results if r.get("status") == "down"] |
| if not down_services: |
| return |
|
|
| |
| alertable = [s for s in down_services if _should_alert(s["service"])] |
| if not alertable: |
| logger.info(f"{len(down_services)} service(s) down but in alert cooldown") |
| return |
|
|
| |
| alert_lines = [] |
| for s in alertable: |
| alert_lines.append(f"**{s['service']}** is DOWN: {s.get('error', 'unknown error')}") |
|
|
| |
| |
| payload = { |
| "text": f"RMI Health Alert β {len(alertable)} service(s) down", |
| "attachments": [ |
| { |
| "title": "Service Health Alert", |
| "color": "danger", |
| "text": "\n".join(alert_lines), |
| "footer": "RMI Health Monitor", |
| "ts": int(time.time()), |
| } |
| ], |
| |
| "services": alertable, |
| "all_results": results, |
| } |
|
|
| |
| try: |
| data = json.dumps(payload).encode() |
| req = urllib.request.Request( |
| HEALTH_WEBHOOK_URL, |
| data=data, |
| method="POST", |
| headers={"Content-Type": "application/json"}, |
| ) |
| with urllib.request.urlopen(req, timeout=10) as resp: |
| logger.info(f"Alert sent to webhook: HTTP {resp.status} β {len(alertable)} services down") |
| except Exception as e: |
| logger.error(f"Failed to send alert to webhook: {e}") |
|
|
|
|
| def send_recovery_alert(recovered: list): |
| """Send recovery notification.""" |
| if not HEALTH_WEBHOOK_URL or not recovered: |
| return |
|
|
| |
| state = _load_cooldown_state() |
| for s in recovered: |
| service_name = s["service"] |
| if service_name in state: |
| del state[service_name] |
| _save_cooldown_state(state) |
|
|
| alertable = [s for s in recovered if _should_alert(f"{s['service']}_recovery")] |
| if not alertable: |
| return |
|
|
| lines = [f"**{s['service']}** is back UP" for s in alertable] |
| payload = { |
| "text": f"RMI Recovery β {len(alertable)} service(s) recovered", |
| "attachments": [ |
| { |
| "title": "Service Recovery", |
| "color": "good", |
| "text": "\n".join(lines), |
| "footer": "RMI Health Monitor", |
| "ts": int(time.time()), |
| } |
| ], |
| } |
| try: |
| data = json.dumps(payload).encode() |
| req = urllib.request.Request( |
| HEALTH_WEBHOOK_URL, |
| data=data, |
| method="POST", |
| headers={"Content-Type": "application/json"}, |
| ) |
| with urllib.request.urlopen(req, timeout=10): |
| logger.info(f"Recovery alert sent: {len(alertable)} services recovered") |
| except Exception as e: |
| logger.error(f"Failed to send recovery alert: {e}") |
|
|
|
|
| |
|
|
|
|
| def _load_prev_state(): |
| """Load previous health state for recovery detection.""" |
| try: |
| state_file = "/tmp/rmi_health_state.json" |
| if os.path.exists(state_file): |
| with open(state_file) as f: |
| return json.load(f) |
| except Exception: |
| pass |
| return {} |
|
|
|
|
| def _save_prev_state(state: dict): |
| """Save current health state for recovery detection.""" |
| try: |
| state_file = "/tmp/rmi_health_state.json" |
| with open(state_file, "w") as f: |
| json.dump(state, f) |
| except Exception: |
| pass |
|
|
|
|
| |
|
|
|
|
| def main(): |
| """Run all health checks and dispatch alerts.""" |
| |
| if "--dotenv" in sys.argv: |
| import dotenv |
|
|
| idx = sys.argv.index("--dotenv") |
| if idx + 1 < len(sys.argv): |
| dotenv.load_dotenv(sys.argv[idx + 1]) |
| else: |
| dotenv.load_dotenv() |
| elif os.path.exists("/root/.env"): |
| try: |
| from dotenv import load_dotenv |
|
|
| load_dotenv("/root/.env") |
| except ImportError: |
| pass |
|
|
| |
| global HEALTH_WEBHOOK_URL, BACKEND_URL, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD |
| global BASE_WORKER_URL, SOLANA_WORKER_URL |
| HEALTH_WEBHOOK_URL = os.getenv("HEALTH_WEBHOOK_URL", "") |
| BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000/health") |
| REDIS_HOST = os.getenv("REDIS_HOST", "localhost") |
| REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) |
| REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") |
| BASE_WORKER_URL = os.getenv("BASE_WORKER_URL", "http://localhost:8000/api/v1/x402/fallback/status") |
| SOLANA_WORKER_URL = os.getenv("SOLANA_WORKER_URL", "") |
|
|
| logger.info("Running health checks...") |
|
|
| results = [ |
| check_backend(), |
| check_redis(), |
| check_base_worker(), |
| check_solana_worker(), |
| ] |
|
|
| |
| prev_state = _load_prev_state() |
| current_state = {r["service"]: r["status"] for r in results} |
| recovered = [] |
| for r in results: |
| if r["status"] == "healthy" and prev_state.get(r["service"]) == "down": |
| recovered.append(r) |
|
|
| |
| _save_prev_state(current_state) |
|
|
| |
| for r in results: |
| status = r["status"] |
| if status == "healthy": |
| logger.info(f" {r['service']}: healthy") |
| elif status == "not_configured": |
| logger.info(f" {r['service']}: not configured (skipped)") |
| else: |
| logger.error(f" {r['service']}: DOWN β {r.get('error', 'unknown')}") |
|
|
| |
| send_alert(results) |
|
|
| |
| if recovered: |
| send_recovery_alert(recovered) |
| for r in recovered: |
| logger.info(f" {r['service']}: RECOVERED") |
|
|
| |
| healthy = sum(1 for r in results if r["status"] == "healthy") |
| total = len(results) |
| down = sum(1 for r in results if r["status"] == "down") |
|
|
| logger.info(f"Health summary: {healthy}/{total} healthy, {down} down") |
|
|
| |
| if down > 0: |
| sys.exit(1) |
| else: |
| sys.exit(0) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|