# Watchdog-Integration Überwachung von Anwendungen, Diensten und Servern über Heartbeats. > **Stand:** Version 2.0 — vollständig überarbeitet. Wer eine ältere Integration > betreibt, findet die Änderungen in Abschnitt 9. --- ## 1. Wie der Zustand ermittelt wird Eine Anwendung meldet sich in festen Abständen. Bleibt die Meldung aus, stuft der **Evaluator** den Monitor herab: | Zeit seit dem letzten Heartbeat | Zustand | |---|---| | innerhalb des Intervalls | wie gemeldet (`up` / `warning`) | | mehr als das **Doppelte** | `warning` | | mehr als das **Vierfache** | `down` | Bei `interval: 60` heißt das: nach 2 Minuten auffällig, nach 4 Minuten ausgefallen. > **Wichtig:** Der Evaluator muss per Cron laufen. Ohne ihn ändert sich der > Zustand ausschließlich beim Eintreffen eines Heartbeats — ein abgestürzter > Dienst bliebe dauerhaft grün. > > ```bash > * * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet > ``` > > Fehlt der Job, zeigt das WebUI oben einen Warnhinweis. Details und die > HTTP-Variante: **[§11](#11-der-evaluator-tick)**. --- ## 2. Token besorgen **Nicht** das Beispiel-Token aus älteren Fassungen dieser Anleitung verwenden — es ist ein Demo-Wert und an eine einzelne Source gebunden. Zwei Wege: **a) Zentrales Token** (empfohlen für neue Integrationen) WebUI → **Token-Verwaltung → Master-Token erstellen**, Recht *🛡️ Watchdog Heartbeat*. Nicht an eine Source gebunden, funktioniert für beliebig viele Dienste. **b) Agent-Token je Monitor** WebUI → **WatchDog → Agent-Tokens**. An eine Source gebunden; meldet der Dienst unter einem anderen Namen, wird er abgewiesen. Beim Anlegen eines Monitors entsteht automatisch eines. Token nie im Quelltext ablegen — Umgebungsvariable oder Konfigurationsdatei. --- ## 3. Heartbeat senden ``` POST https://dc.mhdf.de/api/watchdog/v1/ping Authorization: Bearer (oder X-Agent-Token: ) Content-Type: application/json ``` ```json { "source": "polytrader-worker", "instance": "default", "type": "heartbeat", "status": "ok", "interval": 60, "message": "Verarbeite Warteschlange", "group": "Applications", "os": ".NET 8 Service", "version": "1.4.3", "checks": { "db": { "ok": true }, "market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" } }, "metrics": { "cpu": 18, "ram": 42, "queue_depth": 23 } } ``` | Feld | Pflicht | Bedeutung | |---|---|---| | `source` | ja | Eindeutiger Name des Dienstes oder Hosts | | `instance` | nein | Mehrere Instanzen desselben Dienstes (Vorgabe `default`) | | `type` | nein | `heartbeat`, `host`, `hypervisor_node`, `guest` | | `status` | nein | siehe unten (Vorgabe `ok`) | | `interval` | nein | Erwarteter Abstand in Sekunden (Vorgabe 60) | | `message` | nein | Kurztext, erscheint im Dashboard | | `group` | nein | Gruppierung im Dashboard | | `os` | nein | Plattform, steuert auch die Icon-Erkennung | | `version` | nein | Version der laufenden Anwendung (Alias: `app_version`) | | `checks` | nein | Selbst ermittelter Gesundheitszustand, siehe 4. | | `metrics` | nein | Numerische Werte, siehe 5. | ### Welche Version läuft dort? `version` erscheint als eigene Spalte in der Monitorliste und kommt im Heartbeat-Antwortobjekt als `app_version` zurück. Damit ist auf einen Blick sichtbar, ob ein Ausfall zeitlich zu einem Rollout passt — „Monitor X ist seit dem Rollout von 1.4.3 unten". Das Feld ist optional und **überschreibt einen früheren Wert nicht mit `null`**: ein Agent, der es nicht mitschickt, löscht die zuletzt gemeldete Version nicht. Bestehende Agenten laufen also unverändert weiter. ```csharp // Am einfachsten aus der vom Build erzeugten Klasse, siehe // UPDATESERVICE_INTEGRATION_GUIDE §2B version = BuildInfo.Version ``` > Der Fehler-Stream führt `build`, der Bugtracker `build_version`, die > Aktivierungsliste des Lizenzmoduls `app_version` — nur der Watchdog konnte > bis 2.1 nicht sagen, welche Version tatsächlich läuft. ### Zulässige Werte für `status` | Wert | Zustand | Wirkung | |---|---|---| | `ok` | `up` | Normalbetrieb | | `warning` | `warning` | Auffällig, aber arbeitsfähig | | `error` | `down` | Störung | | `stopped` | `stopped` | **Bewusst beendet** — der Evaluator meldet keinen Ausfall | | `maintenance` | `maintenance` | **Wartung** — von der Bewertung ausgenommen | `stopped` und `maintenance` sind der saubere Weg, ein geplantes Herunterfahren mitzuteilen. Ohne sie erzeugte jeder ordentliche Shutdown wenige Minuten später einen Fehlalarm. --- ## 4. Gesundheitszustand mitsenden Ein Heartbeat beweist nur, dass ein Thread läuft — nicht, dass die Anwendung ihre Arbeit tut. Der klassische Fall: Der Timer meldet brav `ok`, während der Datenfeed seit einer Stunde tot ist. Deshalb kann die Anwendung ihren Zustand selbst beurteilen und mitschicken: ```json "checks": { "db": { "ok": true }, "market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" }, "queue": { "ok": true, "value": 23 } } ``` Das Deploymentcenter **interpretiert die Namen nicht** — es liest nur `ok` und `message`. Was „gesund" bedeutet, entscheidet jede Anwendung selbst. Es gibt also keine zentral gepflegten Kennzahlen, an die sich alle Projekte anpassen müssten. Schlägt eine Prüfung fehl, wird ein als `ok` gemeldeter Heartbeat automatisch auf `warning` herabgestuft; die Antwort nennt die betroffenen Prüfungen: ```json { "status": "success", "monitor": { "state": "warning", "failing_checks": ["market_feed"] } } ``` Kurzform ohne Zusatzangaben ist ebenfalls erlaubt: `"checks": { "db": true, "market_feed": false }` > **Warum kein Abruf durch den Server?** Damit auf euren Maschinen keine Ports > geöffnet werden müssen. Der Weg ist ausgehend. --- ## 5. Metriken und Verlauf Numerische Werte aus `metrics` werden mit Zeitstempel gespeichert (Aufbewahrung 14 Tage). Verschachtelte Angaben werden flach abgelegt: `{"cpu":{"load":1.2}}` wird zu `cpu.load`. ```bash # Welche Metriken liefert dieser Monitor? curl "https://dc.mhdf.de/api/watchdog/v1/metrics?source=polytrader-worker" \ -H "Authorization: Bearer $DC_TOKEN" # Verlauf, auf 15-Minuten-Fenster verdichtet curl "https://dc.mhdf.de/api/watchdog/v1/metrics?source=polytrader-worker&metric=queue_depth&hours=24" \ -H "Authorization: Bearer $DC_TOKEN" ``` Die Antwort enthält zusätzlich `deviation`: den Vergleich des aktuellen Werts mit dem Sieben-Tage-Durchschnitt **desselben** Monitors. Damit lassen sich Auffälligkeiten erkennen, ohne für jedes Projekt Schwellwerte zu pflegen. --- ## 6. Hierarchie und Alarmunterdrückung Ist bei einem Monitor eine **übergeordnete Entität** gesetzt und fällt diese aus, werden Alarme für die untergeordneten Monitore unterdrückt. Ihr Zustand bleibt im Dashboard sichtbar. Ohne das erzeugt ein ausgefallener Hypervisor mit zwölf VMs dreizehn Meldungen für ein Problem. Gepflegt wird das im WebUI unter **WatchDog → System-Hierarchie** im Feld *Übergeordnete Entität*. Es funktioniert über mehrere Ebenen (Rack → Host → Anwendung). > Die Zuordnung erfolgt über `parent_source`, **nicht** über `group`. Die Gruppe > dient nur der Anzeige. --- ## 7. Beispiele Alle Beispiele lesen das Token aus der Umgebung. ### C# — Hintergrunddienst ```csharp using System; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; public sealed class WatchdogReporter { private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(10) }; private readonly string _url = $"{Environment.GetEnvironmentVariable("DC_URL") ?? "https://dc.mhdf.de"}/api/watchdog/v1/ping"; private readonly string _token = Environment.GetEnvironmentVariable("DC_TOKEN") ?? ""; private readonly string _source = Environment.GetEnvironmentVariable("DC_SOURCE") ?? Environment.MachineName; public async Task SendAsync(string status, string message, object? checks = null, object? metrics = null, CancellationToken ct = default) { var payload = new { source = _source, status, // ok | warning | error | stopped | maintenance interval = 60, message, os = Environment.OSVersion.ToString(), version = BuildInfo.Version, // erscheint in der Monitorliste checks, metrics }; var request = new HttpRequestMessage(HttpMethod.Post, _url) { Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") }; request.Headers.Add("Authorization", $"Bearer {_token}"); try { await Http.SendAsync(request, ct); } catch (Exception ex) { // Ein nicht erreichbarer Monitoring-Server darf die Anwendung // niemals stoppen. Console.Error.WriteLine($"[Watchdog] Heartbeat fehlgeschlagen: {ex.Message}"); } } } ``` Einbindung als `BackgroundService`: ```csharp public class HeartbeatService : BackgroundService { private readonly WatchdogReporter _reporter = new(); private readonly IMarketFeed _feed; private readonly IJobQueue _queue; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { bool feedOk = _feed.LastTickAge < TimeSpan.FromMinutes(5); await _reporter.SendAsync( status: feedOk ? "ok" : "warning", message: feedOk ? "Betrieb normal" : "Datenfeed veraltet", checks: new { market_feed = new { ok = feedOk, message = $"Letzter Tick vor {_feed.LastTickAge.TotalMinutes:F0} min" }, queue = new { ok = _queue.Depth < 1000, value = _queue.Depth } }, metrics: new { queue_depth = _queue.Depth }, ct: stoppingToken); await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken); } // Sauberes Beenden ankündigen, damit kein Fehlalarm entsteht. await _reporter.SendAsync("stopped", "Dienst planmäßig beendet", ct: CancellationToken.None); } } ``` ### Python ```python import os import requests URL = os.environ.get("DC_URL", "https://dc.mhdf.de") + "/api/watchdog/v1/ping" TOKEN = os.environ["DC_TOKEN"] SOURCE = os.environ.get("DC_SOURCE", os.uname().nodename) def heartbeat(status="ok", message="", checks=None, metrics=None): payload = { "source": SOURCE, "status": status, "interval": 60, "message": message, } if checks: payload["checks"] = checks if metrics: payload["metrics"] = metrics try: requests.post(URL, json=payload, headers={"Authorization": f"Bearer {TOKEN}"}, timeout=10) except Exception as exc: # Monitoring darf die Anwendung nie zum Stillstand bringen print(f"[Watchdog] Heartbeat fehlgeschlagen: {exc}") heartbeat( status="ok", message="Import abgeschlossen", checks={"db": {"ok": True}, "feed": {"ok": True}}, metrics={"rows_imported": 4213}, ) ``` ### Bash / Cron ```bash #!/usr/bin/env bash set -euo pipefail : "${DC_TOKEN:?DC_TOKEN ist nicht gesetzt}" URL="${DC_URL:-https://dc.mhdf.de}/api/watchdog/v1/ping" SOURCE="${DC_SOURCE:-$(hostname)}" DISK=$(df --output=pcent / | tail -1 | tr -dc '0-9') curl -fsS -X POST "$URL" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $DC_TOKEN" \ -d "{\"source\":\"$SOURCE\",\"status\":\"ok\",\"interval\":3600, \"message\":\"Nächtliches Backup abgeschlossen\", \"checks\":{\"disk\":{\"ok\":$([ "$DISK" -lt 90 ] && echo true || echo false),\"message\":\"${DISK}% belegt\"}}, \"metrics\":{\"disk_percent\":$DISK}}" \ > /dev/null ``` --- ## 8. Ereignisse protokollieren Für einmalige Vorkommnisse statt zyklischer Meldungen: ```bash curl -X POST https://dc.mhdf.de/api/watchdog/v1/event \ -H "Authorization: Bearer $DC_TOKEN" \ -H "Content-Type: application/json" \ -d '{"source":"polytrader-worker","kind":"recovered", "severity":"info","message":"Verbindung zum Feed wiederhergestellt"}' ``` Zulässige `kind`-Werte: `started`, `stopped_graceful`, `crash_suspected`, `hard_error`, `recovered`, `warning_raised`, `warning_cleared`, `maintenance_start`, `maintenance_end`, `watchdog_started`. `severity`: `info`, `warning`, `alarm`. --- ## 9. Was sich gegenüber Version 1 geändert hat | Thema | Vorher | Jetzt | |---|---|---| | Zustandsbewertung | fand nicht statt — ein ausgefallenes System blieb `up` | Evaluator per Cron, 2× / 4× Intervall | | `expected_interval_sec` | gespeichert, nie ausgewertet | bestimmt die Schwellen | | `is_muted`, `suppress_until_utc` | gespeichert, nie ausgewertet | werden beachtet | | Token | nur `wd_live_`-Tokens | zusätzlich zentrale Tokens mit `watchdog:ping` | | Anwendungszustand | nicht übermittelbar | Feld `checks` | | Metriken | nur der letzte Wert | Verlauf über 14 Tage, abrufbar | | Geplantes Beenden | nicht möglich | `status: "stopped"` bzw. `"maintenance"` | | Alarme bei Ausfall eines Hosts | eine Meldung je Kind | Kinder werden unterdrückt | | Antwortformat | uneinheitlich | `{"status":"success",…}` bzw. `{"status":"error","error":{"code":…}}` | | Laufende Version | nicht übermittelbar | Feld `version` (seit 2.1) | | OpenAPI-Beschreibung | `/event`, `/events`, `/status` fehlten, ebenso `group` und `type` am Ping | vollständig | Bestehende Agenten mit `wd_live_`-Token und einfachem `ok`-Ping laufen unverändert weiter — die neuen Felder sind alle optional. --- ## 10. Empfehlungen 1. **Intervall realistisch wählen.** Ein Backup-Job, der stündlich läuft, meldet `interval: 3600` — nicht 60. 2. **Netzwerkfehler immer abfangen.** Der Ausfall des Monitoring-Servers darf die überwachte Anwendung nicht beeinträchtigen. 3. **Beim Beenden `stopped` senden.** Sonst folgt wenige Minuten später ein Fehlalarm. 4. **`checks` nutzen.** Ein Heartbeat sagt nur, dass ein Thread läuft. 5. **Hierarchie pflegen**, wenn Dienste auf gemeinsamen Hosts laufen — sonst bringt ein Hostausfall eine Alarmlawine. 6. **`version` mitschicken.** Ohne sie lässt sich ein Ausfall nicht mit einem Rollout in Verbindung bringen. --- ## 11. Der Evaluator-Tick Der Evaluator ist die Komponente, die Monitore anhand ihres erwarteten Intervalls herabstuft. Er läuft **nicht** von selbst — ohne einen Cron-Eintrag sind die Zustände im Dashboard wertlos. Er erledigt in einem Durchlauf: - Monitore anhand `expected_interval_sec` auf `warning` bzw. `down` stufen - Zustandswechsel im Ereignisprotokoll festhalten und Webhooks auslösen - Alarme für Kinder eines ausgefallenen Hosts unterdrücken - abgelaufene Bugtracker-Leases freigeben - stündlich den Metrik-Verlauf abräumen ### Interner Aufruf (empfohlen) ```bash * * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet ``` Entspricht dem früheren `watchdog/cli/tick.php`. Kein Schlüssel im Crontab, keine Abhängigkeit von Webserver, TLS oder DNS — und damit auch dann lauffähig, wenn der Webserver gerade das Problem ist. | Option | Wirkung | |---|---| | `-q`, `--quiet` | Ausgabe nur bei Zustandswechseln und Fehlern (für Cron) | | `-j`, `--json` | Ergebnis maschinenlesbar | | `--no-lock` | Sperre gegen überlappende Läufe übergehen (nur zur Fehlersuche) | | `-h`, `--help` | Hilfe | | Rückgabewert | Bedeutung | |---|---| | `0` | Lauf erfolgreich | | `1` | Fehler — Meldung auf `stderr`, Einzelheiten im Log unter `var/log/` | | `2` | Übersprungen, es lief bereits ein Tick | Das Skript sperrt sich über `var/watchdog-tick.lock` selbst. Braucht ein Lauf länger als eine Minute, überspringt der nächste Cron-Aufruf — sonst würden zwei Evaluatoren dieselben Zustandswechsel doppelt melden. > Der Aufruf ist auf `PHP_SAPI === 'cli'` beschränkt und `cli/` ist zusätzlich > per `.htaccess` gesperrt. Über den Webserver ist das Skript also nicht > erreichbar — sonst ließe sich ein Evaluationslauf ohne jede Authentifizierung > auslösen, während der HTTP-Endpunkt dafür bewusst den Shared Key verlangt. ### Über die Schnittstelle Sinnvoll, wenn der Cron auf einer anderen Maschine läuft: ```bash * * * * * curl -fsS -H "Authorization: Bearer " https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null ``` Beide Wege rufen denselben Code auf und schreiben denselben Lauf-Vermerk in `watchdog_cron_jobs`. **Nur einen von beiden einrichten** — zwei parallele Zeitpläne bringen keinen Gewinn, nur die Gefahr überlappender Läufe. ### Prüfen, ob er läuft ```bash php cli/tick.php --json curl https://dc.mhdf.de/api/health -H "Authorization: Bearer " ``` `/api/health` meldet unter `checks.evaluator` den letzten Lauf. Im WebUI zeigt das Watchdog-Modul denselben Zustand als Abzeichen **AKTIV** / **INAKTIV**.