330 lines
13 KiB
C#
330 lines
13 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
|
|
namespace QRRapidoApp.Controllers
|
|
{
|
|
[ApiController]
|
|
[Route("health")]
|
|
public class HealthController : ControllerBase
|
|
{
|
|
private readonly HealthCheckService _healthCheckService;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<HealthController> _logger;
|
|
private readonly string _applicationName;
|
|
private readonly string _version;
|
|
private static readonly DateTime _startTime = System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
|
|
|
public HealthController(
|
|
HealthCheckService healthCheckService,
|
|
IConfiguration configuration,
|
|
ILogger<HealthController> logger)
|
|
{
|
|
_healthCheckService = healthCheckService;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_applicationName = configuration["ApplicationName"] ?? "QRRapido";
|
|
_version = configuration["App:Version"] ?? "1.0.0";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Comprehensive health check with detailed information
|
|
/// GET /health/detailed
|
|
/// </summary>
|
|
[HttpGet("detailed")]
|
|
public async Task<IActionResult> 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"),
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MongoDB-specific health check
|
|
/// GET /health/mongodb
|
|
/// </summary>
|
|
[HttpGet("mongodb")]
|
|
public async Task<IActionResult> 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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// System resources health check
|
|
/// GET /health/resources
|
|
/// </summary>
|
|
[HttpGet("resources")]
|
|
public async Task<IActionResult> 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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// External services health check
|
|
/// GET /health/external
|
|
/// </summary>
|
|
[HttpGet("external")]
|
|
public async Task<IActionResult> 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 });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple health check - just overall status
|
|
/// GET /health/simple or GET /health
|
|
/// </summary>
|
|
[HttpGet("simple")]
|
|
[HttpGet("")]
|
|
public async Task<IActionResult> 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
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Health check for Uptime Kuma monitoring
|
|
/// GET /health/uptime-kuma
|
|
/// </summary>
|
|
[HttpGet("uptime-kuma")]
|
|
public async Task<IActionResult> 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 uptimeMinutes = (DateTime.UtcNow - _startTime).TotalMinutes;
|
|
var memoryBytes = GC.GetTotalMemory(false);
|
|
var memoryMB = memoryBytes / 1024.0 / 1024.0;
|
|
|
|
var response = new
|
|
{
|
|
status = overallStatus,
|
|
application = _applicationName,
|
|
version = _version,
|
|
timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
|
uptime = System.FormattableString.Invariant($"{(DateTime.UtcNow - _startTime).TotalHours:F1}h"),
|
|
// Include critical metrics for monitoring
|
|
metrics = new
|
|
{
|
|
mongodb_connected = GetCheckStatus(healthReport, "mongodb") == "healthy",
|
|
resources_ok = GetCheckStatus(healthReport, "resources") == "healthy",
|
|
external_services_ok = GetCheckStatus(healthReport, "external_services") == "healthy"
|
|
}
|
|
};
|
|
|
|
_logger.LogInformation("[HEALTH] Memory: {memoryBytes} MemoryMB: {memoryMB} ThreadPool: {threadPool} ProcessId: {processId} ActiveConnections: {activeConnections} UptimeMinutes: {uptimeMinutes}",
|
|
memoryBytes,
|
|
memoryMB,
|
|
ThreadPool.ThreadCount,
|
|
Process.GetCurrentProcess().Id,
|
|
HttpContext.Connection?.Id ?? "null",
|
|
Math.Round(uptimeMinutes, 1));
|
|
|
|
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<string, object>
|
|
{
|
|
["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";
|
|
}
|
|
}
|
|
} |