175 lines
6.5 KiB
C#
175 lines
6.5 KiB
C#
using System.Text.Json;
|
|
|
|
namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;
|
|
|
|
/// <summary>Was der Server zu einem Heartbeat zurückmeldet.</summary>
|
|
/// <param name="State">Der daraus abgeleitete Monitor-Zustand (<c>up</c>, <c>warning</c>, …).</param>
|
|
/// <param name="FailingChecks">Welche der mitgeschickten Prüfungen fehlgeschlagen sind.</param>
|
|
public sealed record WatchdogPingResult(string State, IReadOnlyList<string> FailingChecks);
|
|
|
|
/// <summary>
|
|
/// Sendet Heartbeats und Ereignisse an das Watchdog-Modul des Deploymentcenters.
|
|
/// </summary>
|
|
public interface IWatchdogClient
|
|
{
|
|
Task<WatchdogPingResult> SendHeartbeatAsync(
|
|
InstanceHealth health, int intervalSeconds, CancellationToken ct);
|
|
|
|
Task SendEventAsync(
|
|
string kind, string severity, string? message, object? meta, CancellationToken ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Watchdog-Anbindung: <c>POST /api/watchdog/v1/ping</c> und
|
|
/// <c>POST /api/watchdog/v1/event</c>.
|
|
///
|
|
/// <para><b>Ein Monitor je Instanz.</b> Der Schlüssel des Monitors ist das Paar
|
|
/// <c>source</c> + <c>instance</c> (so das Datenbankschema:
|
|
/// <c>UNIQUE KEY uq_monitor (source, instance)</c>). Alle ClawdDotNet-Instanzen melden
|
|
/// unter derselben <c>source</c> und tragen ihre eigene <c>instance</c> — damit ist jede
|
|
/// laufende Instanz ein eigener Monitor mit eigenem Zustand, eigenem Intervall und
|
|
/// eigenem Metrik-Verlauf. Stürzt eine von dreien ab, fällt genau deren Monitor.</para>
|
|
///
|
|
/// <para>Der Monitor entsteht beim ersten Heartbeat von selbst (<c>INSERT … ON DUPLICATE
|
|
/// KEY UPDATE</c>) — eine Registrierung vorab gibt es nicht mehr und ist auch nicht
|
|
/// nötig.</para>
|
|
/// </summary>
|
|
public sealed class WatchdogClient : IWatchdogClient, IDisposable
|
|
{
|
|
private readonly DeploymentcenterApi _api;
|
|
private readonly bool _ownsApi;
|
|
private readonly string _source;
|
|
private readonly string _instance;
|
|
private readonly string _group;
|
|
private readonly string _os;
|
|
private readonly string _version;
|
|
|
|
public WatchdogClient(
|
|
DeploymentcenterApi api, string source, string instance, string group, string os,
|
|
string version, bool ownsApi = false)
|
|
{
|
|
_api = api;
|
|
_ownsApi = ownsApi;
|
|
_source = source;
|
|
_instance = instance;
|
|
_group = group;
|
|
_os = os;
|
|
_version = version;
|
|
}
|
|
|
|
public static WatchdogClient Create(
|
|
string baseUrl, string token, string source, string instance, string group, string os,
|
|
string version, HttpClient? httpClient = null)
|
|
=> new(new DeploymentcenterApi(baseUrl, token, httpClient),
|
|
source, instance, group, os, version, ownsApi: true);
|
|
|
|
public async Task<WatchdogPingResult> SendHeartbeatAsync(
|
|
InstanceHealth health, int intervalSeconds, CancellationToken ct)
|
|
{
|
|
var payload = new Dictionary<string, object?>
|
|
{
|
|
["source"] = _source,
|
|
["instance"] = _instance,
|
|
["type"] = "heartbeat",
|
|
["status"] = health.Status,
|
|
["interval"] = intervalSeconds,
|
|
["message"] = health.Message,
|
|
["group"] = _group,
|
|
["os"] = _os,
|
|
|
|
// Landet in watchdog_monitors.app_version. Damit steht im Dashboard, welche
|
|
// Fassung eine Instanz gerade fährt — bei mehreren Instanzen der
|
|
// Unterschied zwischen „läuft" und „läuft noch auf der alten Version".
|
|
["version"] = _version
|
|
};
|
|
|
|
// Leere Objekte weglassen: Der Server übernimmt health_json nur, wenn etwas
|
|
// mitkommt — ein leeres würde den letzten bekannten Zustand nicht ersetzen,
|
|
// aber unnötig Platz im Protokoll kosten.
|
|
if (health.Checks.Count > 0)
|
|
{
|
|
payload["checks"] = health.Checks.ToDictionary(
|
|
c => c.Key,
|
|
c => (object)new { ok = c.Value.Ok, message = c.Value.Message });
|
|
}
|
|
|
|
if (health.Metrics.Count > 0)
|
|
payload["metrics"] = health.Metrics;
|
|
|
|
var response = await _api.PostAsync("/api/watchdog/v1/ping", payload, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
return ReadPingResult(response);
|
|
}
|
|
|
|
private static WatchdogPingResult ReadPingResult(JsonElement response)
|
|
{
|
|
if (!response.TryGetProperty("monitor", out var monitor)
|
|
|| monitor.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return new WatchdogPingResult("unknown", []);
|
|
}
|
|
|
|
var state = monitor.TryGetProperty("state", out var s) && s.ValueKind == JsonValueKind.String
|
|
? s.GetString() ?? "unknown"
|
|
: "unknown";
|
|
|
|
var failing = new List<string>();
|
|
if (monitor.TryGetProperty("failing_checks", out var checks)
|
|
&& checks.ValueKind == JsonValueKind.Array)
|
|
{
|
|
failing.AddRange(checks.EnumerateArray()
|
|
.Where(e => e.ValueKind == JsonValueKind.String)
|
|
.Select(e => e.GetString()!));
|
|
}
|
|
|
|
return new WatchdogPingResult(state, failing);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ein einmaliges Vorkommnis statt einer zyklischen Meldung. Zulässige
|
|
/// <paramref name="kind"/>-Werte siehe <see cref="WatchdogEventKind"/> — der Server
|
|
/// weist andere ab.
|
|
/// </summary>
|
|
public async Task SendEventAsync(
|
|
string kind, string severity, string? message, object? meta, CancellationToken ct)
|
|
{
|
|
var payload = new
|
|
{
|
|
source = _source,
|
|
instance = _instance,
|
|
kind,
|
|
severity,
|
|
message,
|
|
meta
|
|
};
|
|
|
|
await _api.PostAsync("/api/watchdog/v1/event", payload, ct).ConfigureAwait(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_ownsApi)
|
|
_api.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Die vom Server akzeptierten Ereignisarten. Die frühere Anbindung schickte
|
|
/// <c>start</c> und <c>stop</c> — beide stehen nicht auf dieser Liste und wurden
|
|
/// stillschweigend als <c>started</c> abgelegt.
|
|
/// </summary>
|
|
public static class WatchdogEventKind
|
|
{
|
|
public const string Started = "started";
|
|
public const string StoppedGraceful = "stopped_graceful";
|
|
public const string CrashSuspected = "crash_suspected";
|
|
public const string HardError = "hard_error";
|
|
public const string Recovered = "recovered";
|
|
public const string WarningRaised = "warning_raised";
|
|
public const string WarningCleared = "warning_cleared";
|
|
public const string MaintenanceStart = "maintenance_start";
|
|
public const string MaintenanceEnd = "maintenance_end";
|
|
public const string WatchdogStarted = "watchdog_started";
|
|
}
|