Files
Deploymentcenter/docs/WATCHDOG_INTEGRATION_GUIDE.md
Deploymentcenter BotandClaude Opus 5 2388b5abe1 feat(updateservice): Plattform-Dimension, signierte Releases, Update mit Rollback
Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen
verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im
Fehlerfall halb aktualisierte Installationen hinterliessen.

Server
- Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das
  zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version,
  weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System
  zog sich damit das Windows-Paket.
- Aufloesungsregel: je Version das plattformgenaue Paket, sonst das
  plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich
  'any' - lieber kein Update als das falsche.
- manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer,
  wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war.
- Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt
  /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf
  fremden Systemen und darf den Signierschluessel nicht besitzen.

Packager
- Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der
  bestehenden latest.json fehl, ersetzte ein leeres catch die komplette
  Historie durch einen einzigen Eintrag - ohne jede Meldung.
- Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der
  mitgelieferten Beispielkonfiguration nie zu.
- preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am
  Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten
  ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems.
- Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen.
- Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer
  Endlosschleife: Clients aktualisieren, melden weiter die alte Version,
  halten das Release erneut fuer neu.
- --platform mit Ableitung aus dem Publish-Pfad.

Agent
- Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als
  "Atomic Replace with Backup" kommentiert und war eine Kopierschleife.
- Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der
  Vorversion. Was nicht aus einem Release stammt, bleibt liegen.
- Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben.
- API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb
  die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte.
- Installierte Version aus --current-version oder manifest.json statt des
  Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen
  liess. Reparatur funktioniert damit auch ohne manifest.json.
- Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden.

SDK
- ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes
  "update-agent.exe" wird unter Linux nie gefunden.
- LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die
  Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit
  dem Herunterfahren) und --platform.

Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz-
Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen
sich nicht getrennt stagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 19:56:35 +02:00

17 KiB
Raw Permalink Blame History

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.

* * * * * /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.


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 <TOKEN>          (oder X-Agent-Token: <TOKEN>)
Content-Type: application/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.

// 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:

"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:

{ "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.

# 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

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:

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

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

#!/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:

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)

* * * * * /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:

* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" 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

php cli/tick.php --json
curl https://dc.mhdf.de/api/health -H "Authorization: Bearer <SHARED_KEY>"

/api/health meldet unter checks.evaluator den letzten Lauf. Im WebUI zeigt das Watchdog-Modul denselben Zustand als Abzeichen AKTIV / INAKTIV.