|
1 | 1 | #!/usr/bin/env python3 |
2 | | -"""Universal QSL watchdog — ping health endpoints and alert on failure. |
| 2 | +"""Universal QSL Watchdog — ping health endpoints, alert on failure. |
| 3 | +
|
| 4 | +Works for ANY deployment — Cloud Run, VPS, bare metal, self-hosted. |
3 | 5 |
|
4 | | -Works with any deployment type: |
5 | 6 | Cloud Run: qsl_watchdog.py --url https://my-service.run.app |
6 | | - VPS: qsl_watchdog.py --file /var/run/qsl/heartbeat.json |
7 | | - Firestore: qsl_watchdog.py --firestore health/alive |
| 7 | + VPS HTTP: qsl_watchdog.py --url http://my-vps:8080 |
| 8 | + File-based: qsl_watchdog.py --file /tmp/qsl.heartbeat |
| 9 | +
|
| 10 | +Exit 0 = alive, 1 = dead. Can be used with cron, GitHub Actions, or any scheduler. |
8 | 11 |
|
9 | | -Exit code 0 = alive, 1 = dead (integrates with cron / GitHub Actions). |
| 12 | +Optional Telegram alert: set TELEGRAM_TOKEN + GLOBAL_TELEGRAM_CHAT_ID env vars. |
10 | 13 | """ |
11 | 14 | from __future__ import annotations |
12 | 15 |
|
|
15 | 18 | import sys |
16 | 19 |
|
17 | 20 |
|
18 | | -def send_telegram(message: str) -> bool: |
19 | | - token = (os.environ.get("TELEGRAM_TOKEN") or os.environ.get("TG_TOKEN") or "").strip() |
20 | | - chat_id = (os.environ.get("GLOBAL_TELEGRAM_CHAT_ID") or os.environ.get("TG_CHAT_ID") or "").strip() |
| 21 | +def _telegram_alert(message: str) -> bool: |
| 22 | + token = os.environ.get("TELEGRAM_TOKEN", "").strip() |
| 23 | + chat_id = os.environ.get("GLOBAL_TELEGRAM_CHAT_ID", "").strip() |
21 | 24 | if not token or not chat_id: |
22 | | - print("No Telegram config; cannot send alert.", file=sys.stderr) |
23 | 25 | return False |
24 | 26 | import json, urllib.request |
25 | | - url = f"https://api.telegram.org/bot{token}/sendMessage" |
26 | | - body = json.dumps({"chat_id": chat_id, "text": message, "parse_mode": "Markdown"}).encode() |
27 | | - req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST") |
| 27 | + req = urllib.request.Request( |
| 28 | + f"https://api.telegram.org/bot{token}/sendMessage", |
| 29 | + data=json.dumps({"chat_id": chat_id, "text": message}).encode(), |
| 30 | + headers={"Content-Type": "application/json"}, |
| 31 | + method="POST", |
| 32 | + ) |
28 | 33 | try: |
29 | | - with urllib.request.urlopen(req, timeout=10) as resp: |
30 | | - return resp.status == 200 |
| 34 | + with urllib.request.urlopen(req, timeout=10) as r: |
| 35 | + return r.status == 200 |
31 | 36 | except Exception: |
32 | 37 | return False |
33 | 38 |
|
34 | 39 |
|
35 | 40 | def main() -> int: |
36 | | - p = argparse.ArgumentParser(description="QSL health watchdog") |
37 | | - p.add_argument("--url", help="HTTP health endpoint URL") |
38 | | - p.add_argument("--file", help="Local heartbeat file path") |
39 | | - p.add_argument("--firestore", help="Firestore collection/document (e.g. health/alive)") |
40 | | - p.add_argument("--name", default="QSL Platform", help="Service name for alert messages") |
41 | | - p.add_argument("--max-age", type=int, default=300, help="Max heartbeat age in seconds") |
| 41 | + p = argparse.ArgumentParser(description="Universal QSL health watchdog") |
| 42 | + g = p.add_mutually_exclusive_group(required=True) |
| 43 | + g.add_argument("--url", help="HTTP health endpoint URL (e.g. https://svc.run.app or http://vps:8080)") |
| 44 | + g.add_argument("--file", help="Local heartbeat file path (e.g. /tmp/qsl.heartbeat)") |
| 45 | + p.add_argument("--name", default="QSL Platform", help="Service name for alerts") |
| 46 | + p.add_argument("--max-age", type=int, default=300, help="Max heartbeat age in seconds (default 300)") |
42 | 47 | p.add_argument("--alert", action="store_true", help="Send Telegram alert on failure") |
43 | 48 | args = p.parse_args() |
44 | 49 |
|
45 | | - from quant_platform_kit.common.health import check_service_alive, FileHeartbeat, FirestoreHeartbeat |
| 50 | + from quant_platform_kit.common.health import check_alive |
46 | 51 |
|
47 | | - reader = None |
48 | | - if args.file: |
49 | | - reader = FileHeartbeat(args.file) |
50 | | - elif args.firestore: |
51 | | - parts = args.firestore.split("/") |
52 | | - reader = FirestoreHeartbeat(parts[0], parts[1] if len(parts) > 1 else "alive") |
53 | | - |
54 | | - alive, detail = check_service_alive( |
55 | | - heartbeat_url=args.url, |
56 | | - heartbeat_reader=reader, |
57 | | - max_age_seconds=args.max_age, |
58 | | - ) |
| 52 | + alive, detail = check_alive(url=args.url or "", file_path=args.file or "", max_age_seconds=args.max_age) |
59 | 53 |
|
60 | 54 | if alive: |
61 | 55 | print(f"✅ {args.name}: {detail}") |
62 | 56 | return 0 |
63 | 57 |
|
64 | | - msg = f"🚨 *{args.name}* health check FAILED: `{detail}`" |
65 | | - print(f"❌ {args.name}: {detail}") |
| 58 | + msg = f"🚨 *{args.name}* DOWN: {detail}" |
| 59 | + print(f"❌ {msg}") |
66 | 60 | if args.alert: |
67 | | - send_telegram(msg) |
| 61 | + _telegram_alert(msg) |
68 | 62 | return 1 |
69 | 63 |
|
70 | 64 |
|
|
0 commit comments