feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;
|
||||
|
||||
/// <summary>
|
||||
/// Die Statuswerte, die der Watchdog kennt. <c>stopped</c> und <c>maintenance</c> sind
|
||||
/// angekündigte Zustände — der Evaluator lässt solche Monitore in Ruhe, statt wenige
|
||||
/// Minuten nach einem geplanten Herunterfahren einen Fehlalarm zu erzeugen.
|
||||
/// </summary>
|
||||
public static class WatchdogStatus
|
||||
{
|
||||
public const string Ok = "ok";
|
||||
public const string Warning = "warning";
|
||||
public const string Error = "error";
|
||||
public const string Stopped = "stopped";
|
||||
public const string Maintenance = "maintenance";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eine selbst ermittelte Teilprüfung. Das Deploymentcenter interpretiert den Namen
|
||||
/// nicht — es liest nur <see cref="Ok"/> und <see cref="Message"/>. Was „gesund"
|
||||
/// bedeutet, entscheidet damit jede Anwendung selbst.
|
||||
///
|
||||
/// <para>Schlägt eine Prüfung fehl, stuft der Server einen als <c>ok</c> gemeldeten
|
||||
/// Heartbeat auf <c>warning</c> herab. Das ist der Unterschied zwischen „ein Faden
|
||||
/// läuft" und „die Anwendung tut, was sie soll".</para>
|
||||
/// </summary>
|
||||
public sealed record HealthCheck(bool Ok, string? Message = null);
|
||||
|
||||
/// <summary>Momentaufnahme des Instanz-Zustands für einen Heartbeat.</summary>
|
||||
/// <param name="Status">Einer der Werte aus <see cref="WatchdogStatus"/>.</param>
|
||||
/// <param name="Message">Kurzbegründung, erscheint im Dashboard.</param>
|
||||
/// <param name="Metrics">
|
||||
/// Nur Zahlen: Das Deploymentcenter legt sie mit Zeitstempel ab (14 Tage) und vergleicht
|
||||
/// den aktuellen Wert mit dem Sieben-Tage-Schnitt desselben Monitors. Nicht-numerische
|
||||
/// Werte würden dabei stillschweigend verworfen — beschreibende Angaben gehören
|
||||
/// deshalb in <paramref name="Message"/> oder in die Checks.
|
||||
/// </param>
|
||||
/// <param name="Checks">Selbst ermittelter Gesundheitszustand je Teilbereich.</param>
|
||||
public sealed record InstanceHealth(
|
||||
string Status,
|
||||
string? Message,
|
||||
IReadOnlyDictionary<string, double> Metrics,
|
||||
IReadOnlyDictionary<string, HealthCheck> Checks)
|
||||
{
|
||||
public static InstanceHealth Ok(string? message = null) => new(
|
||||
WatchdogStatus.Ok, message,
|
||||
new Dictionary<string, double>(),
|
||||
new Dictionary<string, HealthCheck>());
|
||||
}
|
||||
|
||||
/// <summary>Liefert vor jedem Heartbeat den aktuellen Instanz-Zustand.</summary>
|
||||
public interface IInstanceHealthProvider
|
||||
{
|
||||
Task<InstanceHealth> GetAsync(CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using ClawdDotNet.Core.Accounting;
|
||||
using ClawdDotNet.Core.Config;
|
||||
|
||||
namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;
|
||||
|
||||
/// <summary>
|
||||
/// Leitet den Instanz-Zustand für den Heartbeat ab.
|
||||
///
|
||||
/// <para>Ein Heartbeat allein beweist nur, dass ein Faden läuft. Deshalb geht der
|
||||
/// selbst ermittelte Gesundheitszustand als <c>checks</c> mit — der Server stuft einen
|
||||
/// als <c>ok</c> gemeldeten Beat herab, sobald eine Prüfung fehlschlägt, und nennt in
|
||||
/// der Antwort die betroffene. Der klassische Fall, den das abfängt: Der Takt meldet
|
||||
/// brav <c>ok</c>, während der Aufgaben-Scanner seit einer Stunde tot ist.</para>
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><c>error</c> — kein OpenRouter-Key konfiguriert (Agenten deaktiviert).</item>
|
||||
/// <item><c>warning</c> — Tagesbudget der Instanz erschöpft oder Scanner steht.</item>
|
||||
/// <item><c>ok</c> — sonst.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>Die Metriken sind bewusst schlank und ausschließlich numerisch: keine
|
||||
/// sensiblen Nutzdaten, und nur Zahlen landen im Verlauf.</para>
|
||||
/// </summary>
|
||||
public sealed class InstanceHealthProvider : IInstanceHealthProvider
|
||||
{
|
||||
private readonly string _instanceName;
|
||||
private readonly bool _agentsEnabled;
|
||||
private readonly InstanceBudget _budget;
|
||||
private readonly IUsageRepository? _usage;
|
||||
private readonly Func<int> _agentCount;
|
||||
private readonly Func<int> _runningChats;
|
||||
private readonly Func<bool>? _schedulerRunning;
|
||||
private readonly Func<DateTime> _now;
|
||||
|
||||
public InstanceHealthProvider(
|
||||
string instanceName,
|
||||
bool agentsEnabled,
|
||||
InstanceBudget budget,
|
||||
IUsageRepository? usage,
|
||||
Func<int> agentCount,
|
||||
Func<int> runningChats,
|
||||
Func<bool>? schedulerRunning = null,
|
||||
Func<DateTime>? now = null)
|
||||
{
|
||||
_instanceName = instanceName;
|
||||
_agentsEnabled = agentsEnabled;
|
||||
_budget = budget;
|
||||
_usage = usage;
|
||||
_agentCount = agentCount;
|
||||
_runningChats = runningChats;
|
||||
_schedulerRunning = schedulerRunning;
|
||||
_now = now ?? (() => DateTime.Now);
|
||||
}
|
||||
|
||||
public async Task<InstanceHealth> GetAsync(CancellationToken ct)
|
||||
{
|
||||
var metrics = new Dictionary<string, double>
|
||||
{
|
||||
["agentCount"] = _agentCount(),
|
||||
["runningChats"] = _runningChats()
|
||||
};
|
||||
|
||||
var checks = new Dictionary<string, HealthCheck>
|
||||
{
|
||||
["agents"] = new(_agentsEnabled,
|
||||
_agentsEnabled ? null : "Kein OpenRouter-API-Key konfiguriert.")
|
||||
};
|
||||
|
||||
if (_schedulerRunning is not null)
|
||||
{
|
||||
var running = _schedulerRunning();
|
||||
checks["scheduler"] = new(running,
|
||||
running ? null : "Aufgaben-Scanner läuft nicht.");
|
||||
}
|
||||
|
||||
string? budgetProblem = null;
|
||||
|
||||
if (_usage is not null)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(_now());
|
||||
var used = await _usage.GetDailyAsync(today, agentId: "", ct).ConfigureAwait(false);
|
||||
|
||||
metrics["todayCostUsd"] = (double)decimal.Round(used.CostUsd, 4);
|
||||
metrics["todayTokens"] = used.TotalTokens;
|
||||
|
||||
if (Exceeds(_budget.DailyCostUsd, used.CostUsd))
|
||||
{
|
||||
budgetProblem =
|
||||
$"Tagesbudget erschöpft: {used.CostUsd:F2} von {_budget.DailyCostUsd:F2} USD.";
|
||||
}
|
||||
else if (Exceeds(_budget.DailyTokens, used.TotalTokens))
|
||||
{
|
||||
budgetProblem =
|
||||
$"Token-Tageslimit erschöpft: {used.TotalTokens:N0} von {_budget.DailyTokens:N0}.";
|
||||
}
|
||||
|
||||
checks["budget"] = new(budgetProblem is null, budgetProblem);
|
||||
}
|
||||
|
||||
// Der Instanzname steht in der Meldung, nicht in den Metriken: Metriken sind
|
||||
// Zahlen, alles andere würde der Server beim Verdichten ohnehin verwerfen.
|
||||
if (!_agentsEnabled)
|
||||
{
|
||||
return new InstanceHealth(
|
||||
WatchdogStatus.Error,
|
||||
$"{_instanceName}: Kein OpenRouter-API-Key konfiguriert – Agenten deaktiviert.",
|
||||
metrics, checks);
|
||||
}
|
||||
|
||||
if (budgetProblem is not null)
|
||||
return new InstanceHealth(WatchdogStatus.Warning, $"{_instanceName}: {budgetProblem}", metrics, checks);
|
||||
|
||||
if (_schedulerRunning is not null && !_schedulerRunning())
|
||||
{
|
||||
return new InstanceHealth(
|
||||
WatchdogStatus.Warning,
|
||||
$"{_instanceName}: Aufgaben-Scanner läuft nicht.",
|
||||
metrics, checks);
|
||||
}
|
||||
|
||||
return new InstanceHealth(WatchdogStatus.Ok, $"{_instanceName}: Betrieb normal.", metrics, checks);
|
||||
}
|
||||
|
||||
/// <summary>0 oder kleiner bedeutet: keine Grenze gesetzt. Deckungsgleich mit BudgetGuard.</summary>
|
||||
private static bool Exceeds(decimal limit, decimal used) => limit > 0 && used >= limit;
|
||||
|
||||
private static bool Exceeds(long limit, long used) => limit > 0 && used >= limit;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Deploymentcenter.Watchdog;
|
||||
|
||||
/// <summary>
|
||||
/// Sendet im festen Takt Heartbeats an das Watchdog-Modul und meldet Start und Ende.
|
||||
/// Ein nicht erreichbares Deploymentcenter darf ClawdDotNet nie beeinträchtigen —
|
||||
/// alle Sendefehler werden geloggt und verschluckt.
|
||||
///
|
||||
/// <para><b>Sauberes Beenden.</b> Beim Herunterfahren geht ein Heartbeat mit
|
||||
/// <c>status: "stopped"</c> raus. Der Evaluator lässt einen so gemeldeten Monitor in
|
||||
/// Ruhe; ohne das erzeugte jedes geplante Beenden wenige Minuten später einen
|
||||
/// Fehlalarm. Das reine Ereignis genügt dafür nicht — der Evaluator sieht nur den
|
||||
/// Monitor-Zustand.</para>
|
||||
/// </summary>
|
||||
public sealed class WatchdogHeartbeatService : IAsyncDisposable
|
||||
{
|
||||
private readonly IWatchdogClient _client;
|
||||
private readonly bool _ownsClient;
|
||||
private readonly IInstanceHealthProvider _health;
|
||||
private readonly int _intervalSeconds;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loop;
|
||||
private string _lastState = "unknown";
|
||||
|
||||
public WatchdogHeartbeatService(
|
||||
IWatchdogClient client,
|
||||
IInstanceHealthProvider health,
|
||||
int intervalSeconds,
|
||||
ILogger logger,
|
||||
bool ownsClient = false)
|
||||
{
|
||||
_client = client;
|
||||
_health = health;
|
||||
_intervalSeconds = Math.Clamp(intervalSeconds, 10, 86400);
|
||||
_logger = logger;
|
||||
_ownsClient = ownsClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut Client und Dienst in einem Zug. Wirft nur bei grob falscher Konfiguration
|
||||
/// (fehlende oder nicht-HTTPS-URL).
|
||||
/// </summary>
|
||||
public static WatchdogHeartbeatService Create(
|
||||
string baseUrl, string token, string source, string instance, string group, string os,
|
||||
string version, int intervalSeconds, IInstanceHealthProvider health, ILogger logger)
|
||||
{
|
||||
var client = WatchdogClient.Create(baseUrl, token, source, instance, group, os, version);
|
||||
return new WatchdogHeartbeatService(
|
||||
client, health, intervalSeconds, logger, ownsClient: true);
|
||||
}
|
||||
|
||||
public bool IsRunning => _loop is { IsCompleted: false };
|
||||
|
||||
/// <summary>Der zuletzt vom Server gemeldete Monitor-Zustand — für die Anzeige.</summary>
|
||||
public string LastState => _lastState;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (IsRunning)
|
||||
return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_loop = RunAsync(_cts.Token);
|
||||
_logger.LogInformation("Watchdog-Heartbeat gestartet (alle {Interval}s).", _intervalSeconds);
|
||||
}
|
||||
|
||||
private async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
await TrySendAsync(
|
||||
() => _client.SendEventAsync(
|
||||
WatchdogEventKind.Started, "info", "Instanz gestartet.", null, ct),
|
||||
"Start-Ereignis").ConfigureAwait(false);
|
||||
|
||||
// Erster Beat sofort, damit ein neuer Monitor nicht erst nach einem vollen
|
||||
// Intervall im Dashboard auftaucht.
|
||||
await BeatAsync(ct).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(_intervalSeconds));
|
||||
while (await timer.WaitForNextTickAsync(ct).ConfigureAwait(false))
|
||||
await BeatAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Regulärer Stopp.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BeatAsync(CancellationToken ct)
|
||||
{
|
||||
InstanceHealth health;
|
||||
try
|
||||
{
|
||||
health = await _health.GetAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Selbst wenn die Zustandsermittlung scheitert, soll ein Lebenszeichen
|
||||
// rausgehen — sonst sieht ein Fehler in unserem Code aus wie ein Ausfall.
|
||||
_logger.LogWarning(ex, "Watchdog: Zustandsermittlung fehlgeschlagen – melde warning.");
|
||||
health = new InstanceHealth(
|
||||
WatchdogStatus.Warning, "Zustand konnte nicht ermittelt werden.",
|
||||
new Dictionary<string, double>(), new Dictionary<string, HealthCheck>());
|
||||
}
|
||||
|
||||
await TrySendAsync(async () =>
|
||||
{
|
||||
var result = await _client.SendHeartbeatAsync(health, _intervalSeconds, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.State != _lastState)
|
||||
{
|
||||
_logger.LogInformation("Watchdog: Monitor-Zustand {Previous} → {State}{Failing}",
|
||||
_lastState, result.State,
|
||||
result.FailingChecks.Count > 0
|
||||
? $" (fehlgeschlagen: {string.Join(", ", result.FailingChecks)})"
|
||||
: "");
|
||||
|
||||
_lastState = result.State;
|
||||
}
|
||||
}, "Heartbeat").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task TrySendAsync(Func<Task> send, string what)
|
||||
{
|
||||
try
|
||||
{
|
||||
await send().ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (DeploymentcenterException ex) when (ex.IsAuthorizationProblem)
|
||||
{
|
||||
// Ein abgelehntes Token ist kein Rauschen: Ohne Eingriff bleibt der Monitor
|
||||
// für immer stumm, und niemand merkt es, weil ja nichts abstürzt.
|
||||
_logger.LogWarning(
|
||||
"Watchdog: {What} abgelehnt ({Code}) – Token prüfen (Recht watchdog:ping).",
|
||||
what, ex.Code);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ausfall des Monitorings darf den Betrieb nie stören.
|
||||
_logger.LogDebug(ex, "Watchdog: {What} konnte nicht gesendet werden (ignoriert).", what);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_cts is null)
|
||||
return;
|
||||
|
||||
await _cts.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
if (_loop is not null)
|
||||
{
|
||||
try { await _loop.ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { /* erwartet */ }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Watchdog: Heartbeat-Schleife endete mit Fehler."); }
|
||||
}
|
||||
|
||||
// Angekündigtes Ende, mit kurzer Frist. Hier wird alles geschluckt (auch ein
|
||||
// Zeitüberlauf), damit das Herunterfahren nie hängt oder wirft.
|
||||
try
|
||||
{
|
||||
using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
|
||||
await _client.SendHeartbeatAsync(
|
||||
new InstanceHealth(
|
||||
WatchdogStatus.Stopped, "Instanz planmäßig beendet.",
|
||||
new Dictionary<string, double>(), new Dictionary<string, HealthCheck>()),
|
||||
_intervalSeconds, stopCts.Token).ConfigureAwait(false);
|
||||
|
||||
await _client.SendEventAsync(
|
||||
WatchdogEventKind.StoppedGraceful, "info", "Instanz beendet.", null, stopCts.Token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Watchdog: Ende konnte nicht gemeldet werden (ignoriert).");
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
|
||||
if (_ownsClient && _client is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user