-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support ASP.NET Core healthchecks #29
Comments
Thank you, Kirill! Appreciate the suggestion. We should definitely support them, unless there's a chance they may change their mind... (one can only hope). |
(author here) We don't really specify a format, users are free to configure the health checks endpoint to return whatever they like. |
Actually, I wrote an implementation for ASP.NET Core. I had to remove some internals, so the code might not run but give you an idea, how one could implement this. If this RFC has a future, I could create a library for that. using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Serilog;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
namespace Common.Api.Monitoring;
/// <summary>
/// Writes the response for HealthChecks.
/// It formats them according to https://inadarei.github.io/rfc-healthcheck version 16 October 2021.
/// </summary>
public class RfcDynmonHealthCheckResponseWriter
{
private static readonly ILogger _logger = Log.ForContext<RfcDynmonHealthCheckResponseWriter>();
public string ReleaseId
{
get => _releaseIdEncodedText?.ToString();
init => _releaseIdEncodedText = value == null ? null : JsonEncodedText.Encode(value);
}
private readonly JsonEncodedText? _releaseIdEncodedText;
// see https://inadarei.github.io/rfc-healthcheck
private static readonly JsonEncodedText STATUS_PASS = JsonEncodedText.Encode("pass"); // UP in Spring
private static readonly JsonEncodedText STATUS_FAIL = JsonEncodedText.Encode("fail"); // DOWN in Spring
private static readonly JsonEncodedText STATUS_WARN = JsonEncodedText.Encode("warn"); // healthy with some concerns
private static JsonEncodedText MapHealthStatusToRfcHealthCheck(HealthStatus status)
{
return status switch
{
HealthStatus.Healthy => STATUS_PASS,
HealthStatus.Unhealthy => STATUS_FAIL,
HealthStatus.Degraded => STATUS_WARN,
_ => throw new ArgumentException(nameof(status))
};
}
private static string MapHealthStatusToDynmonStatus(HealthStatus status)
{
return status switch
{
HealthStatus.Healthy => "0",
HealthStatus.Unhealthy => "1",
HealthStatus.Degraded => "0",
_ => throw new ArgumentException(nameof(status))
};
}
private static readonly JsonEncodedText PROPERTY_STATUS = JsonEncodedText.Encode("status");
private static readonly JsonEncodedText PROPERTY_RELEASEID = JsonEncodedText.Encode("releaseId");
private static readonly JsonEncodedText PROPERTY_DURATION = JsonEncodedText.Encode("duration");
private static readonly JsonEncodedText PROPERTY_OUTPUT = JsonEncodedText.Encode("output");
private static readonly JsonEncodedText PROPERTY_CHECKS = JsonEncodedText.Encode("checks");
public async Task WriteStatus(Stream stream, HealthReport report)
{
var writer = new Utf8JsonWriter(stream);
await using var _ = writer.ConfigureAwait(false);
writer.WriteStartObject();
writer.WriteString(PROPERTY_STATUS, MapHealthStatusToRfcHealthCheck(report.Status));
if (_releaseIdEncodedText.HasValue)
{
// TODO(monitoring): how to write that to dynmon?
writer.WriteString(PROPERTY_RELEASEID, _releaseIdEncodedText.Value);
}
// NOTE: duration is not in the RFC and additional keys on root object are not defined
writer.WriteString(PROPERTY_DURATION, report.TotalDuration.ToString("c"));
writer.WritePropertyName(PROPERTY_CHECKS);
writer.WriteStartObject();
foreach (var (key, entry) in report.Entries)
{
writer.WritePropertyName(key);
writer.WriteStartArray();
writer.WriteStartObject();
writer.WriteString(PROPERTY_STATUS, MapHealthStatusToRfcHealthCheck(entry.Status));
// NOTE: additional key not defined in RFC
writer.WriteString(PROPERTY_DURATION, entry.Duration.ToString("c"));
if (entry.Description is not null)
{
writer.WriteString(PROPERTY_OUTPUT, entry.Description);
}
writer.WriteEndObject();
writer.WriteEndArray();
}
writer.WriteEndObject();
writer.WriteEndObject();
await writer.FlushAsync();
}
/// <summary>
/// Writes the response for the health check call.
/// This method is set as a delegate to
/// <see cref="Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions.ResponseWriter"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> the response is written to.</param>
/// <param name="report">The <see cref="HealthReport"/> from which the the body is created.</param>
public async Task WriteStatusToHttpContext(HttpContext context, HealthReport report)
{
_logger.Debug("Write response for HealthReport {@HealthReport}", report);
context.Response.ContentType = "application/json";
await WriteStatus(context.Response.Body, report);
}
} And the service creation looks like: public static IHealthChecksBuilder AddHealthChecksConfigured (this IServiceCollection services,
IConfiguration configuration, Action<IHealthChecksBuilder, TimeSpan> addFn)
{
Require.IsNotNull(services, nameof(services));
if (!SystemConfiguration.LogEnabled(SystemConfiguration.From(configuration).HealthChecksEnabled))
{
return new StubHealthChecksBuilder(services);
}
// TODO: create generic method for options validation
HealthCheckConfiguration options = new();
configuration.GetSection(nameof(HealthCheckConfiguration)).Bind(options);
var validationResult =
new DataAnnotationValidateOptions<HealthCheckConfiguration>(Options.DefaultName).Validate(Options.DefaultName, options);
if (!validationResult.Succeeded)
{
throw new OptionsValidationException(Options.DefaultName, typeof(HealthCheckConfiguration),
validationResult.Failures);
}
var timeout = TimeSpan.FromSeconds(options.CheckTimeoutSeconds);
_logger.Information($"{nameof(HealthCheckConfiguration)}: {{@Options}}", options);
services.AddSingleton<RfcDynmonHealthCheckResponseWriter>(provider =>
{
var versionProvider = provider.GetRequiredService<IVersionProvider>();
var instance = new RfcDynmonHealthCheckResponseWriter()
{
ReleaseId = versionProvider.ExecutionAssemblyVersion,
};
return instance;
});
var builder = services.AddHealthChecks();
addFn(builder, timeout);
// see https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-6.0#health-check-publisher
builder.Services.Configure<HealthCheckPublisherOptions>(publisherOptions =>
{
publisherOptions.Delay = TimeSpan.FromSeconds(options.PublisherDelaySeconds);
publisherOptions.Period = TimeSpan.FromSeconds(options.PublisherPeriodSeconds);
publisherOptions.Timeout = TimeSpan.FromSeconds(options.PublisherTimeoutSeconds);
});
// this adds an IHealthCheckPublisher and thus also the periodical check
if (options.EnableMetricsForwarding)
{
builder.ForwardToPrometheus();
}
return builder;
} |
Hi @inadarei,
Folks from Microsoft are developing their own solution for health-checks (see https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/monitor-app-health).
But there is a problem that their format of the status filed isn't compatible (as usual :-) with this RFC draft. They use "healthy/unhealthy" values for statuses. It would be nice to support these values as optional too.
Thank you.
The text was updated successfully, but these errors were encountered: