Skip to content

Commit b71e7e1

Browse files
Pigbibiclaude
andcommitted
refactor(health): unified HealthMonitor API — one class for all deployments
- HealthMonitor(app=...) → Flask /health endpoint - HealthMonitor(http_port=8080) → standalone HTTP for CLI/VPS - HealthMonitor(file_path='/tmp/qsl.heartbeat') → file-based for self-hosted - HealthMonitor() → auto-detect (Firestore if GCP, otherwise file) - Backward-compat: register_health_endpoint, FileHeartbeat, FirestoreHeartbeat Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9f49a2e commit b71e7e1

2 files changed

Lines changed: 192 additions & 213 deletions

File tree

scripts/qsl_watchdog.py

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#!/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.
35
4-
Works with any deployment type:
56
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.
811
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.
1013
"""
1114
from __future__ import annotations
1215

@@ -15,56 +18,47 @@
1518
import sys
1619

1720

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()
2124
if not token or not chat_id:
22-
print("No Telegram config; cannot send alert.", file=sys.stderr)
2325
return False
2426
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+
)
2833
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
3136
except Exception:
3237
return False
3338

3439

3540
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)")
4247
p.add_argument("--alert", action="store_true", help="Send Telegram alert on failure")
4348
args = p.parse_args()
4449

45-
from quant_platform_kit.common.health import check_service_alive, FileHeartbeat, FirestoreHeartbeat
50+
from quant_platform_kit.common.health import check_alive
4651

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)
5953

6054
if alive:
6155
print(f"✅ {args.name}: {detail}")
6256
return 0
6357

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}")
6660
if args.alert:
67-
send_telegram(msg)
61+
_telegram_alert(msg)
6862
return 1
6963

7064

0 commit comments

Comments
 (0)