Nas minhas aventuras pelos estudos no mundo da arquitetura de serviços distribuídos, eu sempre notei que autores enfatizam a necessidade de saber quando parar de “consumir” um serviço que está indisponível. Isso é saudável quando o motivo da indisponibilidade do serviço é justamente o excesso de requisições, não é? Neste artigo eu trago uma forma simples de implementar a interface IHealthCheck para que nossas API’s possam fornecer de uma forma rápida e objetiva os meios para que as aplicações client saibam quando algum recurso está indisponível. Eu aprendi sobre este recurso no último evento no qual eu estive presente aqui em Campinas, São Paulo. O evento foi o Microsoft Connect(); 2019 e contou com a presença de alguns MVPs de calibre da Micro<3soft.
Criando o projeto
Atualmente eu estou na versão 2.2.102 do .NET Core, mas provavelmente estes comandos irão continuar nas versões futuras.
Aqui eu invoquei o meu terminal e criei um diretório chamado healthcheck, naveguei para a raiz e rodei o comando que cria um projeto padrão do tipo webapi:
1 2 3 4 |
thalesreis@trntb:~/dev/dotnet$ mkdir healthcheck thalesreis@trntb:~/dev/dotnet$ cd healthcheck/ thalesreis@trntb:~/dev/dotnet/healthcheck$ dotnet new webapi The template "ASP.NET Core Web API" was created successfully. |
A ideia aqui é criar duas classes que implementam a interface IHealthCheck e simular uma cenário onde um dos recursos irá falhar e outro terá um retorno Ok. Com isso poderemos observar a flexibilidade e a facilidade deste recurso. Estou o Visual Studio Code como editor de código padrão. Com alguns plugins e pouca configuração, é possível ter todos os recursos que precisamos para poder “codar” C# em qualquer sistema operacional.
A classe CheckDatabaseHealthStatus
Na raiz do projeto eu criei uma nova classe com nome de CheckDatabaseHealthStatus e declarei que ela implementa a interface IHealthCheck.
No fim, o arquivo CheckDatabaseHealthStatus.cs deve ficar assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace healthcheck { public class CheckDatabaseHealthStatus : IHealthCheck { public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { //Variável boolean para receber o resultado das verificações bool healthStatus = true; /* Essa váriavél é do tipo Dictionary onde podemos criar um relação de chave e valor para podermos retornar informações úteis para quem quer verificar o status da nossa API */ var infoData = new Dictionary<string, object>() { { "tabelas_criadas", "true" }, { "info_db", new { version = 1.00, info_size = 1024, any_value = "anything" } }, { "last_activity", DateTime.Now } }; /* Aqui é onde é feito o retorno da verificação. Se tudo "correu" bem nas verificações o valor de "healthStatus" será true */ if (healthStatus) { return Task.FromResult( HealthCheckResult.Healthy("Tudo parece OK com o banco de dados.", infoData)); } //Quando healthStatus = false return Task.FromResult( HealthCheckResult.Unhealthy("Serviço de banco de dados indisponível.")); } } } |
A variável healthStatus é onde vamos “jogar” todo o resultado das verificações que precisamos fazer para saber se este recurso específico está “em pé” ou não. Neste exemplo eu simplesmente setei ela para false, mas em uma aplicação real seria necessário a implementação de algo um pouco mais complexo, como uma consulta de testes ao banco de dados, por exemplo.
É na variável infoData onde vamos “anexar” as mais diversas informações que o usuário possa vir a precisar no momento da consulta. Ela segue um modelo de chave e valor que facilita bastante na hora de popular os valores.
A classe CheckWebserviceHealthStatus
Na raiz do projeto eu criei uma nova classe com o nome de CheckWebserviceHealthStatus e declarei que ela implementa a interface IHealthCheck.
No fim, o arquivoCheckWebserviceHealthStatus.cs deve ficar assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace healthcheck { public class CheckWebserviceHealthStatus : IHealthCheck { public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { //Variável boolean para receber o resultado das verificações bool healthStatus = true; try { throw new Exception("Falha na conexão com o webservice de consulta de valores."); if (healthStatus) { return Task.FromResult( HealthCheckResult.Healthy("Tudo parece OK com o webservice de consulta de valores")); } } catch (System.Exception ex) { var infoData = new Dictionary<string, object>() { { "info", new { Source = ex.Source, Stack = ex.StackTrace } }, { "tries", 5 }, { "last_activity", DateTime.Now.AddDays(-10) } }; return Task.FromResult( HealthCheckResult.Unhealthy(e.Message, ex, infoData)); } } } } |
Para a CheckDatabaseHealthStatus, aproveitei algumas coisas da nossa primeira classe. O que realmente precisamos observar aqui, é onde eu causo uma exceção proposital para poder ilustrar o nosso exemplo. Dentro bloco catch, acesso a exceção via variável ex, capturo as informações úteis e as utilizo para popular as chaves na variável infoData. Com a execução do programa desviada para o bloco catch, o retorno da função será um HealthCheckResult.Unhealthy, indicando que este recurso está com algum problema.
Até agora…
Bom, até agora nós criamos duas classes que implementam a interface IHealthCheck. O método CheckHealthAsync originado da interface, retorna um HealthCheckResult que atualmente implementa três opções de retorno:
- HealthStatus.Healthy – Saudável “em pé, tudo ok, sucesso total”
- HealthStatus.Degraded – Degradado “capenga, alta latência ou pouco recurso disponível”
- HealthStatus.Unhealthy – Indisponível “totalmente quebrada, fora do ar, offline”
Podemos variar os tipos retornados conforme a realidade da disponibilidade do recurso.
Registrando o HealthCheckService
Dentro do arquivo Startup.cs é onde vamos registrar o serviço para que o HealthCheck seja executado na aplicação. O método onde vamos registrar o serviço é o ConfigureServices.
1 2 3 4 5 6 7 |
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks().AddCheck<CheckDatabaseHealthStatus>("Databases"); services.AddHealthChecks().AddCheck<CheckWebserviceHealthStatus>("Webservices"); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } |
Observe que as classes passadas como parâmetro no AddCheck são as classes que implementamos anteriormente. As strings “Databases” e “Webservices” são apenas identificadores nominais que serão utilizados posteriormente. Poderíamos chamar de “cenoura” e “beterraba” que não teria problema algum.
Configurando o HealthCheckService
Ainda dentro do arquivo Startup.cs, vamos “dizer” para a nossa aplicação que ela deve usar o HealthCheckService e em qual URL ela irá responder com as informações dos status de cada recurso. O método a ser configurado é o Configure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHealthChecks("/health", new HealthCheckOptions() { ResponseWriter = WriteResponse }); app.UseMvc(); } private static Task WriteResponse(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "application/json"; var json = new JObject( new JProperty("status", result.Status.ToString()), new JProperty("results", new JObject(result.Entries.Select(pair => new JProperty(pair.Key, new JObject( new JProperty("status", pair.Value.Status.ToString()), new JProperty("description", pair.Value.Description), new JProperty("data", new JObject(pair.Value.Data.Select( p => new JProperty(p.Key, p.Value.ToString())))))))))); return httpContext.Response.WriteAsync( json.ToString(Formatting.Indented)); } |
O app.UseHealthChecks aceita dois parâmetros. O primeiro é uma string onde configuramos a rota para acessar as informações e o segundo é um objeto do tipo HealthCheckOptions. O segundo não é obrigatório, mas é interessante para podermos customizar o retorno dos nossos status. Neste exemplo eu optei por implementar o segundo parâmetro, mas sem ele a declaração de uso é ainda mais simples:
1 |
app.UseHealthChecks("/health"); |
O método WriteResponse também implementado no Startup.cs é responsável pelo nosso “output” personalizado. Reparem que eu atribuo o método WriteResponse para a propriedade ResponseWriter do segundo parâmetro e com isso nós ganhamos a capacidade de manipular o conteúdo retornado.
Implementar um ResponseWriter não é obrigatório, pois o HealthCheckService já entrega um resultado padrão de Healthy para resultados positivos e Unhealthy para negativos.
1 |
Healthy |
1 |
Unhealthy |
Check-list
Até agora temos:
- Implementar a interface IHealthCheck nas classes de verificação
- Registrar o serviço de HealthCheck no método ConfigureServices
- Configurar o uso do HealthCheck com o app.UseHealthChecks no método Configure
Testando
O teste é a parte mais simples. O esperado aqui é que a aplicação devolva um status geral de falha, pois uma das nossas classes irá falhar propositalmente. O serviço de HealthCheck acumula os retornos dos métodos implementados em cada classe e devolve uma espécie de “catado lógico”, onde se pelo menos um único recurso está “unhealthy”, o “status geral” da aplicação toda encontra-se com problema. A vantagem é que ainda temos a informação sobre os recursos que estão “healthy”, permitindo que seja lá quem for consultar os status dos recursos, consiga identificar qual recurso ainda está em “pé” e desativar o consumo dos problemáticos, e manter ativo os recursos que estão em perfeito funcionamento.
Executei o servidor web embutido com o comando:
1 |
thalesreis@trntb:~/dev/dotnet/healthcheck$ dotnet run |
Acessei a URL http://localhost:5000/health no browser e obtive o seguinte resultado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "status": "Unhealthy", "results": { "Databases": { "status": "Healthy", "description": "Tudo parece OK com o banco de dados.", "data": { "tabelas_criadas": "true", "info_db": "{ version = 1, info_size = 1024, any_value = anything }", "last_activity": "1/23/19 8:17:42 PM" } }, "Webservices": { "status": "Unhealthy", "description": "Falha na conexão com o webservice de consulta de valores.", "data": { "info": "{ Source = healthcheck, Stack = at healthcheck.CheckWebserviceHealthStatus.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) in /home/thalesreis/dev/dotnet/healthcheck/CheckWebserviceHealthStatus.cs:line 18 }", "tries": "5", "last_activity": "1/13/19 8:17:42 PM" } } } } |
Notem a chave “status” de cada item da lista de resultados. Cada um manteve o seu status de forma individual, porém, como um deles falhou, o status geral retornado pela “página” foi de falha.
Http Codes
Um detalhe bem importante é considerar o Status Code de retorno do cabeçalho Http. Aplicações de modelo Restful dão (deveriam dar) bastante importância para os códigos retornados nas transações efetuadas. Em caso de falha geral, o serviço HealthCheck altera o cabeçalho do response e devolve com o código 503 – Service Unavailable e quando todos os recursos estão ok, naturalmente o retorno é o código 200 – OK.

Fim e referências
Bom pessoal, é isso. Aqui está uma forma simples e objetiva de publicarmos os status dos recursos disponíveis em nossa API.
Para escrever este humilde artigo eu busquei referências em alguns sites. Resolvi deixar as fontes originais aqui caso eu não tenha sido muito claro em alguma parte específica.
Documentação oficial do HealthCheck:
https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-2.2
Criando um objeto anônimo em C#
https://docs.microsoft.com/pt-br/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types
Tabela com os Http Codes
Projeto avançado de verificação de HealthChek
https://github.com/Xabaril/BeatPulse
GitHub com os fontes criados