Como implementar Health Checks para sua aplicação usando as extensões do ASP.NET Core 3.0

O ASP.NET Core 3.0 fornece uma forma bastante prática de implementar um endpoint para checar a saúde de uma aplicação.

Um endpoint de Health Check bem implementado pode nos ajudar de inúmeras formas a manter uma aplicação rodando. Podemos usá-los em conjunto com o liveness probes do Kubernetes, por exemplo, para que ele possa verificar a saúde de um serviço e assim poder reiniciá-lo caso as coisas dêem errado. Ferramentas de monitoramento também podem fazer uso desses endpoints para gerar alertas e estatísticas.

O pacote Microsoft.Extensions.Diagnostics.HealthChecks traz uma abstração simples de implementar e colocar para funcionar na aplicação. Trata-se da interface IHealthCheck.

public class RecursoImportanteHealthCheck : IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
    {
        // checagem da saúde do recurso
    }
    //...
}

Como podemos ver no snippet acima, o método CheckHealthAsync recebe um objeto que representa o contexto da checagem de saúde da aplicação, do tipo HealthCheckContext, e espera um resultado do tipo HealthCheckResult.

Vamos nos ater primeiro ao retorno, que será mais comumente usado. Existem três status possíveis para a checagem:

  • Healthy
  • Degraded
  • Unhealthy

O status Healthy dispensa explicações, já que tudo está OK. Já o status Unhealthy indica uma falha e pode inda detalhar a natureza dos erros identificados:

HealthCheckResult.Unhealthy(
    description: "detalhamento do problema",
    exception: e,
    data: new Dictionary<string, object>() {
        {"chave1", "valor1"},
        {"chave2", "valor2"},
    }
);

Como todos os argumentos são opcionais, podemos montar uma resposta dessas simplesmente assim:

HealthCheckResult.Unhealthy();

O status Degraded funciona exatamente da mesma forma. A diferença está no seu significado: o recurso está respondendo, mas a qualidade do serviço está abaixo do esperado, está degradada.

Implementando um IHealthCheck

Vamos tomar por exemplo uma integração com um serviço externo do qual nossa aplicação depende. A implementação de uma validação dessas pode ser bastante simples, como esta a seguir, que toma a decisão com base numa requisição GET ao serviço externo:

public class ServicoExternoHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
    {
        var result = await new HttpClient().GetAsync("http://servicoX.com/api", cancellationToken);
        if (result.IsSuccessStatusCode)
        {
            return await Task.FromResult(HealthCheckResult.Healthy());
        }
        return await Task.FromResult(HealthCheckResult.Unhealthy());
    }
}

Um caso de uso mais elaborado poderia decidir se a saúde do serviço está degradada de acordo com o tempo de resposta. Para isso, podemos utilizar a classe Stopwatch como timer e assim tomar uma decisão:

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
    try
    {
        var stopwatch = Stopwatch.StartNew();
        var resultado = await new HttpClient().GetAsync("http://servicoX.com/api", cancellationToken);
        stopwatch.Stop();

        var healthCheckResult = (resultado.IsSuccessStatusCode, stopwatch.Elapsed) switch
        {
            (true, var duracao) when duracao <= TimeSpan.FromSeconds(5) => HealthCheckResult.Healthy(),
            (true, _) => HealthCheckResult.Degraded(),
            (false, _) => HealthCheckResult.Unhealthy(),
        };
        return await Task.FromResult(healthCheckResult);
    }
    catch (Exception excecao)
    {
        return await Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, exception: excecao));
    }
}

O exemplo acima e bem próximo de uma implementação que fiz no trabalho e possui alguns detalhes interessantes para analisarmos. O primeiro ponto é o uso da sintaxe de pattern matching do C# 8 para selecionar o resultado da checagem com base em dois parâmetros: 1) o sucesso ou insucesso na consulta ao serviço externo, e 2) o tempo de resposta do serviço.

var healthCheckResult = (result.IsSuccessStatusCode, stopwatch.Elapsed) switch
{
    (true, var tempoResposta) when tempoResposta <= TimeSpan.FromSeconds(5) => HealthCheckResult.Healthy(),
    (true, _) => HealthCheckResult.Degraded(),
    (false, _) => HealthCheckResult.Unhealthy(),
};

Esta é uma forma bem compacta de tratar todos os casos. Gosto dessa sintaxe porque fica muito fácil de identificar as combinações de entradas que levam a cada saída. Num post futuro, abordarei o pattern matching do C# em mais detalhes.

Seguindo no exemplo, se obtivermos sucesso na requisição (o primeiro parâmetro de cada tupla) e o tempo de resposta for de até 5 segundos, então consideramos que o serviço está saudável e retornamos HealthCheckResult.Healthy().

Já caso obtivermos sucesso, mas o tempo de resposta for superior a 5 segundos, então consideramos que está lento demais e retornamos HealthCheckResult.Degraded().

