From 5edc5d49a8bdf5dc2b5e435b0516ed87cb70d71c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 29 Jun 2026 09:35:49 +0200 Subject: [PATCH] monitoring: BSN Health Check mit Testschleife (snkbnet + CF-Europa-Re-Routing) - 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 --- monitoring/bsn_health_check.py | 255 +++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 monitoring/bsn_health_check.py diff --git a/monitoring/bsn_health_check.py b/monitoring/bsn_health_check.py new file mode 100644 index 0000000..9044d99 --- /dev/null +++ b/monitoring/bsn_health_check.py @@ -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()