using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Diagnostics; using System.Text; namespace QRRapidoApp.Services.HealthChecks { public class SeqHealthCheck : IHealthCheck { private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly int _timeoutSeconds; private readonly string _testLogMessage; public SeqHealthCheck( IConfiguration configuration, ILogger logger, IHttpClientFactory httpClientFactory) { _configuration = configuration; _logger = logger; _httpClient = httpClientFactory.CreateClient(); _timeoutSeconds = configuration.GetValue("HealthChecks:Seq:TimeoutSeconds", 3); _testLogMessage = configuration.GetValue("HealthChecks:Seq:TestLogMessage", "QRRapido health check test"); } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); var data = new Dictionary(); var seqUrl = _configuration["Serilog:SeqUrl"]; if (string.IsNullOrEmpty(seqUrl)) { return HealthCheckResult.Degraded("Seq URL not configured - logging to console only", data: data); } try { using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds)); using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); // Test basic connectivity to Seq server var pingUrl = $"{seqUrl.TrimEnd('/')}/api"; var response = await _httpClient.GetAsync(pingUrl, combinedCts.Token); var latencyMs = stopwatch.ElapsedMilliseconds; data["reachable"] = response.IsSuccessStatusCode; data["latency"] = $"{latencyMs}ms"; data["seqUrl"] = seqUrl; data["statusCode"] = (int)response.StatusCode; if (!response.IsSuccessStatusCode) { data["error"] = $"HTTP {response.StatusCode}"; return HealthCheckResult.Unhealthy($"Seq server not reachable at {seqUrl} (HTTP {response.StatusCode})", data: data); } // Try to send a test log message if we can access the raw events endpoint try { await SendTestLogAsync(seqUrl, combinedCts.Token); data["lastLog"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); data["testLogSent"] = true; } catch (Exception logEx) { _logger.LogWarning(logEx, "Failed to send test log to Seq during health check"); data["testLogSent"] = false; data["testLogError"] = logEx.Message; } stopwatch.Stop(); data["totalCheckTimeMs"] = stopwatch.ElapsedMilliseconds; // Determine health status if (latencyMs > 2000) { return HealthCheckResult.Degraded($"Seq responding slowly ({latencyMs}ms)", data: data); } return HealthCheckResult.Healthy($"Seq healthy ({latencyMs}ms)", data: data); } catch (OperationCanceledException) { data["reachable"] = false; data["error"] = "timeout"; return HealthCheckResult.Unhealthy($"Seq health check timed out after {_timeoutSeconds} seconds", data: data); } catch (Exception ex) { _logger.LogWarning(ex, "Seq health check failed"); data["reachable"] = false; data["error"] = ex.Message; return HealthCheckResult.Unhealthy($"Seq health check failed: {ex.Message}", data: data); } } private async Task SendTestLogAsync(string seqUrl, CancellationToken cancellationToken) { var apiKey = _configuration["Serilog:ApiKey"]; var eventsUrl = $"{seqUrl.TrimEnd('/')}/api/events/raw"; // Create a simple CLEF (Compact Log Event Format) message var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffK"); var logEntry = $"{{\"@t\":\"{timestamp}\",\"@l\":\"Information\",\"@m\":\"Health check test from QRRapido\",\"ApplicationName\":\"QRRapido\",\"HealthCheck\":true,\"TestMessage\":\"{_testLogMessage}\"}}"; var content = new StringContent(logEntry, Encoding.UTF8, "application/vnd.serilog.clef"); // Add API key if configured if (!string.IsNullOrEmpty(apiKey)) { content.Headers.Add("X-Seq-ApiKey", apiKey); } var response = await _httpClient.PostAsync(eventsUrl, content, cancellationToken); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"Failed to send test log to Seq: HTTP {response.StatusCode}"); } } } }