using System.Text; using System.Text.Json; using Serilog; namespace Predictalytics.WinFormsHost.Services; /// /// Sends periodic dead-man's-switch heartbeats to the external Watchdog server /// (POST /api/heartbeat) so an outage of this app — or the whole machine — raises /// an alarm. A Watchdog outage must never impact the app: every call is best effort. /// public sealed class WatchdogHeartbeatService : IDisposable { private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(10) }; private readonly string _baseUrl; private readonly string _apiKey; private readonly string _source; private readonly string _instance; private readonly int _intervalSeconds; private readonly Func? _metadataProvider; private readonly DateTime _startedUtc = DateTime.UtcNow; private System.Threading.Timer? _timer; private bool _lastSendFailed; public WatchdogHeartbeatService( string baseUrl, string apiKey, string source, string instance, int intervalSeconds, Func? metadataProvider = null) { _baseUrl = baseUrl.TrimEnd('/'); _apiKey = apiKey; _source = source; _instance = string.IsNullOrWhiteSpace(instance) ? "default" : instance; _intervalSeconds = Math.Max(15, intervalSeconds); _metadataProvider = metadataProvider; } public void Start() { _timer?.Dispose(); _timer = new System.Threading.Timer( async _ => await SendHeartbeatAsync("ok"), null, TimeSpan.Zero, TimeSpan.FromSeconds(_intervalSeconds)); Log.Information("🐕 Watchdog heartbeat started → {Url} (source={Source}, every {Interval}s)", _baseUrl, _source, _intervalSeconds); } /// /// Reports a planned shutdown so it is not alarmed as a crash. /// Must be "stopped_graceful": the server's event_log.kind ENUM has no "stopping" /// value, and that variant fails the insert with HTTP 500 after the state update. /// public void NotifyStopping() { try { var payload = new { source = _source, instance = _instance, kind = "stopped_graceful", severity = "info", message = "Predictalytics wird planmäßig beendet." }; // Synchronous with a short cap: the form is closing and must not hang. PostAsync("/api/event", payload).Wait(TimeSpan.FromSeconds(4)); } catch { // Best effort only. } } private async Task SendHeartbeatAsync(string status) { try { var payload = new { source = _source, instance = _instance, type = "heartbeat", status, message = (string?)null, metrics = new { uptimeSec = (long)(DateTime.UtcNow - _startedUtc).TotalSeconds, app = _metadataProvider?.Invoke() }, group = "C# Applications", interval = _intervalSeconds }; await PostAsync("/api/heartbeat", payload); if (_lastSendFailed) { _lastSendFailed = false; Log.Information("🐕 Watchdog heartbeat wieder erfolgreich zugestellt."); } } catch (Exception ex) { // Log the first failure as warning, subsequent ones quietly (no log flood). if (!_lastSendFailed) { _lastSendFailed = true; Log.Warning("🐕 Watchdog heartbeat fehlgeschlagen (weitere Fehler werden unterdrückt): {Error}", ex.Message); } else { Log.Debug(ex, "Watchdog heartbeat failed"); } } } private async Task PostAsync(string path, object payload) { using var request = new HttpRequestMessage(HttpMethod.Post, _baseUrl + path); request.Headers.Add("X-Watchdog-Key", _apiKey); request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); using var response = await _http.SendAsync(request); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new HttpRequestException($"Watchdog API HTTP {(int)response.StatusCode}: {body}"); } } public void Dispose() { _timer?.Dispose(); _timer = null; _http.Dispose(); } }