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):
Para o Degraded é a mesma coisa:
Já quando o serviço retorna Unhealthy, o Status HTTP retornado é o 503 (Serviço Indisponível):
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.
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!