using ClawdDotNet.Core.Accounting; using ClawdDotNet.Core.Config; namespace ClawdDotNet.Core.Deploymentcenter.Watchdog; /// /// Leitet den Instanz-Zustand für den Heartbeat ab. /// /// Ein Heartbeat allein beweist nur, dass ein Faden läuft. Deshalb geht der /// selbst ermittelte Gesundheitszustand als checks mit — der Server stuft einen /// als ok 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 ok, während der Aufgaben-Scanner seit einer Stunde tot ist. /// /// /// error — kein OpenRouter-Key konfiguriert (Agenten deaktiviert). /// warning — Tagesbudget der Instanz erschöpft oder Scanner steht. /// ok — sonst. /// /// /// Die Metriken sind bewusst schlank und ausschließlich numerisch: keine /// sensiblen Nutzdaten, und nur Zahlen landen im Verlauf. /// public sealed class InstanceHealthProvider : IInstanceHealthProvider { private readonly string _instanceName; private readonly bool _agentsEnabled; private readonly InstanceBudget _budget; private readonly IUsageRepository? _usage; private readonly Func _agentCount; private readonly Func _runningChats; private readonly Func? _schedulerRunning; private readonly Func _now; public InstanceHealthProvider( string instanceName, bool agentsEnabled, InstanceBudget budget, IUsageRepository? usage, Func agentCount, Func runningChats, Func? schedulerRunning = null, Func? now = null) { _instanceName = instanceName; _agentsEnabled = agentsEnabled; _budget = budget; _usage = usage; _agentCount = agentCount; _runningChats = runningChats; _schedulerRunning = schedulerRunning; _now = now ?? (() => DateTime.Now); } public async Task GetAsync(CancellationToken ct) { var metrics = new Dictionary { ["agentCount"] = _agentCount(), ["runningChats"] = _runningChats() }; var checks = new Dictionary { ["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); } /// 0 oder kleiner bedeutet: keine Grenze gesetzt. Deckungsgleich mit BudgetGuard. private static bool Exceeds(decimal limit, decimal used) => limit > 0 && used >= limit; private static bool Exceeds(long limit, long used) => limit > 0 && used >= limit; }