Add external monitoring stack and alerting docs

This commit is contained in:
2569718930@qq.com
2026-04-01 01:49:58 +08:00
parent 6626b5472c
commit a8462e188b
13 changed files with 729 additions and 1 deletions
+103
View File
@@ -0,0 +1,103 @@
from __future__ import annotations
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any, Dict, List
import requests
def _chat_ids() -> List[str]:
raw = (
os.getenv("POLYWEATHER_MONITORING_ALERT_CHAT_IDS")
or os.getenv("TELEGRAM_CHAT_IDS")
or os.getenv("TELEGRAM_CHAT_ID")
or ""
)
return [item.strip() for item in raw.split(",") if item.strip()]
def _format_alerts(payload: Dict[str, Any]) -> str:
alerts = payload.get("alerts") or []
if not isinstance(alerts, list) or not alerts:
return "PolyWeather monitoring received an empty alert payload."
lines = ["PolyWeather monitoring alert"]
for alert in alerts[:10]:
if not isinstance(alert, dict):
continue
status = str(alert.get("status") or "unknown").upper()
labels = alert.get("labels") or {}
annotations = alert.get("annotations") or {}
alert_name = labels.get("alertname") or "unknown_alert"
severity = labels.get("severity") or "info"
summary = annotations.get("summary") or annotations.get("description") or ""
lines.append(f"- [{status}] {alert_name} ({severity})")
if summary:
lines.append(f" {summary}")
return "\n".join(lines)
def _send_telegram_message(text: str) -> None:
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
chat_ids = _chat_ids()
if not token or not chat_ids:
return
url = f"https://api.telegram.org/bot{token}/sendMessage"
for chat_id in chat_ids:
try:
requests.post(
url,
json={
"chat_id": chat_id,
"text": text,
"disable_web_page_preview": True,
},
timeout=10,
).raise_for_status()
except Exception:
continue
class _Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None: # noqa: N802
if self.path.rstrip("/") == "/healthz":
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(b'{"ok":true}')
return
self.send_error(404)
def do_POST(self) -> None: # noqa: N802
if self.path.rstrip("/") != "/alerts":
self.send_error(404)
return
length = int(self.headers.get("Content-Length", "0") or "0")
raw = self.rfile.read(length) if length > 0 else b"{}"
try:
payload = json.loads(raw.decode("utf-8"))
except Exception:
self.send_error(400, "invalid json")
return
if not isinstance(payload, dict):
self.send_error(400, "invalid payload")
return
_send_telegram_message(_format_alerts(payload))
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(b'{"ok":true}')
def log_message(self, format: str, *args: object) -> None: # noqa: A003
return
def main() -> None:
port = 9099
server = HTTPServer(("0.0.0.0", port), _Handler)
server.serve_forever()
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import argparse
import json
import sys
from typing import Dict, Tuple
import requests
def _get_json(url: str, timeout: float) -> Tuple[int, Dict]:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return response.status_code, response.json()
def _get_text(url: str, timeout: float) -> Tuple[int, str]:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return response.status_code, response.text
def main() -> int:
parser = argparse.ArgumentParser(description="Run basic PolyWeather ops checks.")
parser.add_argument("--base-url", default="http://127.0.0.1:8000")
parser.add_argument("--timeout", type=float, default=8.0)
args = parser.parse_args()
base = args.base_url.rstrip("/")
timeout = args.timeout
report = {"checks": []}
failed = False
try:
_, health = _get_json(f"{base}/healthz", timeout)
ok = str(health.get("status") or "").lower() == "ok"
report["checks"].append({"name": "healthz", "ok": ok, "detail": health})
failed = failed or not ok
except Exception as exc:
report["checks"].append({"name": "healthz", "ok": False, "detail": str(exc)})
failed = True
try:
_, status = _get_json(f"{base}/api/system/status", timeout)
features = status.get("features") or {}
ok = status.get("status") == "ok" and bool((status.get("db") or {}).get("ok"))
report["checks"].append({"name": "system_status", "ok": ok, "detail": {"features": features, "db": status.get("db")}})
failed = failed or not ok
except Exception as exc:
report["checks"].append({"name": "system_status", "ok": False, "detail": str(exc)})
failed = True
try:
_, metrics = _get_text(f"{base}/metrics", timeout)
ok = "polyweather_http_requests_total" in metrics or "polyweather_source_requests_total" in metrics
report["checks"].append({"name": "metrics", "ok": ok, "detail": "metrics exposed"})
failed = failed or not ok
except Exception as exc:
report["checks"].append({"name": "metrics", "ok": False, "detail": str(exc)})
failed = True
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())