Se, no entanto, o status code retornado não for de sucesso, retornamos que não está saudável com HealthCheckResult.Unhealthy().

Além disso, temos um tratamento de exceção no código completo mais acima. Ele é totalmente opcional. Caso a checagem atire uma exceção, a API saberá que o serviço está Unhealthy e vai retornar uma resposta de acordo.

Ainda assim, o HealthCheckResult é construído de uma forma um pouco diferente aqui, usando o próprio construtor da classe:

try
{
    // ...
}
catch (Exception excecao)
{
    return await Task.FromResult(new HealthCheckResult(context.Registration.FailureStatus, exception: excecao));
}

Note que o primeiro argumento que passamos é o status de falha do contexto. A propriedade FailureStatus terá o valor padrão Unhealthy, a não ser que informado outro valor ao registrar este HealthCheck. E esta é uma oportunidade para explorarmos como é feito o registro do ServicoExternoHealthCheck.

Registrando um endpoint de Health Check

A configuração dos health checks é bastante simples e é feita em duas etapas. A primeira trata de registrar nossas implementações da interface IHealthCheck. Isto é feito no método ConfigureServices da classe Startup.

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
        .AddCheck<ServicoExternoHealthCheck>("ServicoTerceiro");
    // ...
}

Neste momento, podemos fazer algumas configurações adicionais para personalizar nosso health check. Por exemplo, podemos definir qual será o status quando ocorrer uma falha na verificação, como vimos um pouco acima.

services.AddHealthChecks()
    .AddCheck<ServicoExternoHealthCheck>("ServicoTerceiro", failureStatus: HealthStatus.Unhealthy);

Outra opção é passar uma coleção de tags para serem utilizadas durante a verificação de saúde do serviço:

services.AddHealthChecks()
    .AddCheck<ServicoExternoHealthCheck>("teste", tags: new [] {"feature1", "feature2"});

Por fim, na segunda etapa da configuração, precisamos associar um endpoint para consulta. Isto é feito no método Configure, ainda na classe Startup:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        // ...
        endpoints.MapHealthChecks("/health");
    });
    // ...
}

E estamos prontos para testar! Basta subir a aplicação e fazer uma requisição GET no endpoint /health. No caso de o status ser Healthy, note que a resposta tem Status HTTP 200 (OK):

Chamada do endpoint /health com status 200 e corpo contendo a palavra Healthy

Para o Degraded é a mesma coisa:

Chamada do endpoint /health com status 200 e corpo contendo a palavra Degraded

Já quando o serviço retorna Unhealthy, o Status HTTP retornado é o 503 (Serviço Indisponível):

Chamada do endpoint /health com status 503 e corpo contendo a palavra Unhealthy

Como modificar o comportamento do Health Check

Ao registrar o endpoint de Health Check, também é possível informar um objeto do tipo HealthCheckOptions.

Uma das opções mais interessantes que ele fornece é poder mudar o HTTP Status Code retornado para cada um dos três status de saúde (Healthy, Degraded e Unhealthy).

Se quisermos retornar um status 599 para Unhealthy, por exemplo, basta fazer assim:

app.UseEndpoints(endpoints =>
{
    var healthCheckOptions = new HealthCheckOptions();
    healthCheckOptions.ResultStatusCodes[HealthStatus.Unhealthy] = 599;
    endpoints.MapHealthChecks("/health", healthCheckOptions);
});

Outra opção, é fornecer um filtro que decide dinamicamente quais Health Checks serão executados:

app.UseEndpoints(endpoints =>
{
    var healthCheckOptions = new HealthCheckOptions();
    // este exemplo não é lá muito realista :)
    healthCheckOptions.Predicate = registration => registration.Name.StartsWith("M");
    endpoints.MapHealthChecks("/health", healthCheckOptions);
});

Também podemos customizar a resposta. Abaixo, temos um exemplo de configuração para retornar um JSON, mas poderíamos facilmente montar uma página de relatório HTML:

app.UseEndpoints(endpoints =>
{
    var healthCheckOptions = new HealthCheckOptions();
    healthCheckOptions.ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var result = JsonConvert.SerializeObject(new
        {
            status = report.Status.ToString(),
            errors = report.Entries
                .Select(e => new
                {
                    key = e.Key,
                    value = e.Value.Status.ToString()
                })
        });
        await context.Response.WriteAsync(result);
    };
    endpoints.MapHealthChecks("/health", healthCheckOptions);
});

Felizmente, a biblioteca AspNetCore.HealthChecks.UI já faz o trabalho de gerar um dashboard de acompanhamento. Não vou entrar em detalhes da configuração dessa ferramenta neste artigo, mas é bem simples.

Dashboard do AspNetCore.HealthChecks.UI

E é isso. Se você conhece outras formas de implementar um endpoint de health check, compartilhe nos comentários. E se este post foi útil pra você, deixe um comentário ou compartilhe nas redes sociais!

Outras opções para compartilhar:
comments powered by Disqus