docs: Integrationsanleitungen auf Stand 2.0 bringen, Status "stopped" ergänzen

Watchdog-Anleitung vollständig überarbeitet
- Alle drei Codebeispiele trugen das geseedete Demo-Token fest im Quelltext.
  Es ist an die Source "srv-db-01" gebunden — wer es übernommen hätte, wäre für
  jeden anderen Dienst abgewiesen worden. Jetzt Umgebungsvariable und eine
  Anleitung, wie man ein eigenes Token erzeugt.
- Der Ratschlag "sende beim Beenden einen Ping mit Status stopped" beschrieb
  etwas, das die API nicht konnte. Statt die Anleitung an die Lücke anzupassen,
  ist die Lücke geschlossen: status akzeptiert jetzt "stopped" und
  "maintenance". Der Evaluator lässt solche Monitore in Ruhe, statt wenige
  Minuten nach jedem sauberen Shutdown einen Fehlalarm zu erzeugen.
- Neu dokumentiert: checks (Gesundheitszustand per Push, ohne offene Ports),
  metrics samt Verlauf und Abweichungsvergleich, Alarmunterdrückung über die
  Hierarchie, die Schwellen des Evaluators (2x warning, 4x down) und der
  erforderliche Cron-Job.

Lizenz-Anleitung
- Neuer Abschnitt zum Antwortformat. Die Lizenz-Endpunkte antworten bewusst
  ohne den status/error-Umschlag der übrigen API; das Feld status auf oberster
  Ebene trägt den Lizenzzustand. Genau diese Besonderheit hatte ich beim Umbau
  übersehen, weshalb sie jetzt ausdrücklich festgehalten ist — samt Tabelle
  aller Zustände.
- Ergänzt: Deaktivierung braucht den shared_key, mit Beispiel für den .NET-
  Client und curl. Verhalten bei Ratenbegrenzung.

UpdateService-Anleitung
- Prüf-Endpunkte dokumentiert (check, latest, releases) samt Antwortformat.
- Tabelle zum Versionsvergleich mit den Fällen, die vorher falsch liefen.
- Auto-Resolve beim Veröffentlichen beschrieben.

Agent-Prompt-Vorlage
- Fehler-Schnittstelle ergänzt, inklusive Hinweis auf "ignored": true, damit
  ein Agent bekannte Fehler nicht untersucht.

Alle in der Dokumentation genannten API-Pfade und Scopes wurden maschinell
gegen Routing und TokenManager abgeglichen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-08 13:49:56 +02:00
co-authored by Claude Opus 5
parent 65e60899f9
commit 5f9b0c5596
7 changed files with 548 additions and 110 deletions
+28
View File
@@ -54,6 +54,34 @@ dem nächsten Agenten das Parsen des Stacktrace.
Schweregrade: `idea` (Gedanke für später), `wishlist` (Backlog), Schweregrade: `idea` (Gedanke für später), `wishlist` (Backlog),
`low`, `medium`, `high`, `critical`. `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 ### Arbeit übernehmen
Bevor du an einem Item arbeitest, übernimm es — sonst arbeiten zwei Agenten Bevor du an einem Item arbeitest, übernimm es — sonst arbeiten zwei Agenten
parallel am selben Problem: parallel am selben Problem:
+87 -10
View File
@@ -1,16 +1,58 @@
# Deploymentcenter — Lizenzsystem Integration für KI-Agenten # 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 > **Zielgruppe**: KI-Agenten & Softwareentwickler
> **Gültig ab**: Hardware-ID v2 Specification (August 2026) > **Gültig ab**: Hardware-ID v2, Deploymentcenter 2.0
> **Plattformen**: Windows, Linux (inkl. systemd Services & Docker-Container), macOS > **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)**
---
## 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 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 ## 5. Migration v1 → v2 ohne Platzverlust
+69
View File
@@ -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 ## 5. LEMP Verzeichnisstruktur auf dem Server
```text ```text
+12
View File
@@ -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 `warning` herabgestuft. **Es müssen keine Ports geöffnet werden**, der Weg ist
ausgehend. 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 ## 12. Abhängigkeitsbewusste Alarmierung
Fällt ein Monitor aus, für den `parent_source` gesetzt ist, und ist der Fällt ein Monitor aus, für den `parent_source` gesetzt ist, und ist der
+323 -94
View File
@@ -1,162 +1,391 @@
# Deploymentcenter — Watchdog Integration für KI-Agenten # Watchdog-Integration
> **⚠️ Geändert in Version 2.0** — Neu ist der Evaluator unter Überwachung von Anwendungen, Diensten und Servern über Heartbeats.
> `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)**.
> **Stand:** Version 2.0 — vollständig überarbeitet. Wer eine ältere Integration
> **Zielgruppe**: KI-Agenten & Softwareentwickler > betreibt, findet die Änderungen in Abschnitt 8.
> **Zweck**: Einbindung von Heartbeat-Monitoring, Statusmeldungen und Telemetrie in Anwendungen & Serverdienste.
--- ---
## 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 <SHARED_KEY>" 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` **Nicht** das Beispiel-Token aus älteren Fassungen dieser Anleitung verwenden —
- **Content-Type**: `application/json` es ist ein Demo-Wert und an eine einzelne Source gebunden.
- **Header**: `X-Agent-Token: <dein_watchdog_agent_token>`
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
```
### Request Body Schema (JSON)
```json ```json
{ {
"source": "srv-db-01", "source": "polytrader-worker",
"instance": "default", "instance": "default",
"type": "heartbeat", "type": "heartbeat",
"status": "ok", "status": "ok",
"message": "Service running smoothly",
"interval": 60, "interval": 60,
"group": "Infrastructure", "message": "Verarbeite Warteschlange",
"os": "Ubuntu 24.04 LTS" "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: | Feld | Pflicht | Bedeutung |
- `source` *(string, erforderlich)*: Eindeutiger Name des Dienstes oder Hostnames (z.B. `srv-db-01` oder `PolyTrader Worker`). |---|---|---|
- `instance` *(string, optional)*: Instanzbezeichner (Standard: `default`). | `source` | ja | Eindeutiger Name des Dienstes oder Hosts |
- `type` *(string)*: `heartbeat`, `host`, `hypervisor_node` oder `guest`. | `instance` | nein | Mehrere Instanzen desselben Dienstes (Vorgabe `default`) |
- `status` *(string)*: `ok`, `warning` oder `error`. | `type` | nein | `heartbeat`, `host`, `hypervisor_node`, `guest` |
- `message` *(string, optional)*: Status- oder Fehlermeldung. | `status` | nein | siehe unten (Vorgabe `ok`) |
- `interval` *(int)*: Erwarteter Abstand in Sekunden zwischen zwei Pings (Standard: `60`). | `interval` | nein | Erwarteter Abstand in Sekunden (Vorgabe 60) |
- `group` *(string, optional)*: Gruppierung im Dashboard (z.B. `Applications`, `Infrastructure`). | `message` | nein | Kurztext, erscheint im Dashboard |
- `os` *(string, optional)*: Betriebssystem-Name (z.B. `.NET 8 Service`, `Debian 12`). | `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 ```csharp
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
public class WatchdogHeartbeatService public sealed class WatchdogReporter
{ {
private static readonly HttpClient Client = new HttpClient(); private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(10) };
private static readonly string PingUrl = "https://dc.mhdf.de/api/watchdog/v1/ping";
private static readonly string Token = "wd_live_token_infra_01_secure";
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 var payload = new
{ {
source = sourceName, source = _source,
instance = "default", status, // ok | warning | error | stopped | maintenance
type = "heartbeat",
status = status,
message = message,
interval = 60, interval = 60,
group = "Services", message,
os = Environment.OSVersion.ToString() os = Environment.OSVersion.ToString(),
checks,
metrics
}; };
string json = JsonSerializer.Serialize(payload); var request = new HttpRequestMessage(HttpMethod.Post, _url)
var request = new HttpRequestMessage(HttpMethod.Post, PingUrl)
{ {
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 try
{ {
HttpResponseMessage response = await Client.SendAsync(request); await Http.SendAsync(request, ct);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("[✔] Watchdog Heartbeat erfolgreich gesendet.");
}
} }
catch (Exception ex) 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 ```csharp
import requests 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" protected override async Task ExecuteAsync(CancellationToken stoppingToken)
AGENT_TOKEN = "wd_live_token_infra_01_secure" {
while (!stoppingToken.IsCancellationRequested)
{
bool feedOk = _feed.LastTickAge < TimeSpan.FromMinutes(5);
def send_heartbeat(source_name, status="ok", message="Python Background Task running"): await _reporter.SendAsync(
headers = { status: feedOk ? "ok" : "warning",
"Content-Type": "application/json", message: feedOk ? "Betrieb normal" : "Datenfeed veraltet",
"X-Agent-Token": AGENT_TOKEN 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 ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail
WATCHDOG_URL="https://dc.mhdf.de/api/watchdog/v1/ping" : "${DC_TOKEN:?DC_TOKEN ist nicht gesetzt}"
TOKEN="wd_live_token_infra_01_secure" URL="${DC_URL:-https://dc.mhdf.de}/api/watchdog/v1/ping"
SOURCE="$(hostname)" SOURCE="${DC_SOURCE:-$(hostname)}"
curl -s -X POST "$WATCHDOG_URL" \ DISK=$(df --output=pcent / | tail -1 | tr -dc '0-9')
-H "Content-Type: application/json" \
-H "X-Agent-Token: $TOKEN" \ curl -fsS -X POST "$URL" \
-d '{"source": "'"$SOURCE"'", "status": "ok", "message": "Hourly Backup Task Completed", "interval": 3600}' -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. Für einmalige Vorkommnisse statt zyklischer Meldungen:
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. ```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.
+11 -3
View File
@@ -311,10 +311,18 @@ $spec = [
'properties' => [ 'properties' => [
'source' => ['type' => 'string'], 'source' => ['type' => 'string'],
'instance' => ['type' => 'string', 'default' => 'default'], 'instance' => ['type' => 'string', 'default' => 'default'],
'status' => ['type' => 'string', 'enum' => ['ok', 'warning', 'error']], 'status' => [
'interval' => ['type' => 'integer', 'description' => 'Erwarteter Abstand in Sekunden; danach gilt der Monitor als auffaellig'], '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'], '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'], 'os' => ['type' => 'string'],
], ],
]]], ]]],
+19 -4
View File
@@ -102,10 +102,17 @@ final class MonitorRepo
? json_encode($checks, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ? json_encode($checks, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: null; : 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) { $state = match ($status) {
'ok' => 'up', 'ok' => 'up',
'warning' => 'warning', 'warning' => 'warning',
default => 'down', 'stopped' => 'stopped',
'maintenance' => 'maintenance',
default => 'down',
}; };
// Eine fehlgeschlagene Pruefung stuft einen als "ok" gemeldeten // Eine fehlgeschlagene Pruefung stuft einen als "ok" gemeldeten
@@ -114,6 +121,14 @@ final class MonitorRepo
$state = 'warning'; $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); $previous = $this->getMonitor($source, $instance);
$previousState = $previous !== null ? (string)$previous['state'] : null; $previousState = $previous !== null ? (string)$previous['state'] : null;
@@ -155,7 +170,7 @@ final class MonitorRepo
':type' => $type, ':type' => $type,
':state' => $state, ':state' => $state,
':interval' => $intervalSec, ':interval' => $intervalSec,
':last_status' => in_array($status, ['ok', 'warning', 'error'], true) ? $status : 'error', ':last_status' => $lastStatus,
':message' => $message, ':message' => $message,
':metrics' => $metricsJson, ':metrics' => $metricsJson,
':health' => $healthJson, ':health' => $healthJson,