#!/usr/bin/env python3 """ Simple uptime/health monitor. Checks the deployed app's /health endpoint every 30 seconds, logs the result, and writes a rolling JSON history file that the dashboard (or anything else) can read. Usage: python monitor.py --url https://your-app.ghaymah.systems python monitor.py --url http://localhost:3000 --interval 30 """ import argparse import json import time import urllib.request import urllib.error from datetime import datetime, timezone from pathlib import Path LOG_FILE = Path("monitor.log") HISTORY_FILE = Path("monitor_history.json") MAX_HISTORY = 200 # keep the last N checks def now_iso(): return datetime.now(timezone.utc).isoformat() def load_history(): if HISTORY_FILE.exists(): try: return json.loads(HISTORY_FILE.read_text()) except json.JSONDecodeError: return [] return [] def save_history(history): HISTORY_FILE.write_text(json.dumps(history[-MAX_HISTORY:], indent=2)) def log_line(message): line = f"[{now_iso()}] {message}" print(line) with LOG_FILE.open("a", encoding="utf-8") as f: f.write(line + "\n") def check_health(base_url, timeout, health_path="/health"): url = base_url.rstrip("/") + health_path start = time.monotonic() try: req = urllib.request.Request(url, headers={"User-Agent": "ghaymah-monitor/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: elapsed_ms = round((time.monotonic() - start) * 1000, 2) status_code = resp.getcode() raw = resp.read().decode("utf-8", errors="ignore") try: body = json.loads(raw) except json.JSONDecodeError: # Not a JSON API (e.g. static site returning plain "ok") - # any 200 response is considered healthy. body = {"raw": raw.strip()[:200]} healthy = status_code == 200 return { "timestamp": now_iso(), "healthy": healthy, "status_code": status_code, "response_time_ms": elapsed_ms, "detail": body, } except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, Exception) as exc: elapsed_ms = round((time.monotonic() - start) * 1000, 2) return { "timestamp": now_iso(), "healthy": False, "status_code": None, "response_time_ms": elapsed_ms, "error": str(exc), } def main(): parser = argparse.ArgumentParser(description="Health monitor for the deployed API") parser.add_argument("--url", required=True, help="Base URL of the deployed app, e.g. https://myapp.ghaymah.systems") parser.add_argument("--health-path", default="/health", help="Path to check (default: /health)") parser.add_argument("--interval", type=int, default=30, help="Seconds between checks (default: 30)") parser.add_argument("--timeout", type=int, default=5, help="Request timeout in seconds (default: 5)") parser.add_argument("--once", action="store_true", help="Run a single check and exit (useful for testing)") args = parser.parse_args() history = load_history() consecutive_failures = 0 log_line(f"Starting monitor for {args.url} (interval={args.interval}s)") while True: result = check_health(args.url, args.timeout, args.health_path) history.append(result) save_history(history) if result["healthy"]: consecutive_failures = 0 log_line(f"OK status={result['status_code']} response_time={result['response_time_ms']}ms") else: consecutive_failures += 1 reason = result.get("error", f"status_code={result.get('status_code')}") log_line(f"FAIL consecutive_failures={consecutive_failures} reason={reason}") if consecutive_failures == 3: log_line("ALERT: app has failed 3 consecutive health checks") if args.once: break time.sleep(args.interval) if __name__ == "__main__": main()