diff --git a/docs/AGENT_PROMPT_TEMPLATE.md b/docs/AGENT_PROMPT_TEMPLATE.md index 8809936..e91425b 100644 --- a/docs/AGENT_PROMPT_TEMPLATE.md +++ b/docs/AGENT_PROMPT_TEMPLATE.md @@ -54,6 +54,34 @@ dem nächsten Agenten das Parsen des Stacktrace. Schweregrade: `idea` (Gedanke für später), `wishlist` (Backlog), `low`, `medium`, `high`, `critical`. +### Laufzeitfehler automatisch melden +Für den globalen Exception-Handler einer Anwendung gibt es einen schlankeren +Eingang — Titel und Dringlichkeit leitet der Server ab: + +`POST /api/errors/v1/report` + +```json +{ + "project_slug": "myapp", + "exception": "PDOException", + "message": "Exakte Fehlermeldung", + "stack_trace": "...", + "level": "error", + "build": "v1.4.2", + "environment": "production", + "file": "src/Core/Service.php", + "line": 42 +} +``` + +`level`: `fatal` (Prozess beendet), `error` (Vorgang fehlgeschlagen, Programm +läuft weiter), `warning`. + +Gleiche Fehler werden zu einer Gruppe zusammengefasst und hochgezählt; +veränderliche Anteile wie Schlüsselwerte, Adressen und Zeitstempel werden dabei +ausgeblendet. Kommt `"ignored": true` zurück, ist der Fehler als bekannt und +harmlos eingestuft — dann nicht weiter untersuchen, sondern nur zählen lassen. + ### Arbeit übernehmen Bevor du an einem Item arbeitest, übernimm es — sonst arbeiten zwei Agenten parallel am selben Problem: diff --git a/docs/LICENSE_INTEGRATION_GUIDE.md b/docs/LICENSE_INTEGRATION_GUIDE.md index 0ff4881..be7e0a3 100644 --- a/docs/LICENSE_INTEGRATION_GUIDE.md +++ b/docs/LICENSE_INTEGRATION_GUIDE.md @@ -1,16 +1,58 @@ # Deploymentcenter — Lizenzsystem Integration für KI-Agenten -> **⚠️ Geändert in Version 2.0** — `/api/license/v1/validate` bleibt unverändert -> und ohne Token erreichbar. `/api/license/v1/deactivate` verlangt weiterhin -> Authentifizierung, allerdings mit dem **neu erzeugten** `shared_key`: der alte -> Wert lag im Repository und wurde ersetzt. Die Signatur der Offline-Lizenzdateien -> ist jetzt ein ehrlich benanntes HMAC-SHA256 statt der irreführenden Bezeichnung -> `ED25519_SIG_`. Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**. +> **Zielgruppe**: KI-Agenten & Softwareentwickler +> **Gültig ab**: Hardware-ID v2, Deploymentcenter 2.0 +> **Plattformen**: Windows, Linux (inkl. systemd und Docker), macOS +> **⚠️ Änderungen in Version 2.0** +> - `/api/license/v1/validate` bleibt **unverändert** und ohne Token erreichbar. +> Ausgelieferte Clients laufen ohne Anpassung weiter. +> - `/api/license/v1/deactivate` verlangt weiterhin Authentifizierung — aber mit +> dem **neu erzeugten** `shared_key`. Der alte Wert lag im Repository und wurde +> ersetzt; wer ihn irgendwo eingetragen hat, muss nachziehen. +> - Die Signatur der Offline-Lizenzdateien heißt jetzt ehrlich HMAC-SHA256 statt +> irreführend `ED25519_SIG_`. Der frühere „Server Public Key" im WebUI war frei +> erfunden und wurde entfernt. +> +> Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)** -> **Zielgruppe**: KI-Agenten & Softwareentwickler -> **Gültig ab**: Hardware-ID v2 Specification (August 2026) -> **Plattformen**: Windows, Linux (inkl. systemd Services & Docker-Container), macOS +--- + +## 0. Antwortformat — bitte beachten + +Die Lizenz-Endpunkte antworten **ohne** den `status`/`error`-Umschlag der +übrigen Deploymentcenter-API. Das Feld `status` auf oberster Ebene trägt den +**Lizenzzustand**: + +```json +{ + "type": "validation_result", + "status": "valid", + "issued_at": 1786435199, + "expires_at": 1817971199, + "cache_ttl_hours": 168, + "hardware_id": "2:win:a765bd47...", + "license_key": "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", + "nonce": "der übergebene Wert", + "endpoints": { "validate": "/api/license/v1/validate", + "deactivate": "/api/license/v1/deactivate" }, + "message": "License is valid" +} +``` + +| `status` | Bedeutung | +|---|---| +| `valid` | Lizenz gültig, Aktivierung eingetragen | +| `not_found` | Projekt oder Schlüssel unbekannt | +| `revoked` | Lizenz widerrufen **oder diese Hardware gesperrt** | +| `suspended` | Lizenz vorübergehend ausgesetzt | +| `expired` | Ablaufdatum überschritten | +| `activation_limit` | Maximale Anzahl Aktivierungen erreicht | + +> Diese Sonderstellung ist bewusst: Ein Umschlag mit `status: "success"` würde +> von jedem bestehenden Client als „nicht valid" gelesen — sämtliche Lizenzen +> gälten schlagartig als ungültig. Wer eine eigene Anbindung schreibt, muss +> `status` also **von der obersten Ebene** lesen, nicht aus einem `result`-Objekt. --- @@ -123,6 +165,41 @@ my-service --license-set-key LLAB1-98A72-B3C4D-5E6F7-89012 my-service --license-deactivate ``` +### Deaktivierung braucht Authentifizierung + +`/api/license/v1/deactivate` gibt einen Aktivierungsplatz frei und ist deshalb +geschützt — sonst könnte jeder fremde Installationen abmelden. Der Aufruf +verlangt den `shared_key` aus `config/config.php`: + +```csharp +// Der Schlüssel gehört auf den Administrationsrechner, nicht in die +// ausgelieferte Anwendung. +var client = new LicenseClient(); +bool released = await client.DeactivateAsync( + productSlug: "myapp", + licenseKey: "LLAB1-98A72-B3C4D-5E6F7-89012", + serverBaseUrl: "https://dc.mhdf.de", + authToken: Environment.GetEnvironmentVariable("DC_SHARED_KEY")); +``` + +```bash +curl -X POST https://dc.mhdf.de/api/license/v1/deactivate \ + -H "Authorization: Bearer $DC_SHARED_KEY" \ + -H "Content-Type: application/json" \ + -d '{"product":"myapp","license_key":"XXXXX-...","hardware_id":"2:win:a765..."}' +``` + +Alternativ genügt für Einzelfälle der Knopf **Freigeben** in der Hardware-Liste +des WebUI — das ist der übliche Weg und braucht keinen Schlüssel im Feld. + +### Was passiert bei Ratenbegrenzung + +`/validate` ist auf 120 Anfragen pro Minute und IP begrenzt. Darüber kommt +`429` mit `{"status":"error","error":{"code":"rate_limited"}}` — hier greift +ausnahmsweise das Umschlagformat, weil die Drosselung vor der Lizenzlogik +zuschlägt. Ein Client sollte in dem Fall den lokalen Cache verwenden und es +später erneut versuchen, statt die Anwendung zu blockieren. + --- ## 5. Migration v1 → v2 ohne Platzverlust diff --git a/docs/UPDATESERVICE_INTEGRATION_GUIDE.md b/docs/UPDATESERVICE_INTEGRATION_GUIDE.md index bedf1c8..f06c49d 100644 --- a/docs/UPDATESERVICE_INTEGRATION_GUIDE.md +++ b/docs/UPDATESERVICE_INTEGRATION_GUIDE.md @@ -159,6 +159,75 @@ update-agent --project myapp --channel prod --action list --- +## 4a. Prüf-Endpunkte (für eigene Anbindungen) + +Die Lese-Endpunkte sind bewusst **ohne Token** erreichbar, damit ausgelieferte +Anwendungen ohne Anpassung weiter nach Updates suchen können. Sie liefern nur +Release-Metadaten, die über die Download-URL ohnehin öffentlich sind. + +```bash +GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod +``` + +```json +{ + "status": "success", + "update_available": true, + "current_version": "1.4.2", + "latest_version": "1.4.3", + "is_critical": false, + "latest_release": { + "version": "1.4.3", + "download_url": "https://dc.mhdf.de/releases/myapp/prod/1.4.3/package.tar.gz", + "sha256_hash": "e3b0c442...", + "git_commit": "a21536f", + "size_bytes": 8412160, + "release_notes": "Behebt den Login-Fehler.", + "is_critical": 0 + } +} +``` + +Weitere Endpunkte: + +| Aufruf | Zweck | +|---|---| +| `GET .../latest?product=myapp&channel=prod` | Höchstes Release, unabhängig von der Client-Version | +| `GET .../releases?product=myapp` | Alle Releases, nach Versionsordnung sortiert | + +### Versionsvergleich + +Der Vergleich folgt der semantischen Versionsordnung. Konkret bedeutet das: + +| Installiert | Verfügbar | Update? | +|---|---|---| +| `1.9.0` | `1.10.0` | ja — zweistellige Minor ist höher | +| `1.10.0` | `1.9.0` | nein | +| `1.0.0-rc.1` | `1.0.0` | ja — Release schlägt Vorabversion | +| `1.0.0` | `1.0.0-rc.1` | nein | +| `v1.4.2` | `v1.4.3` | ja — führendes `v` wird ignoriert | + +> Zuvor verglich der Server lexikografisch. `1.9.0` galt dadurch als neuer als +> `1.10.0`, und Clients bekamen ein Downgrade als Update angeboten. Derselbe +> Fehler steckte im .NET-Client bei `v`-präfigierten Versionen und ist dort +> ebenfalls behoben. + +### Verknüpfung mit dem Bugtracker + +Beim Veröffentlichen schließen sich alle Bugtracker-Items, deren +`resolved_in_build` der veröffentlichten Version entspricht, automatisch. Die +Antwort nennt die Anzahl: + +```json +{ "status": "success", "release_id": 12, "created": true, "auto_resolved": 3, + "message": "Release 1.4.3 (prod) für \"myapp\" veröffentlicht. 3 Bugtracker-Item(s) automatisch geschlossen." } +``` + +Damit schließt sich der Kreis: Ein Agent markiert einen Bug als „gelöst in +v1.4.3", der Packager veröffentlicht v1.4.3, und das Item schließt sich selbst. + +--- + ## 5. LEMP Verzeichnisstruktur auf dem Server ```text diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index fa5d623..acad4ad 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -232,6 +232,18 @@ Das Deploymentcenter interpretiert die Namen nicht — es liest nur `ok` und `warning` herabgestuft. **Es müssen keine Ports geöffnet werden**, der Weg ist ausgehend. +Neu sind außerdem die Statuswerte `stopped` und `maintenance`. Ohne sie erzeugte +jedes geplante Herunterfahren wenige Minuten später einen Fehlalarm — es gab +schlicht keinen Weg, ein beabsichtigtes Ende mitzuteilen: + +```json +{ "source": "polytrader-worker", "status": "stopped", + "message": "Dienst planmäßig beendet" } +``` + +Der Evaluator lässt solche Monitore in Ruhe, bis wieder ein normaler Heartbeat +eintrifft. + ## 12. Abhängigkeitsbewusste Alarmierung Fällt ein Monitor aus, für den `parent_source` gesetzt ist, und ist der diff --git a/docs/WATCHDOG_INTEGRATION_GUIDE.md b/docs/WATCHDOG_INTEGRATION_GUIDE.md index 63082bb..837fb03 100644 --- a/docs/WATCHDOG_INTEGRATION_GUIDE.md +++ b/docs/WATCHDOG_INTEGRATION_GUIDE.md @@ -1,162 +1,391 @@ -# Deploymentcenter — Watchdog Integration für KI-Agenten +# Watchdog-Integration -> **⚠️ Geändert in Version 2.0** — Neu ist der Evaluator unter -> `GET /api/watchdog/v1/evaluate`, der per Cron minütlich laufen muss. Ohne ihn -> ändert sich der Zustand eines Monitors nur beim Eintreffen eines Heartbeats, -> ein ausgefallenes System bliebe dauerhaft `up`. `expected_interval_sec`, -> `is_muted` und `suppress_until_utc` werden jetzt ausgewertet. Neben den -> bisherigen `wd_live_`-Tokens werden auch zentrale Tokens mit dem Scope -> `watchdog:ping` akzeptiert. Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**. +Überwachung von Anwendungen, Diensten und Servern über Heartbeats. - -> **Zielgruppe**: KI-Agenten & Softwareentwickler -> **Zweck**: Einbindung von Heartbeat-Monitoring, Statusmeldungen und Telemetrie in Anwendungen & Serverdienste. +> **Stand:** Version 2.0 — vollständig überarbeitet. Wer eine ältere Integration +> betreibt, findet die Änderungen in Abschnitt 8. --- -## 1. Übersicht +## 1. Wie der Zustand ermittelt wird -Der **Watchdog** in Deploymentcenter überwacht kontinuierlich den Zustand von Hosts, Diensten, Cronjobs und Proxmox-Hypervisoren. +Eine Anwendung meldet sich in festen Abständen. Bleibt die Meldung aus, stuft +der **Evaluator** den Monitor herab: -Anwendungen senden in regelmäßigen Abständen (standardmäßig alle 60 Sekunden) einen HTTP POST Ping an die Watchdog API. Ausbleibende Pings oder gemeldete Fehler erzeugen automatisch Warnungen im Admin-Dashboard. +| 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 +> * * * * * curl -fsS -H "Authorization: Bearer " https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null +> ``` +> +> Fehlt der Job, zeigt das WebUI oben einen Warnhinweis. --- -## 2. API Endpunkt & Authentifizierung +## 2. Token besorgen -- **URL**: `POST https://dc.mhdf.de/api/watchdog/v1/ping` -- **Content-Type**: `application/json` -- **Header**: `X-Agent-Token: ` +**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 +``` -### Request Body Schema (JSON) ```json { - "source": "srv-db-01", + "source": "polytrader-worker", "instance": "default", - "type": "heartbeat", - "status": "ok", - "message": "Service running smoothly", + "type": "heartbeat", + "status": "ok", "interval": 60, - "group": "Infrastructure", - "os": "Ubuntu 24.04 LTS" + "message": "Verarbeite Warteschlange", + "group": "Applications", + "os": ".NET 8 Service", + + "checks": { "db": { "ok": true }, + "market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" } }, + "metrics": { "cpu": 18, "ram": 42, "queue_depth": 23 } } ``` -#### Felder: -- `source` *(string, erforderlich)*: Eindeutiger Name des Dienstes oder Hostnames (z.B. `srv-db-01` oder `PolyTrader Worker`). -- `instance` *(string, optional)*: Instanzbezeichner (Standard: `default`). -- `type` *(string)*: `heartbeat`, `host`, `hypervisor_node` oder `guest`. -- `status` *(string)*: `ok`, `warning` oder `error`. -- `message` *(string, optional)*: Status- oder Fehlermeldung. -- `interval` *(int)*: Erwarteter Abstand in Sekunden zwischen zwei Pings (Standard: `60`). -- `group` *(string, optional)*: Gruppierung im Dashboard (z.B. `Applications`, `Infrastructure`). -- `os` *(string, optional)*: Betriebssystem-Name (z.B. `.NET 8 Service`, `Debian 12`). +| 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 | +| `checks` | nein | Selbst ermittelter Gesundheitszustand, siehe 4. | +| `metrics` | nein | Numerische Werte, siehe 5. | + +### 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. --- -## 3. Implementierungsbeispiele +## 4. Gesundheitszustand mitsenden -### 3.1 C# (.NET Core / .NET 8+) +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 class WatchdogHeartbeatService +public sealed class WatchdogReporter { - private static readonly HttpClient Client = new HttpClient(); - private static readonly string PingUrl = "https://dc.mhdf.de/api/watchdog/v1/ping"; - private static readonly string Token = "wd_live_token_infra_01_secure"; + private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(10) }; - public static async Task SendPingAsync(string sourceName, string status = "ok", string message = "Service active") + 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 = sourceName, - instance = "default", - type = "heartbeat", - status = status, - message = message, + source = _source, + status, // ok | warning | error | stopped | maintenance interval = 60, - group = "Services", - os = Environment.OSVersion.ToString() + message, + os = Environment.OSVersion.ToString(), + checks, + metrics }; - string json = JsonSerializer.Serialize(payload); - var request = new HttpRequestMessage(HttpMethod.Post, PingUrl) + var request = new HttpRequestMessage(HttpMethod.Post, _url) { - Content = new StringContent(json, Encoding.UTF8, "application/json") + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") }; - request.Headers.Add("X-Agent-Token", Token); + request.Headers.Add("Authorization", $"Bearer {_token}"); try { - HttpResponseMessage response = await Client.SendAsync(request); - if (response.IsSuccessStatusCode) - { - Console.WriteLine("[✔] Watchdog Heartbeat erfolgreich gesendet."); - } + await Http.SendAsync(request, ct); } catch (Exception ex) { - Console.WriteLine($"[✖] Watchdog Ping Fehlgeschlagen: {ex.Message}"); + // Ein nicht erreichbarer Monitoring-Server darf die Anwendung + // niemals stoppen. + Console.Error.WriteLine($"[Watchdog] Heartbeat fehlgeschlagen: {ex.Message}"); } } } ``` -### 3.2 Python 3 +Einbindung als `BackgroundService`: -```python -import requests +```csharp +public class HeartbeatService : BackgroundService +{ + private readonly WatchdogReporter _reporter = new(); + private readonly IMarketFeed _feed; + private readonly IJobQueue _queue; -WATCHDOG_URL = "https://dc.mhdf.de/api/watchdog/v1/ping" -AGENT_TOKEN = "wd_live_token_infra_01_secure" + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + bool feedOk = _feed.LastTickAge < TimeSpan.FromMinutes(5); -def send_heartbeat(source_name, status="ok", message="Python Background Task running"): - headers = { - "Content-Type": "application/json", - "X-Agent-Token": AGENT_TOKEN + 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); } - payload = { - "source": source_name, - "instance": "default", - "type": "heartbeat", - "status": status, - "message": message, - "interval": 60, - "group": "Python Services" - } - try: - response = requests.post(WATCHDOG_URL, json=payload, headers=headers, timeout=10) - if response.status_code == 200: - print("[✔] Watchdog Ping OK") - except Exception as e: - print(f"[✖] Watchdog Ping Error: {e}") +} ``` -### 3.3 Bash / Cronjob (Linux) +### 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 -WATCHDOG_URL="https://dc.mhdf.de/api/watchdog/v1/ping" -TOKEN="wd_live_token_infra_01_secure" -SOURCE="$(hostname)" +: "${DC_TOKEN:?DC_TOKEN ist nicht gesetzt}" +URL="${DC_URL:-https://dc.mhdf.de}/api/watchdog/v1/ping" +SOURCE="${DC_SOURCE:-$(hostname)}" -curl -s -X POST "$WATCHDOG_URL" \ - -H "Content-Type: application/json" \ - -H "X-Agent-Token: $TOKEN" \ - -d '{"source": "'"$SOURCE"'", "status": "ok", "message": "Hourly Backup Task Completed", "interval": 3600}' +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 ``` --- -## 4. Best Practices für KI-Agenten +## 8. Ereignisse protokollieren -1. **Heartbeat-Schleife**: Lasse in eigenständigen Hoster-Diensten einen periodischen Timer (z.B. `System.Threading.Timer` oder `BackgroundService`) alle 60s `SendPingAsync` aufrufen. -2. **Graceful Shutdown**: Sende beim Beenden des Dienstes einen Ping mit Status `stopped` oder `maintenance`. -3. **Fehlerbehandlung**: Fange Netzwerkfehler bei Watchdog-Pings stets stumm/abgefangen ab, damit der Ausfall des Monitoring-Servers niemals den Hauptanwendungsfluss unterbricht. +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":…}}` | + +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. diff --git a/public/api/openapi.php b/public/api/openapi.php index 524fd6c..afe5eac 100644 --- a/public/api/openapi.php +++ b/public/api/openapi.php @@ -311,10 +311,18 @@ $spec = [ 'properties' => [ 'source' => ['type' => 'string'], 'instance' => ['type' => 'string', 'default' => 'default'], - 'status' => ['type' => 'string', 'enum' => ['ok', 'warning', 'error']], - 'interval' => ['type' => 'integer', 'description' => 'Erwarteter Abstand in Sekunden; danach gilt der Monitor als auffaellig'], + 'status' => [ + 'type' => 'string', + 'enum' => ['ok', 'warning', 'error', 'stopped', 'maintenance'], + 'description' => 'stopped und maintenance sind angekuendigte Zustaende - der Evaluator meldet dafuer keinen Ausfall.', + ], + 'interval' => ['type' => 'integer', 'description' => 'Erwarteter Abstand in Sekunden. Nach dem Doppelten gilt der Monitor als auffaellig, nach dem Vierfachen als ausgefallen.'], 'message' => ['type' => 'string'], - 'metrics' => ['type' => 'object'], + 'checks' => [ + 'type' => 'object', + 'description' => 'Selbst ermittelter Gesundheitszustand, z. B. {"db":{"ok":true},"feed":{"ok":false,"message":"..."}}. Die Namen werden nicht interpretiert; eine fehlgeschlagene Pruefung stuft einen als ok gemeldeten Heartbeat auf warning herab.', + ], + 'metrics' => ['type' => 'object', 'description' => 'Numerische Werte; landen im Verlauf und sind ueber /metrics abrufbar.'], 'os' => ['type' => 'string'], ], ]]], diff --git a/src/Modules/Watchdog/MonitorRepo.php b/src/Modules/Watchdog/MonitorRepo.php index 8e2d4c5..8a07f37 100644 --- a/src/Modules/Watchdog/MonitorRepo.php +++ b/src/Modules/Watchdog/MonitorRepo.php @@ -102,10 +102,17 @@ final class MonitorRepo ? json_encode($checks, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null; + // "stopped" und "maintenance" sind angekuendigte Zustaende: der Dienst + // wurde bewusst beendet bzw. gewartet. Der Evaluator laesst solche + // Monitore in Ruhe, statt sie als Ausfall zu melden. Ohne diese beiden + // Werte gab es keinen Weg, ein geplantes Herunterfahren mitzuteilen - + // jeder saubere Shutdown erzeugte kurz darauf einen Fehlalarm. $state = match ($status) { - 'ok' => 'up', - 'warning' => 'warning', - default => 'down', + 'ok' => 'up', + 'warning' => 'warning', + 'stopped' => 'stopped', + 'maintenance' => 'maintenance', + default => 'down', }; // Eine fehlgeschlagene Pruefung stuft einen als "ok" gemeldeten @@ -114,6 +121,14 @@ final class MonitorRepo $state = 'warning'; } + // last_status kennt nur ok/warning/error; angekuendigte Zustaende + // gelten dort als unauffaellig. + $lastStatus = match ($status) { + 'ok', 'stopped', 'maintenance' => 'ok', + 'warning' => 'warning', + default => 'error', + }; + $previous = $this->getMonitor($source, $instance); $previousState = $previous !== null ? (string)$previous['state'] : null; @@ -155,7 +170,7 @@ final class MonitorRepo ':type' => $type, ':state' => $state, ':interval' => $intervalSec, - ':last_status' => in_array($status, ['ok', 'warning', 'error'], true) ? $status : 'error', + ':last_status' => $lastStatus, ':message' => $message, ':metrics' => $metricsJson, ':health' => $healthJson,