ASP.NET Core + Health Checks: implementando rapidamente uma solução de monitoramento


O monitoramento em aplicações web é uma atividade de extrema importância, já que fornece meios para o diagnóstico e a resolução de problemas, por vezes, intermitentes. Essa prática contribui, ainda, para um melhor entendimento acerca de como uma solução é acessada, trazendo informações úteis em decisões envolvendo a alocação de recursos, visando assegurar uma alta disponibilidade.

 

Além das próprias aplicações sujeitas à verificação, é fundamental que dependências das mesmas sejam monitoradas. Isso inclui servidores de bancos de dados, APIs REST consumidas, brokers de mensageria, recursos como servidores FTP e SMTP, serviços na nuvem, entre outros elementos.

 

Levando em conta tudo isso, como o ASP.NET Core poderia nos ajudar? Desde a versão 2.2, essa plataforma conta com Health Checks, um mecanismo de fácil configuração e que pode ser empregado no monitoramento de diferentes tipos de recursos.

 

É possível habilitarmos Health Checks tanto em um projeto que estaremos monitorando quanto desenvolvermos separadamente uma solução que possibilitará o acompanhamento de elementos compartilhados por várias aplicações.

 

O projeto open source Xabaril/AspNetCore.Diagnostics.HealthChecks conta com implementações de Health Checks para o monitoramento de diferentes recursos, economizando tempo e permitindo assim a criação de uma solução bastante completa. Entre as tecnologias suportadas por essa iniciatival, estão: SQL Server, Oracle, PostgreSQL, MySQL, MongoDB, Elasticsearch, SignalR, RabbitMQ, Kafka, Kubernetes, diversos serviços do Microsoft Azure e AWS, recursos como servidores FTP e SMTP, entre outras alternativas comumente empregadas por aplicações web.

 

Para conhecer mais sobre o projeto Xabaril/AspNetCore.Diagnostics.HealthChecks, acesse.

 

Já abordei a utilização de Health Checks com ASP.NET Core nos seguintes artigos:

 

 

 

Neste novo artigo, demonstrarei como implementar sem muitos esforços uma solução de monitoramento baseada no ASP.NET Core, fazendo uso, para isso, de packages disponibilizados pelo projeto Xabaril/AspNetCore.Diagnostics.HealthChecks e verificando:

  • Servidores do SQL Server 2017 e 2019, MongoDB, Redis, PostgreSQL, MySQL e DocumentDB/Cosmos DB;
  • A disponibilidade de 2 APIs REST;
  • Um broker do RabbitMQ;
  • Uma fila criada com o Azure Service Bus (serviço de mensageria oferecido pelo Microsoft Azure);
  • Uma conta de armazenamento do Azure Storage.

 

Aproveito este espaço e o grande interesse por Docker também para um convite: tem interesse em conhecer mais sobre Docker? Que tal então fazer um curso completo, cobrindo desde fundamentos a diferentes possibilidades de uso de contêineres com tecnologias em alta no mercado? Adquira conhecimentos profundos sobre Docker, evolua e se diferencie no mercado, seja você um profissional DevOps, um desenvolvedor ou um arquiteto de software. Acompanhe o portal Docker Definitivo para ficar por dentro de novidades a serem anunciadas em breve! https://dockerdefinitivo.com.

 

Criando e configurando o projeto de monitoramento

 

Para a implementação da aplicação descrita neste artigo, será utilizado o template ASP.NET Core Empty, com a criação do projeto SiteMonitoramento por meio do seguinte comando:

dotnet new empty -n SiteMonitoramento

 

A escolha por esse template se deve ao fato de que não será necessária a criação de controllers, views ou páginas para a visualização dos status dos recursos que passarão por monitoramento. Os próprios packages que integram o projeto Xabaril/AspNetCore.Diagnostics.HealthChecks permitem a geração de um dashboard para visualização dos diferentes Health Checks e de seus respectivos estados, dispensando, assim, a implementação de artefatos para a visualização dessas informações.

 

