using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Diagnostics; using System.Text.Json; namespace QRRapidoApp.Controllers { [ApiController] [Route("health")] public class HealthController : ControllerBase { private readonly HealthCheckService _healthCheckService; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _applicationName; private readonly string _version; private static readonly DateTime _startTime = DateTime.UtcNow; public HealthController( HealthCheckService healthCheckService, IConfiguration configuration, ILogger logger) { _healthCheckService = healthCheckService; _configuration = configuration; _logger = logger; _applicationName = configuration["ApplicationName"] ?? "QRRapido"; _version = configuration["App:Version"] ?? "1.0.0"; } /// /// Comprehensive health check with detailed information /// GET /health/detailed /// [HttpGet("detailed")] public async Task GetDetailedHealth() { try { var stopwatch = Stopwatch.StartNew(); var healthReport = await _healthCheckService.CheckHealthAsync(); stopwatch.Stop(); var uptime = DateTime.UtcNow - _startTime; var overallStatus = DetermineOverallStatus(healthReport.Status); var response = new { applicationName = _applicationName, status = overallStatus, timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), uptime = $"{uptime.Days}d {uptime.Hours}h {uptime.Minutes}m", totalDuration = $"{stopwatch.ElapsedMilliseconds}ms", checks = new { mongodb = ExtractCheckResult(healthReport, "mongodb"), seq = ExtractCheckResult(healthReport, "seq"), resources = ExtractCheckResult(healthReport, "resources"), externalServices = ExtractCheckResult(healthReport, "external_services") }, version = _version, environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production" }; var statusCode = overallStatus switch { "unhealthy" => 503, "degraded" => 200, // Still return 200 for degraded to avoid false alarms _ => 200 }; _logger.LogInformation("Detailed health check completed - Status: {Status}, Duration: {Duration}ms", overallStatus, stopwatch.ElapsedMilliseconds); return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "Detailed health check failed"); return StatusCode(503, new { applicationName = _applicationName, status = "unhealthy", timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), error = "Health check system failure", version = _version }); } } /// /// MongoDB-specific health check /// GET /health/mongodb /// [HttpGet("mongodb")] public async Task GetMongoDbHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == "mongodb"); var mongoCheck = healthReport.Entries.FirstOrDefault(e => e.Key == "mongodb"); if (mongoCheck.Key == null) { return StatusCode(503, new { status = "unhealthy", error = "MongoDB health check not found" }); } var statusCode = mongoCheck.Value.Status == HealthStatus.Healthy ? 200 : 503; var response = ExtractCheckResult(healthReport, "mongodb"); return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "MongoDB health check failed"); return StatusCode(503, new { status = "unhealthy", error = ex.Message }); } } /// /// Seq logging service health check /// GET /health/seq /// [HttpGet("seq")] public async Task GetSeqHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == "seq"); var seqCheck = healthReport.Entries.FirstOrDefault(e => e.Key == "seq"); if (seqCheck.Key == null) { return StatusCode(503, new { status = "unhealthy", error = "Seq health check not found" }); } var statusCode = seqCheck.Value.Status == HealthStatus.Unhealthy ? 503 : 200; var response = ExtractCheckResult(healthReport, "seq"); return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "Seq health check failed"); return StatusCode(503, new { status = "unhealthy", error = ex.Message }); } } /// /// System resources health check /// GET /health/resources /// [HttpGet("resources")] public async Task GetResourcesHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == "resources"); var resourceCheck = healthReport.Entries.FirstOrDefault(e => e.Key == "resources"); if (resourceCheck.Key == null) { return StatusCode(503, new { status = "unhealthy", error = "Resources health check not found" }); } var statusCode = resourceCheck.Value.Status == HealthStatus.Unhealthy ? 503 : 200; var response = ExtractCheckResult(healthReport, "resources"); return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "Resources health check failed"); return StatusCode(503, new { status = "unhealthy", error = ex.Message }); } } /// /// External services health check /// GET /health/external /// [HttpGet("external")] public async Task GetExternalServicesHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == "external_services"); var externalCheck = healthReport.Entries.FirstOrDefault(e => e.Key == "external_services"); if (externalCheck.Key == null) { return StatusCode(503, new { status = "unhealthy", error = "External services health check not found" }); } var statusCode = externalCheck.Value.Status == HealthStatus.Unhealthy ? 503 : 200; var response = ExtractCheckResult(healthReport, "external_services"); return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "External services health check failed"); return StatusCode(503, new { status = "unhealthy", error = ex.Message }); } } /// /// Simple health check - just overall status /// GET /health/simple or GET /health /// [HttpGet("simple")] [HttpGet("")] public async Task GetSimpleHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(); var overallStatus = DetermineOverallStatus(healthReport.Status); var statusCode = overallStatus switch { "unhealthy" => 503, _ => 200 }; var response = new { status = overallStatus, timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") }; return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "Simple health check failed"); return StatusCode(503, new { status = "unhealthy", timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), error = ex.Message }); } } /// /// Health check for Uptime Kuma monitoring /// GET /health/uptime-kuma /// [HttpGet("uptime-kuma")] public async Task GetUptimeKumaHealth() { try { var healthReport = await _healthCheckService.CheckHealthAsync(); var overallStatus = DetermineOverallStatus(healthReport.Status); // For Uptime Kuma, we want to return 200 OK for healthy/degraded and 503 for unhealthy var statusCode = overallStatus == "unhealthy" ? 503 : 200; var response = new { status = overallStatus, application = _applicationName, version = _version, timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), uptime = $"{(DateTime.UtcNow - _startTime).TotalHours:F1}h", // Include critical metrics for monitoring metrics = new { mongodb_connected = GetCheckStatus(healthReport, "mongodb") != "unhealthy", seq_reachable = GetCheckStatus(healthReport, "seq") != "unhealthy", resources_ok = GetCheckStatus(healthReport, "resources") != "unhealthy", external_services_ok = GetCheckStatus(healthReport, "external_services") != "unhealthy" } }; return StatusCode(statusCode, response); } catch (Exception ex) { _logger.LogError(ex, "Uptime Kuma health check failed"); return StatusCode(503, new { status = "unhealthy", application = _applicationName, timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), error = "Health check system failure" }); } } private object ExtractCheckResult(HealthReport healthReport, string checkName) { if (!healthReport.Entries.TryGetValue(checkName, out var entry)) { return new { status = "not_configured", error = $"Health check '{checkName}' not found" }; } var baseResult = new Dictionary { ["status"] = entry.Status.ToString().ToLower(), ["duration"] = $"{entry.Duration.TotalMilliseconds}ms", ["description"] = entry.Description ?? "" }; // Add all custom data from the health check if (entry.Data != null) { foreach (var kvp in entry.Data) { baseResult[kvp.Key] = kvp.Value; } } if (entry.Exception != null) { baseResult["error"] = entry.Exception.Message; } return baseResult; } private string DetermineOverallStatus(HealthStatus healthStatus) { return healthStatus switch { HealthStatus.Healthy => "healthy", HealthStatus.Degraded => "degraded", HealthStatus.Unhealthy => "unhealthy", _ => "unknown" }; } private string GetCheckStatus(HealthReport healthReport, string checkName) { if (healthReport.Entries.TryGetValue(checkName, out var entry)) { return entry.Status.ToString().ToLower(); } return "not_configured"; } } }