monitoring: BSN Health Check mit Testschleife (snkbnet + CF-Europa-Re-Routing)
Quality Gate / 🛡️ Lint + Type Check + Tests (push) Failing after 14m50s
Quality Gate / 🛡️ Lint + Type Check + Tests (push) Failing after 14m50s
- Erweitert um snkbnet.de-Check: Server- vs Cloudflare-Problem-Differenzierung - CF-Status-Seiten-Scraping auf Europa-Re-Routing (HTML + Components-API) - Gleiche Logik: still bei OK, Alarm nur bei Problemen
This commit is contained in:
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""BSN Health Check — Überwacht brettspiel-news.de + Cloudflare-Status.
|
||||||
|
|
||||||
|
Ausgabe: LEER wenn alles OK. Nur bei Problemen wird ein Alarm-Text ausgegeben.
|
||||||
|
Läuft als Hermes-Cronjob (no_agent=True, alle 5 Minuten).
|
||||||
|
|
||||||
|
Überwachte Fehlertypen:
|
||||||
|
- HTTP 5xx mit CF-Ray-Header (Cloudflare-Fehlerseiten)
|
||||||
|
- HTTP 502/521/522/523/524/525/526/530 (spezifische CF-Error-Codes)
|
||||||
|
- Cloudflare-Status-API: ungelöste Incidents (gefiltert auf CDN/DNS/Routing)
|
||||||
|
- Komplettausfall (keine HTTP-Antwort)
|
||||||
|
|
||||||
|
Testschleife (neu):
|
||||||
|
1. Wenn Seite offline/CF-Fehler → prüfe Alternativ-URL (wp.snkbnet.de)
|
||||||
|
→ Erreichbar = Server läuft, Cloudflare-Problem wahrscheinlich
|
||||||
|
2. Cloudflare-Status-Seite scrapen → Europa-Re-Routing erkennen
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
TARGET = "https://www.brettspiel-news.de/"
|
||||||
|
SNKBNET_URL = "https://wp.snkbnet.de/login_up.php?success_redirect_url=%2Fsmb%2Fweb%2Fview"
|
||||||
|
CF_STATUS_API = "https://www.cloudflarestatus.com/api/v2"
|
||||||
|
CF_STATUS_PAGE = "https://www.cloudflarestatus.com/"
|
||||||
|
TIMEOUT = 15
|
||||||
|
|
||||||
|
# Cloudflare-Fehlermuster (Body-Scan nach HTTP 5xx)
|
||||||
|
CF_ERROR_PATTERNS = [
|
||||||
|
("Error 502", "502 Bad Gateway — Origin-Server nicht erreichbar"),
|
||||||
|
("Error 521", "521 Web Server Down — Server antwortet nicht"),
|
||||||
|
("Error 522", "522 Connection Timeout — Timeout zum Origin"),
|
||||||
|
("Error 523", "523 Origin Unreachable — Origin nicht erreichbar"),
|
||||||
|
("Error 524", "524 A Timeout Occurred — Timeout"),
|
||||||
|
("Error 525", "525 SSL Handshake Failed — SSL-Fehler"),
|
||||||
|
("Error 526", "526 Invalid SSL Certificate — Ungültiges Zertifikat"),
|
||||||
|
("Error 530", "530 Origin DNS Error — DNS-Fehler"),
|
||||||
|
("cloudflare-nginx", "Cloudflare-nginx — CF-Fehlerseite"),
|
||||||
|
("Bad gateway", "Bad Gateway — CF-Proxy-Fehler"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Cloudflare-Services die NICHTS mit Website-Erreichbarkeit zu tun haben
|
||||||
|
IRRELEVANT_SERVICES = [
|
||||||
|
"workers ai", "workers kv", "durable objects", "d1", "r2",
|
||||||
|
"pages", "images", "stream", "zero trust", "access",
|
||||||
|
"spectrum", "magic", "warp",
|
||||||
|
"analytics", "logs", "dashboard", "registrar",
|
||||||
|
"billing", "support case", "ssl for saas", "custom certificates"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def curl(url, timeout=TIMEOUT):
|
||||||
|
"""Führe curl aus und gib Status, Header, Body zurück."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["curl", "-sL", "-o", "/tmp/bsn_health_body.tmp",
|
||||||
|
"-w", "%{http_code}|%{time_total}|%{size_download}",
|
||||||
|
"-D", "/tmp/bsn_health_headers.tmp",
|
||||||
|
"--max-time", str(timeout), url],
|
||||||
|
capture_output=True, text=True, timeout=timeout + 5
|
||||||
|
)
|
||||||
|
parts = result.stdout.strip().split("|")
|
||||||
|
http_code = int(parts[0]) if parts and parts[0].isdigit() else 0
|
||||||
|
time_total = float(parts[1]) if len(parts) > 1 else 0
|
||||||
|
size = int(parts[2]) if len(parts) > 2 else 0
|
||||||
|
|
||||||
|
body = headers = ""
|
||||||
|
try:
|
||||||
|
with open("/tmp/bsn_health_body.tmp", "r", errors="replace") as f:
|
||||||
|
body = f.read()[:5000]
|
||||||
|
with open("/tmp/bsn_health_headers.tmp", "r", errors="replace") as f:
|
||||||
|
headers = f.read()[:2000]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"http_code": http_code, "time": time_total, "size": size,
|
||||||
|
"body": body, "headers": headers, "ok": True}
|
||||||
|
except Exception as e:
|
||||||
|
return {"http_code": 0, "time": 0, "size": 0,
|
||||||
|
"body": "", "headers": "", "ok": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def check_snkbnet():
|
||||||
|
"""Testschleife: Prüfe Alternativ-URL auf demselben Server.
|
||||||
|
|
||||||
|
Wenn wp.snkbnet.de erreichbar ist, läuft der Server grundsätzlich —
|
||||||
|
das Problem liegt dann mit hoher Wahrscheinlichkeit bei Cloudflare.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict mit 'reachable' (bool), 'http_code', 'time', 'error'
|
||||||
|
"""
|
||||||
|
r = curl(SNKBNET_URL)
|
||||||
|
if not r["ok"]:
|
||||||
|
return {"reachable": False, "http_code": 0, "time": 0,
|
||||||
|
"error": r.get("error", "Unbekannt")}
|
||||||
|
# Erfolg: Alles außer 5xx gilt als erreichbar
|
||||||
|
# (302/303 Redirects bei Login sind normal und bedeuten Server läuft)
|
||||||
|
is_reachable = r["http_code"] < 500 and r["http_code"] > 0
|
||||||
|
return {"reachable": is_reachable, "http_code": r["http_code"],
|
||||||
|
"time": r["time"], "error": None}
|
||||||
|
|
||||||
|
|
||||||
|
def check_cf_europe_rerouting():
|
||||||
|
"""Scrape cloudflarestatus.com auf Europa-Re-Routing.
|
||||||
|
|
||||||
|
Sucht die HTML-Seite nach Hinweisen auf Rerouting in Europa.
|
||||||
|
Cloudflare listet regionale Status in der Komponenten-Tabelle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: Alarm-Text bei Fund, None sonst
|
||||||
|
"""
|
||||||
|
r = curl(CF_STATUS_PAGE)
|
||||||
|
if not r["ok"] or r["http_code"] != 200:
|
||||||
|
return None
|
||||||
|
|
||||||
|
html = r["body"].lower()
|
||||||
|
alerts = []
|
||||||
|
|
||||||
|
# Suche nach "re-routed" oder "partially re-routed" im Europa-Kontext
|
||||||
|
# Pattern: Im HTML gibt's Tabellen/Divs mit Region+Status
|
||||||
|
reroute_patterns = [
|
||||||
|
(r"europe.*?re-routed", "🟡 Europa: Re-Routed erkannt"),
|
||||||
|
(r"europe.*?partially re-routed", "🟠 Europa: Partially Re-Routed"),
|
||||||
|
(r"re-routed.*?europe", "🟡 Re-Routed in Europa"),
|
||||||
|
(r"partially re-routed.*?europe", "🟠 Partially Re-Routed in Europa"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, desc in reroute_patterns:
|
||||||
|
if re.search(pattern, html, re.DOTALL):
|
||||||
|
alerts.append(f" • {desc}")
|
||||||
|
|
||||||
|
# Alternativ: Prüfe Components-API auf Europa-Status
|
||||||
|
try:
|
||||||
|
comp_r = curl(CF_STATUS_API + "/components.json")
|
||||||
|
if comp_r["ok"]:
|
||||||
|
data = json.loads(comp_r["body"])
|
||||||
|
components = data.get("components", [])
|
||||||
|
for comp in components:
|
||||||
|
name = comp.get("name", "").lower()
|
||||||
|
status = comp.get("status", "")
|
||||||
|
# "partial_outage" oder "degraded_performance" im Europa-Kontext
|
||||||
|
if ("europe" in name or "europa" in name) and \
|
||||||
|
status in ("partial_outage", "degraded_performance", "major_outage"):
|
||||||
|
alerts.append(
|
||||||
|
f" • CF-Komponente [{status}]: {comp.get('name', '?')}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "\n".join(alerts) if alerts else None
|
||||||
|
|
||||||
|
|
||||||
|
def check_cf_status():
|
||||||
|
"""Prüfe Cloudflare-Status-API auf ungelöste Incidents."""
|
||||||
|
try:
|
||||||
|
r = curl(CF_STATUS_API + "/incidents/unresolved.json")
|
||||||
|
if not r["ok"]:
|
||||||
|
return None
|
||||||
|
data = json.loads(r["body"])
|
||||||
|
incidents = data.get("incidents", [])
|
||||||
|
if not incidents:
|
||||||
|
return None
|
||||||
|
alerts = []
|
||||||
|
for inc in incidents:
|
||||||
|
name = inc.get("name", "").lower()
|
||||||
|
if any(kw in name for kw in IRRELEVANT_SERVICES):
|
||||||
|
continue
|
||||||
|
impact = inc.get("impact", "unknown")
|
||||||
|
link = inc.get("shortlink", "")
|
||||||
|
alerts.append(f"🔴 CF-Incident [{impact}]: {inc.get('name', '?')}\n {link}")
|
||||||
|
return "\n".join(alerts) if alerts else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
now = datetime.now(timezone.utc).strftime("%d.%m.%Y %H:%M UTC")
|
||||||
|
alerts = []
|
||||||
|
|
||||||
|
r = curl(TARGET)
|
||||||
|
has_problem = False
|
||||||
|
|
||||||
|
# --- Fall 1: Komplettausfall ---
|
||||||
|
if not r["ok"]:
|
||||||
|
has_problem = True
|
||||||
|
alerts.append(f"❌ brettspiel-news.de NICHT ERREICHBAR\n Fehler: {r.get('error', 'Unbekannt')}")
|
||||||
|
|
||||||
|
# --- Fall 2: HTTP-Fehler ---
|
||||||
|
elif r["http_code"] >= 500:
|
||||||
|
has_problem = True
|
||||||
|
has_cf = "cf-ray" in r["headers"].lower()
|
||||||
|
if has_cf:
|
||||||
|
cf_ray = ""
|
||||||
|
for line in r["headers"].split("\n"):
|
||||||
|
if line.lower().startswith("cf-ray:"):
|
||||||
|
cf_ray = line.split(":", 1)[1].strip()
|
||||||
|
break
|
||||||
|
found = []
|
||||||
|
for pattern, desc in CF_ERROR_PATTERNS:
|
||||||
|
if pattern.lower() in r["body"].lower():
|
||||||
|
found.append(f" • {desc}")
|
||||||
|
alerts.append(
|
||||||
|
f"❌ brettspiel-news.de: HTTP {r['http_code']} (CF-Fehler)\n"
|
||||||
|
f" CF-Ray: {cf_ray}\n" + ("\n".join(found) if found else " CF-Fehlerseite erkannt")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
alerts.append(f"⚠️ brettspiel-news.de: HTTP {r['http_code']} (Server-Fehler)")
|
||||||
|
|
||||||
|
elif r["http_code"] == 403:
|
||||||
|
has_problem = True
|
||||||
|
alerts.append(f"⚠️ brettspiel-news.de: HTTP 403 (möglicher CF-Block/Challenge)")
|
||||||
|
|
||||||
|
elif r["http_code"] == 0 or r["http_code"] >= 600:
|
||||||
|
has_problem = True
|
||||||
|
alerts.append(f"❌ brettspiel-news.de: Keine gültige HTTP-Antwort (Code: {r['http_code']})")
|
||||||
|
|
||||||
|
# --- Testschleife: Nur bei Problemen ---
|
||||||
|
if has_problem:
|
||||||
|
# 1. Prüfe Alternativ-URL (gleicher Server, andere Domain)
|
||||||
|
snkb = check_snkbnet()
|
||||||
|
if snkb["reachable"]:
|
||||||
|
alerts.append(
|
||||||
|
f"\n🧪 Testschleife:\n"
|
||||||
|
f" ✅ Alternativ-URL erreichbar (HTTP {snkb['http_code']}, {snkb['time']:.2f}s)\n"
|
||||||
|
f" 📡 Server läuft — Cloudflare-Problem wahrscheinlich"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
alerts.append(
|
||||||
|
f"\n🧪 Testschleife:\n"
|
||||||
|
f" ❌ Alternativ-URL NICHT erreichbar"
|
||||||
|
+ (f" (Fehler: {snkb['error']})" if snkb.get('error') else "")
|
||||||
|
+ f"\n ⚠️ Server selbst könnte betroffen sein"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Cloudflare-Status-Seite auf Europa-Re-Routing prüfen
|
||||||
|
cf_europe = check_cf_europe_rerouting()
|
||||||
|
if cf_europe:
|
||||||
|
alerts.append(f"\n🌍 Cloudflare-Status (Europa):\n{cf_europe}")
|
||||||
|
else:
|
||||||
|
alerts.append("\n🌍 Cloudflare-Status: Kein Europa-Re-Routing erkannt")
|
||||||
|
|
||||||
|
# --- CF-Status-API immer mitprüfen ---
|
||||||
|
cf_alert = check_cf_status()
|
||||||
|
if cf_alert:
|
||||||
|
alerts.append(f"\n📢 Cloudflare-Incidents:\n{cf_alert}")
|
||||||
|
|
||||||
|
# --- Ausgabe ---
|
||||||
|
if has_problem:
|
||||||
|
print(f"🚨 BSN-Health-Alarm {now}\n" + "\n".join(alerts))
|
||||||
|
# Sonst: still (kein stdout → kein Alarm)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user