Foram adicionados ao projeto SiteMonitoramento, via instrução dotnet add package, os seguintes pacotes:

 

  • HealthChecks.AzureServiceBus;
  • HealthChecks.AzureStorage;
  • HealthChecks.DocumentDb;
  • HealthChecks.MongoDb;
  • HealthChecks.MySql;
  • HealthChecks.Npgsql;
  • HealthChecks.RabbitMQ;
  • HealthChecks.Redis;
  • HealthChecks.SqlServer;
  • HealthChecks.UI (necessário para a geração do dashboard de visualização dos Health Checks);
  • HealthChecks.UI.Client (também utilizado para a montagem do dashboard mencionado no package anterior);
  • HealthChecks.Uris (utilizado para o envio de requisições HTTP a fim de determinar se um site/API REST está no ar).

 

Implementação da solução

 

No arquivo appsettings.json, estará a seção Dependencies, que conterá um array com as diferentes dependências a serem monitoradas. Na seção HealthChecks-UI, ficarão as configurações empregadas na geração do dashboard de monitoramento (a URL aqui presente é um endpoint com dados no formato JSON, contendo o status dos diferentes Health Checks):

{
  "Dependencies": [
    {
      "Name": "sqlserver-baseIndicadores",
      "ConnectionString": "CONNECTION STRING"
    },
    {
      "Name": "sqlserver-baseCotacoes",
      "ConnectionString": "Data Source=localhost,31433;Initial Catalog=BaseCotacoes;User Id=sa;Password=SqlServer2017!;"
    },
    {
      "Name": "mongodb-local",
      "ConnectionString": "mongodb://root:MongoDB2019!@localhost:37017"
    },
    {
      "Name": "redis-azure",
      "ConnectionString": "CONNECTION STRING"
    },
    {
      "Name": "url-testeapi01",
      "Url": "http://localhost:1111/status"
    },
    {
      "Name": "url-testeapi02",
      "Url": "http://localhost:1112/status"
    },
    {
      "Name": "documentdb-local",
      "UriEndpoint": "https://localhost:8081",
      "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
    },
    {
      "Name": "postgres-local",
      "ConnectionString": "Server=localhost;Port=15432;Database=postgres;Port=15432;User Id=postgres;Password=Postgres2019!;"
    },
    {
      "Name": "mysql-local",
      "ConnectionString": "Server=localhost; Port=3306; Database=testedb; Uid=root; Pwd=MySql2019!;"
    },
    {
      "Name": "rabbitmq-local",
      "ConnectionString": "amqp://testes:RabbitMQ2019!@localhost:5672/"
    },
    {
      "Name": "azureservicebusqueue-filaExemplo",
      "ConnectionString": "CONNECTION STRING",
      "QueueName": "FilaExemplo"
    },
    {
      "Name": "azureblobstorage-groffestorage",
      "ConnectionString": "CONNECTION STRING"
    }
  ],
  "HealthChecks-UI": {
    "HealthChecks": [
      {
        "Name": "Infraestrutura",
        "Uri": "https://localhost:5001/healthchecks-data-ui"
      }
    ]
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Já a classe Dependency será utilizada para o carregamento das configurações declaradas em appsettings.json, contando com propriedades que podem se referir à conexão com um banco de dados (ConnectionString), ao endereço de um site ou API REST (Url), ao endpoint e à chave primária de acesso a uma conta do Azure Cosmos DB/Document (UriEndpoint e PrimaryKey), além do nome de uma fila vinculada a um recurso do Azure Service Bus (QueueName):

namespace SiteMonitoramento.Models
{
    public class Dependency
    {
        public string Name { get; set; }
        public string ConnectionString { get; set; }
        public string Url { get; set; }
        public string UriEndpoint { get; set; }
        public string PrimaryKey { get; set; }
        public string QueueName { get; set; }
    }
}

Na classe DependenciesExtensions, foi definido o método estático AddDependencies. Trata-se de uma extensão a ser empregada com instâncias do tipo IHealthChecksBuilder (namespace Microsoft.Extensions.DependencyInjection), o qual armazena as configurações de Health Checks especificados para uma aplicação:

  • Um dos parâmetros recebidos por essa operação é uma lista de objetos do tipo Dependencies;
  • Essa coleção será utilizada na configuração do objeto IHealthChecksBuilder, com diferentes chamadas a métodos cujo nome se inicia por Add (AddSqlServer e AddMongoDb, por exemplo) utilizando definições, como strings de conexão armazenadas em instâncias da classe Dependencies.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using SiteMonitoramento.Models;

namespace SiteMonitoramento.Extensions
{
    public static class DependenciesExtensions
    {
        public static IHealthChecksBuilder AddDependencies(
            this IHealthChecksBuilder builder,
            List<Dependency> dependencies)
        {
            foreach (var dependencia in dependencies)
            {
                string nomeDependencia = dependencia.Name.ToLower();

                if (nomeDependencia.StartsWith("sqlserver-"))
                    builder = builder.AddSqlServer(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("mongodb-"))
                    builder = builder.AddMongoDb(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("redis-"))
                    builder = builder.AddRedis(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("postgres-"))
                    builder = builder.AddNpgSql(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("mysql-"))
                    builder = builder.AddMySql(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("url-"))
                    builder = builder.AddUrlGroup(new Uri(dependencia.Url), name: dependencia.Name);
                else if (nomeDependencia.StartsWith("rabbitmq-"))
                    builder = builder.AddRabbitMQ(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("azureservicebusqueue-"))
                    builder = builder.AddAzureServiceBusQueue(dependencia.ConnectionString, queueName: dependencia.QueueName, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("azureblobstorage-"))
                    builder = builder.AddAzureBlobStorage(dependencia.ConnectionString, name: dependencia.Name);
                else if (nomeDependencia.StartsWith("documentdb-"))
                {
                    builder = builder.AddDocumentDb(
                        docdb => {
                            docdb.UriEndpoint = dependencia.UriEndpoint;
                            docdb.PrimaryKey = dependencia.PrimaryKey;
                        });
                }                    
            }

            return builder;
        }
    }
}

Por fim, alterações serão realizadas na classe Startup:

 

O método AddDependencies (implementado em DependenciesExtensions) será invocado logo após AddHealthChecks em ConfigureServices, recebendo como parâmetro uma lista com instâncias do tipo Dependencies. Já a chamada a AddHealthChecksUI permitirá configurar o dashboard para visualização dos status das dependências.

O endpoint com os dados dos Health Checks será gerado ao acionar o método UseHealthChecks em Configure. Ao invocar UseHealthChecksUI, um dashboard de monitoramento estará disponível em /healthchecks-ui.

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using HealthChecks.UI.Client;
using SiteMonitoramento.Models;
using SiteMonitoramento.Extensions;

namespace SiteMonitoramento
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var dadosDependencias = new List<Dependency>();

            new ConfigureFromConfigurationOptions<List<Dependency>>(
                Configuration.GetSection("Dependencies"))
                    .Configure(dadosDependencias);
            dadosDependencias = dadosDependencias.OrderBy(d => d.Name).ToList();

            // Verificando a disponibilidade dos bancos de dados
            // da aplicação através de Health Checks
            services.AddHealthChecks()
                .AddDependencies(dadosDependencias);
            services.AddHealthChecksUI();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            // Gera o endpoint que retornará os dados utilizados no dashboard
            app.UseHealthChecks("/healthchecks-data-ui", new HealthCheckOptions()
            {
                Predicate = _ => true,
                ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
            });

            // Ativa o dashboard para a visualização da situação de cada Health Check
            app.UseHealthChecksUI();
        }
    }
}

Testes

 

Na imagem a seguir, temos um exemplo com alguns Health Checks apontando falhas (não foi implementado nenhum código em HTML para a obtenção deste resultado):

uma imagem de um telefone celular

Já esta segunda imagem mostra o status de normalidade para todas as dependências:

uma imagem de um telefone celular

Referências

 

.NET Core 2.2 e ASP.NET Core 2.2: Guia de Referência


Autor:

Consultor em atividades voltadas ao desenvolvimento de sistemas há mais de 15 anos. Microsoft MVP (Most Valuable Professional) e participante do programa MTAC (Multi-Plataform Technical Audience Contributor). Bacharel em Sistemas de Informação, com Especialização em Engenharia de Software e MBA em Business Intelligence. Também é palestrante e autor técnico em portais e revistas especializadas, com foco em tecnologias Microsoft e boas práticas na área de software.