Skip to content
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

Open
kirillk77 opened this issue Nov 15, 2018 · 3 comments
Open

Support ASP.NET Core healthchecks #29

kirillk77 opened this issue Nov 15, 2018 · 3 comments

Comments

@kirillk77
Copy link

kirillk77 commented Nov 15, 2018

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.

@inadarei
Copy link
Owner

inadarei commented Nov 16, 2018

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).

@rynowak
Copy link

rynowak commented Feb 19, 2019

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.

(author here) We don't really specify a format, users are free to configure the health checks endpoint to return whatever they like.

@delixfe
Copy link

delixfe commented Nov 30, 2022

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;
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants