creates simple healthchecks
This commit is contained in:
230
__/HealthCheckContainer.cs
Normal file
230
__/HealthCheckContainer.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using MCVIngenieros.Healthchecks.Abstracts;
|
||||
using MCVIngenieros.Healthchecks.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MCVIngenieros.Healthchecks.__;
|
||||
|
||||
// Health check container implementation
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class HealthCheckContainer : IHealthCheckContainer, IDisposable
|
||||
{
|
||||
private readonly IServiceProvider provider;
|
||||
private readonly ILogger<HealthCheckContainer> _logger;
|
||||
private readonly TimeSpan _cacheDuration;
|
||||
private readonly TimeSpan? _timeout;
|
||||
private readonly static ConcurrentDictionary<string, Type> _checkTypes = new();
|
||||
private readonly ConcurrentDictionary<string, (HealthCheckResult Result, DateTime LastUpdate)> _cache = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _backgroundTask;
|
||||
private readonly Assembly[]? _assembliesToScan;
|
||||
|
||||
public HealthCheckContainer(IServiceProvider provider, ILogger<HealthCheckContainer> logger, IOptions<HealthCheckContainerOptions> options)
|
||||
{
|
||||
this.provider = provider;
|
||||
_logger = logger;
|
||||
_cacheDuration = options.Value.CacheDuration;
|
||||
_timeout = options.Value.Timeout;
|
||||
|
||||
_assembliesToScan = options.Value.AssembliesToScan?.ToArray() ?? null;
|
||||
_assembliesToScan = _assembliesToScan?.Distinct().ToArray();
|
||||
|
||||
LoadHealthChecks();
|
||||
|
||||
// Start background refresh
|
||||
_backgroundTask = Task.Run(() => BackgroundRefreshAsync(_cts.Token));
|
||||
}
|
||||
|
||||
public void LoadHealthChecks()
|
||||
{
|
||||
var checks = provider.GetServices<IHealthCheck>();
|
||||
foreach (var check in checks)
|
||||
{
|
||||
var type = check.GetType();
|
||||
if (_checkTypes.TryAdd(type.Name, type))
|
||||
{
|
||||
_logger.LogInformation($"Loaded health check: {type.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Health check {type.Name} is already registered. Skipping duplicate.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover all IHealthCheck implementations in loaded assemblies
|
||||
public void DiscoverHealthChecks()
|
||||
{
|
||||
if (_assembliesToScan == null)
|
||||
return;
|
||||
|
||||
foreach (var assembly in _assembliesToScan)
|
||||
{
|
||||
var checkTypes = assembly.GetTypes()
|
||||
.Where(t => typeof(IHealthCheck).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
|
||||
|
||||
assembly.GetReferencedAssemblies()
|
||||
.Select(Assembly.Load)
|
||||
.ToList()
|
||||
.ForEach(a =>
|
||||
{
|
||||
var types = a.GetTypes()
|
||||
.Where(t => typeof(IHealthCheck).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
|
||||
checkTypes = checkTypes.Concat(types);
|
||||
});
|
||||
|
||||
checkTypes = checkTypes.Distinct();
|
||||
|
||||
foreach (var type in checkTypes)
|
||||
{
|
||||
if (_checkTypes.TryAdd(type.Name, type))
|
||||
{
|
||||
_logger.LogInformation($"Discovered health check: {type.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"Health check {type.Name} is already registered. Skipping duplicate.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetAllCheckNames() => [.. _checkTypes.Keys];
|
||||
|
||||
public async Task<List<HealthCheckResult>> RunAllChecks(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Running all health checks...");
|
||||
var tasks = _checkTypes.Keys.Select(name => RunCheck(name, cancellationToken));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
_logger.LogInformation("Completed running all health checks.");
|
||||
return results.Where(r => r != null).ToList()!;
|
||||
}
|
||||
|
||||
public Task<List<HealthCheckResult>> GetAllChecksResults(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<HealthCheckResult>();
|
||||
foreach (var item in _cache)
|
||||
{
|
||||
results.Add(item.Value.Result);
|
||||
}
|
||||
return Task.FromResult(results);
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResult?> RunCheck(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
_logger.LogInformation($"Running health check: {name}");
|
||||
if (!_checkTypes.TryGetValue(name, out var type))
|
||||
{
|
||||
_logger.LogWarning($"Health check {name} not found.");
|
||||
return null;
|
||||
}
|
||||
if (_cache.TryGetValue(name, out var cached) && now - cached.LastUpdate < _cacheDuration)
|
||||
{
|
||||
_logger.LogInformation($"Returning cached result for {name}.");
|
||||
return cached.Result;
|
||||
}
|
||||
var result = await RunCheckInstanceAsync(type, cancellationToken);
|
||||
_cache[name] = (result, DateTime.UtcNow);
|
||||
_logger.LogInformation($"Health check {name} completed. Result: {(result.IsHealthy ? "Healthy" : "Unhealthy")}");
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<HealthCheckResult>> RunChecks(IEnumerable<string> names, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation($"Running health checks: {string.Join(", ", names)}");
|
||||
var tasks = names.Select(name => RunCheck(name, cancellationToken));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
_logger.LogInformation($"Completed running specified health checks. Runned {results.Count(r => r != null)} checks.");
|
||||
return results.Where(r => r != null).ToList()!;
|
||||
}
|
||||
|
||||
// Background refresh of all checks
|
||||
private async Task BackgroundRefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = _checkTypes.Keys.Select(name => RunCheck(name, cancellationToken));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during background health check refresh");
|
||||
}
|
||||
await Task.Delay(_cacheDuration, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Run a single check instance with timeout, retry, circuit breaker, fallback
|
||||
private async Task<HealthCheckResult> RunCheckInstanceAsync(Type type, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"Starting health check: {type.Name}");
|
||||
var name = type.Name;
|
||||
var logger = provider.GetService(typeof(ILogger<>).MakeGenericType(type)) as ILogger;
|
||||
try
|
||||
{
|
||||
IHealthCheck? check = ActivatorUtilities.CreateInstance(provider, type) as IHealthCheck
|
||||
?? throw new InvalidOperationException($"Could not create health check: {name}");
|
||||
var retry = check.RetryAttempts ?? HealthChecksConfigs.Default.RetryAttempts;
|
||||
TimeSpan timeout = (TimeSpan)((check.Timeout ?? _timeout) ?? HealthChecksConfigs.Default.Timeout!);
|
||||
TimeSpan delay = (TimeSpan)(check.RetryDelay ?? HealthChecksConfigs.Default.RetryDelay!);
|
||||
|
||||
for (int attempt = 0; attempt <= retry; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(timeout);
|
||||
var result = await check.CheckAsync(cts.Token);
|
||||
_logger.LogInformation($"Health check {name} completed. Result: {(result.IsHealthy ? "Healthy" : "Unhealthy")}");
|
||||
result.Name = name;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, $"Health check {name} failed on attempt {attempt + 1} -- Exception was not controlled.");
|
||||
if (attempt == retry)
|
||||
{
|
||||
return new HealthCheckResult(false)
|
||||
{
|
||||
Name = name,
|
||||
Details = $"Exception: {ex.GetBaseException().Message} -- StackTrace: {ex.GetBaseException().StackTrace}",
|
||||
Severity = HealthCheckSeverity.Critical
|
||||
};
|
||||
}
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex.GetBaseException(), $"Error creating instance of health check {name}");
|
||||
return new HealthCheckResult(false) { Name = name, Details = $"Error creating instance: {ex.GetBaseException().Message}", Severity = HealthCheckSeverity.Critical };
|
||||
}
|
||||
return new HealthCheckResult(false) { Name = name, Details = "Unknown error", Severity = HealthCheckSeverity.Critical };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_backgroundTask?.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, IHealthCheck> GetAllChecks()
|
||||
{
|
||||
var dict = new Dictionary<string, IHealthCheck>();
|
||||
foreach (var item in _checkTypes.ToImmutableDictionary())
|
||||
{
|
||||
IHealthCheck? check = (IHealthCheck?)ActivatorUtilities.CreateInstance(provider, item.Value);
|
||||
if (check == null) continue;
|
||||
dict[item.Key] = check;
|
||||
}
|
||||
return dict.ToImmutableDictionary();
|
||||
}
|
||||
}
|
13
__/HealthCheckContainerOptions.cs
Normal file
13
__/HealthCheckContainerOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace MCVIngenieros.Healthchecks.__;
|
||||
|
||||
// Options for health check container
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class HealthCheckContainerOptions
|
||||
{
|
||||
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(10);
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3);
|
||||
public IEnumerable<System.Reflection.Assembly> AssembliesToScan { get; set; } = [];
|
||||
}
|
||||
|
292
__/__Controllers/HealthCheckController.cs
Normal file
292
__/__Controllers/HealthCheckController.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using MCVIngenieros.Healthchecks.Abstracts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Net.Mime;
|
||||
|
||||
namespace MCVIngenieros.Healthchecks.__.__Controllers;
|
||||
|
||||
[ApiController, Route("api/health"), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class HealthCheckController(IHealthCheckContainer container, ILogger<HealthCheckController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IHealthCheckContainer _container = container;
|
||||
private readonly ILogger<HealthCheckController> _logger = logger;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Run([FromQuery] string? checks = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation($"Health check requested for checks: {checks ?? "ALL"}");
|
||||
List<HealthCheckResult> results;
|
||||
if (!string.IsNullOrEmpty(checks))
|
||||
{
|
||||
var names = checks.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
await _container.RunChecks(names, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _container.RunAllChecks(cancellationToken);
|
||||
}
|
||||
results = await _container.GetAllChecksResults(cancellationToken);
|
||||
|
||||
var status = results.All(r => r.IsHealthy) ? "Healthy" : "Unhealthy";
|
||||
var code = results.All(r => r.IsHealthy) ? 200 : 503;
|
||||
var acceptHeader = Request.Headers.Accept.ToString();
|
||||
var userAgent = Request.Headers.UserAgent.ToString();
|
||||
if (IsBrowser(Request))
|
||||
{
|
||||
return Content(RenderHtml(status, results, _container), MediaTypeNames.Text.Html);
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
status,
|
||||
results = results.ToDictionary(r => r.Name, r => new { isHealthy = r.IsHealthy, details = r.Details, severity = r.Severity.ToString() })
|
||||
};
|
||||
return StatusCode(code, response);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet, Route("{checkName}")]
|
||||
public async Task<IActionResult> Get([FromRoute] string checkName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _container.RunCheck(checkName, cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
_logger.LogWarning($"Health check '{checkName}' not found.");
|
||||
return NotFound();
|
||||
}
|
||||
_logger.LogWarning($"Health check requested for {checkName}.");
|
||||
|
||||
var status = result.IsHealthy ? "Healthy" : "Unhealthy";
|
||||
var code = result.IsHealthy ? 200 : 503;
|
||||
var response = new
|
||||
{
|
||||
status,
|
||||
result = result == null ? null : new { isHealthy = result.IsHealthy, details = result.Details, severity = result.Severity.ToString() }
|
||||
};
|
||||
return StatusCode(code, response);
|
||||
}
|
||||
|
||||
private static bool UserAgentIndicatesBrowser(string userAgent)
|
||||
=> !string.IsNullOrEmpty(userAgent) && (
|
||||
userAgent.Contains("Mozilla", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("Chrome", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("Safari", StringComparison.OrdinalIgnoreCase)
|
||||
|| userAgent.Contains("Edge", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
private static bool AcceptsHtml(string acceptHeader)
|
||||
=> !string.IsNullOrEmpty(acceptHeader)
|
||||
&& acceptHeader.Contains("text/html", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBrowser(HttpRequest request)
|
||||
=> AcceptsHtml(request.Headers["Accept"].ToString())
|
||||
|| UserAgentIndicatesBrowser(request.Headers["User-Agent"].ToString());
|
||||
|
||||
private static string GetSeverityClases()
|
||||
{
|
||||
var severitiesDict = Enum.GetValues<HealthCheckSeverity>()
|
||||
.Cast<HealthCheckSeverity>()
|
||||
.ToDictionary(e => (int)e, e => e.ToString());
|
||||
|
||||
var totalSeverities = severitiesDict.Count;
|
||||
var colores = ColorInterpolation.GenerateColorGradient(totalSeverities);
|
||||
string cssClasses = string.Empty;
|
||||
int i = 0;
|
||||
foreach (var item in severitiesDict)
|
||||
{
|
||||
string cssClass = $".{item.Value}{{ color: {colores[i++]}; font-weight: bold; }}";
|
||||
cssClasses += cssClass + Environment.NewLine;
|
||||
}
|
||||
return cssClasses;
|
||||
}
|
||||
|
||||
// Render HTML page for browser
|
||||
private static string RenderHtml(string status, List<HealthCheckResult> results, IHealthCheckContainer container)
|
||||
{
|
||||
var severitiesDict = Enum.GetValues<HealthCheckSeverity>()
|
||||
.Cast<HealthCheckSeverity>()
|
||||
.ToDictionary(e => (int)e, e => e.ToString());
|
||||
var healthy = status == "Healthy";
|
||||
var bannerColor = healthy ? "#4caf50" : "#f44336";
|
||||
var html = $@"<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""UTF-8"">
|
||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||
<title>Health Checks</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f5f5f5; }}
|
||||
.banner {{ background: {bannerColor}; color: #fff; padding: 1.5rem; text-align: center; font-size: 2rem; }}
|
||||
.container {{ max-width: 900px; margin: 2rem auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 2rem; }}
|
||||
table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
|
||||
th, td {{ padding: 0.75rem; border-bottom: 1px solid #eee; text-align: left; }}
|
||||
td.clickable:hover {{ background: #f0f0f0; cursor: pointer; }}
|
||||
th {{ background: #fafafa; }}
|
||||
tr:last-child td {{ border-bottom: none; }}
|
||||
td > label {{ width: 100%; height: 100%; cursor: pointer; padding: 0; display: block; }}
|
||||
td > label > input[type=""checkbox""] {{ width: 1.5em; height: 1.5em; color: #000000; accent-color: #4caf50; cursor: pointer; }}
|
||||
.healthy {{ color: #4caf50; font-weight: bold; }}
|
||||
.unhealthy {{ color: #f44336; font-weight: bold; }}
|
||||
{GetSeverityClases()}
|
||||
@media (max-width: 600px) {{
|
||||
.container {{ padding: 0.5rem; }}
|
||||
.banner {{ font-size: 1.2rem; }}
|
||||
table, th, td {{ font-size: 0.9rem; }}
|
||||
}}
|
||||
.run-single {{ margin-top: 2rem; }}
|
||||
.run-single button {{ padding: 0.5rem 1rem; background: #2196f3; color: #fff; border: none; border-radius: 4px; cursor: pointer; }}
|
||||
.run-single button:hover {{ background: #1976d2; }}
|
||||
/* Modal styles */
|
||||
.modal {{ display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background: rgba(0,0,0,0.4); }}
|
||||
.modal-content {{ background: #fff; margin: 10% auto; padding: 2rem; border-radius: 8px; max-width: 500px; position: relative; }}
|
||||
.close {{ position: absolute; top: 10px; right: 20px; font-size: 2rem; color: #888; cursor: pointer; }}
|
||||
.modal-content pre {{ background: #f5f5f5; padding: 1em; border-radius: 4px; overflow-x: auto; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""banner"">Health Status: {status}</div>
|
||||
<div class=""container"">
|
||||
<h2>Health Check Results</h2>
|
||||
<table id=""ChecksTable"">
|
||||
<thead>
|
||||
<tr><th>Selected</th><th>Name</th><th>Status</th><th>Severity</th><th>Details</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{string.Join("", results.Select((r, idx) => $"<tr data-idx='{idx}' class='row-hover'>" +
|
||||
@$"<td><label class='custom-checkbox'><input type='checkbox' name='checks' value='{r.Name}'></label></td>" +
|
||||
$"<td class='clickable'>{r.Name.Replace("HealthCheck", "", StringComparison.OrdinalIgnoreCase)}</td>" +
|
||||
$"<td class='clickable {(r.IsHealthy ? "healthy" : "unhealthy")}'>{(r.IsHealthy ? "Healthy" : "Unhealthy")}</td>" +
|
||||
@$"<td class='clickable " + r.Severity + @$"'>{r.Severity}</td>" +
|
||||
$"<td class='clickable'>{System.Net.WebUtility.HtmlEncode(r.Details)}</td>" +
|
||||
"</tr>"))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class=""run-single"">
|
||||
<h3>Run Selected Checks</h3>
|
||||
<form id=""runChecksForm"" method=""get"" action=""/api/healthcheck"" onsubmit=""event.preventDefault(); runSelectedChecks();"">
|
||||
<button type=""submit"">Run</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id=""modal"" class=""modal"">
|
||||
<div class=""modal-content"">
|
||||
<span class=""close"" onclick=""closeModal()"">×</span>
|
||||
<h3 id=""modalTitle""></h3>
|
||||
<div id=""modalBody""></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const results = {System.Text.Json.JsonSerializer.Serialize(results)};
|
||||
const tests = {System.Text.Json.JsonSerializer.Serialize(container.GetAllChecks())}
|
||||
const severities = {System.Text.Json.JsonSerializer.Serialize(severitiesDict)}
|
||||
function getSeverity(severity){{
|
||||
return severities[severity] ?? ""Critical""
|
||||
}}
|
||||
function getSeverityClass(severity){{
|
||||
return severities[severity] ?? ""Critical""
|
||||
}}
|
||||
function runSelectedChecks() {{
|
||||
const table = document.getElementById('ChecksTable');
|
||||
const selected = Array.from(table.querySelectorAll('input[name=\'checks\']:checked')).map(cb => cb.value);
|
||||
if(selected.length === 0) {{
|
||||
return;
|
||||
}}
|
||||
const url = '/api/healthcheck?checks=' + encodeURIComponent(selected.join(','));
|
||||
window.location = url;
|
||||
}}
|
||||
// Modal logic
|
||||
function closeModal() {{
|
||||
document.getElementById('modal').style.display = 'none';
|
||||
}}
|
||||
document.addEventListener('DOMContentLoaded', function() {{
|
||||
var table = document.getElementById('ChecksTable');
|
||||
table.querySelectorAll('tbody tr').forEach(function(row) {{
|
||||
row.querySelectorAll('td.clickable').forEach(function(cell) {{
|
||||
cell.addEventListener('click', function() {{
|
||||
const idx = row.getAttribute('data-idx');
|
||||
const r = results[idx];
|
||||
const test = tests[r.Name];
|
||||
const config = (({{ RetryAttempts, RetryDelay, Timeout }}) => ({{ RetryAttempts, RetryDelay, Timeout }}))(test);
|
||||
const severity = getSeverity(r.Severity);
|
||||
const description = test.Description;
|
||||
let html = '';
|
||||
html += '<p><b>Description:</b> ' + description + '</p>';
|
||||
html += '<p><b>Severity:</b> <span class=""' + severity + '"">' + severity + '</span></p>';
|
||||
html += '<p><b>Config:</b> <pre>' + (typeof config === 'object' ? JSON.stringify(config, null, 2) : config) + '</pre></p>';
|
||||
html += '<p><b>Is Healthy:</b> ' + '<span class=""' +(r.IsHealthy ? 'healthy' : 'unhealthy')+ '"">' + (r.IsHealthy ? 'Yes' : 'No') + '</span></p>';
|
||||
html += '<p><b>Last execution details:</b> <pre>' + r.Details + '</pre><p>';
|
||||
document.getElementById('modalTitle').innerText = r.Name;
|
||||
document.getElementById('modalBody').innerHTML = html;
|
||||
document.getElementById('modal').style.display = 'block';
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
window.onclick = function(event) {{
|
||||
var modal = document.getElementById('modal');
|
||||
if (event.target == modal) {{
|
||||
closeModal();
|
||||
}}
|
||||
}};
|
||||
}});
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
return html;
|
||||
}
|
||||
|
||||
public class ColorInterpolation
|
||||
{
|
||||
// Convierte un código hex a Color
|
||||
private static Color HexToColor(string hex)
|
||||
{
|
||||
return ColorTranslator.FromHtml(hex);
|
||||
}
|
||||
|
||||
// Convierte Color a código hex
|
||||
private static string ColorToHex(Color color)
|
||||
{
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
}
|
||||
|
||||
// Interpola entre dos colores según factor (0 a 1)
|
||||
private static Color InterpolateColor(Color start, Color end, double factor)
|
||||
{
|
||||
int r = (int)(start.R + factor * (end.R - start.R));
|
||||
int g = (int)(start.G + factor * (end.G - start.G));
|
||||
int b = (int)(start.B + factor * (end.B - start.B));
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
|
||||
// Genera una lista de colores interpolados para n valores
|
||||
public static List<string> GenerateColorGradient(int n)
|
||||
{
|
||||
var startColor = HexToColor("#4caf50"); // verde
|
||||
var midColor = HexToColor("#e5be01"); // amarillo
|
||||
var endColor = HexToColor("#f44336"); // rojo
|
||||
|
||||
var colors = new List<string>(n);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double factor = (double)i / (n - 1);
|
||||
if (factor <= 0.5)
|
||||
{
|
||||
double localFactor = factor / 0.5;
|
||||
var c = InterpolateColor(startColor, midColor, localFactor);
|
||||
colors.Add(ColorToHex(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
double localFactor = (factor - 0.5) / 0.5;
|
||||
var c = InterpolateColor(midColor, endColor, localFactor);
|
||||
colors.Add(ColorToHex(c));
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user