W dzisiejszych czasach przy coraz bardziej złożonych usługach monitoring zyskuje na znaczeniu. W ASP.NET Core dostajemy wbudowane wsparcie do badania stanu aplikacji poprzez Health Checks Middleware. Co nam daje to w praktyce? Zastanówmy się jak możemy sprawdzić czy udostępnione API działa prawidłowo? Zapewne wykonamy żądanie do pierwszego lepszego punktu końcowego. Załóżmy, że nasze API korzysta z brokera wiadomości, bazy danych, zewnętrznej usługi czy zasobów sieciowych. Odpytując losowy endpoint nie mamy pewności czy wszystkie usługi kryjące się za API działają prawidłowo. Nie którzy zabezpieczają się monitorując inne usługi. Co w przypadku, gdy zawiedzie komunikacja pomiędzy naszym API, a dowolną usługą na innym serwerze. Z pewnością wygodniej byłoby odpytać jeden endpoint i uzyskać jak najwięcej informacji przydatnych przy monitoringu. I tutaj do gry wchodzi Health Check. We wpisie omówię wykorzystanie Health Check w ASP.NET Core API od najprostszego przykładu do coraz bardziej rozbudowanego.

Podstawowy Health Check

Podstawowa konfiguracja zapewnia sprawdzenie dostępności usługi tzw. liveness. W ASP.NET Core paczka Microsoft.AspNetCore.Diagnostics.HealthChecks jest wykorzystywana do dodania health check-u w aplikacji. Na ten moment nie musimy pobierać żadnej dodatkowej paczki z NuGet, mamy wszystko dostępne od razu z pudełka po utworzeniu projektu API w ASP.NET Core 3.1. Otwórzmy naszą klasę Startup i zarejestrujmy usługę health checks poprzez wywołanie metody AddHealthChecks w metodzie ConfigureServices. Zostało nam jeszcze stworzenie punktu końcowego do monitorowania naszego API poprzez wywołanie metody MapHealthChecks w metodzie Configure.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

		public IConfiguration Configuration { get; }

		public void ConfigureServices(IServiceCollection services)
		{
			services.AddControllers();
			services.AddHealthChecks();
		}

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

			app.UseHttpsRedirection();

			app.UseRouting();

			app.UseAuthorization();

			app.UseEndpoints(endpoints =>
			{
				endpoints.MapControllers();
				endpoints.MapHealthChecks("/health");
			});
		}
	}
}

Nie mamy skonfigurowanych dodatkowych ustawień, jeśli API działa to dostaniemy odpowiedź Healthy. Sprawdźmy zatem nasz endpoint /health w przeglądarce.

Liveness

W odpowiedzi uzyskaliśmy status usługi w postaci zwykłego tekstu. Wyróżniamy trzy statusy usług: HealthStatus.Healthy, HealthStatus.Degraded oraz HealthStatus.Unhealthy. Podobnym wzorcem weryfikującym czy serwer/usługa działa jest tzw. PING PONG, który działa np. w Redis. W Redis CLI wywołujemy komendę PING i oczekujemy w przypadku uruchomionego serwera odpowiedzi PONG.

Implementujemy własny Health Check

Pakiet Microsoft.AspNetCore.Diagnostics.HealthChecks nie zawiera żadnych wbudowanych rozwiązań, stanowi jedynie bazę do dalszej implementacji. W celu stworzenia własnego rozwiązania należy zaimplementować w klasie interfejs IHealthCheck. Wszystko opiera się na implementacji asynchronicznej metody CheckHealthAsync, która zwraca status naszej usługi jako obiekt HealthCheckResult. W celu pokazania, że nie zawsze zwracany jest status healthy, wprowadzimy losowość w implementacji.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace API.HealthChecks
{
	public class MyHealthCheck : IHealthCheck
	{
		public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
		{
			if (new Random().Next(1,10) % 2 == 0)
			{
				return Task.FromResult(HealthCheckResult.Healthy("Success"));
			}

			return Task.FromResult(HealthCheckResult.Unhealthy(description: "Failed"));
		}
	}
}

Został jeszcze jeden krok, czyli rejestracja MyHealthCheck za pomocą metody AddCheck w metodzie ConfigureServices.

public void ConfigureServices(IServiceCollection services)
		{
			services.AddControllers();
			services.AddHealthChecks()
				.AddCheck<MyHealthCheck>("My custom health check");
		}

Ze względu na wprowadzenie losowości uzyskujemy już nie tylko status healthy, ale także status unhealthy. Przejdźmy teraz do bardziej życiowego przykładu i zaimplementujmy health check dla SQL Server. U mnie instancja SQL Server będzie uruchamiana z poziomu Dockera. Do odpytania bazy danych wspomogę się biblioteką Dapper. W związku z tym z NuGet musimy zainstalować dwie paczki Dapper i Microsoft.Data.SqlClient.

using System;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace API.HealthChecks
{
	public class SqlServerHealthCheck : IHealthCheck
	{
		private readonly string _connectionString;

		public SqlServerHealthCheck(string connectionString)
		{
			_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
		}

		public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
		{
			try
			{
				using (var conn = new SqlConnection(_connectionString))
				{
					await conn.QueryAsync($"SELECT 1", cancellationToken);
					return HealthCheckResult.Healthy();
				}
			}
			catch (Exception ex)
			{
				return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
			}
		}
	}
}

W następnym kroku musimy dodać sekcje ze swoim connection string do appsettings.json.

 "sqlServerConnectionString": "Server=<IP>;Database=<Database>;User Id=<user>; Password=<pass>;",

Zostało jeszcze zarejestrowanie SqlServerHealthCheck. Ze względu, że przekazujemy connection string jako argument użyjemy metody AddTypeActivatedCheck zamiast AddCheck.

public void ConfigureServices(IServiceCollection services)
{
	var sqlServerConnectionString = Configuration["sqlServerConnectionString"];

	services.AddControllers();
	services.AddHealthChecks()
		.AddCheck<MyHealthCheck>("My custom health check")
		.AddTypeActivatedCheck<SqlServerHealthCheck>("Sql Server", args: new object[] {sqlServerConnectionString});
}

Możemy MyHealthCheck za komentować, by mieć pewność, że komunikacja z Microsoft SQL Server działa prawidłowo.

AspNetCore.Diagnostics.HealthChecks

Zamiast implementować każdy health check samemu, możemy wykorzystać dostępną paczkę AspNetCore.Diagnostics.HealthChecks. Jeśli przeglądaliście dokumentacje Microsoftu w tym temacie, to kilka razy w fioletowej ramce wyświetlił się wam komunikat „AspNetCore.Diagnostics.HealthChecks isn’t maintained or supported by Microsoft.”. Łatwo skojarzyć tę paczkę z Microsoft.AspNetCore.Diagnostics.HealthChecks, przez co wszystko wydaje się, że jest przez Microsoft wspierane. Powyższa paczka zawiera implementacje dla wielu usług np.:

  • Sql Server
  • MySql
  • Oracle
  • Sqlite
  • RavenDB
  • Postgres
  • EventStore
  • RabbitMQ
  • IbmMQ
  • Elasticsearch
  • Solr
  • Redis
  • System: Disk Storage, Private Memory, Virtual Memory, Process, Windows Service
  • Azure Service Bus, Azure Storage: Blob, Azure Key Vault, Azure DocumentDb, Azure IoT Hub
  • Amazon DynamoDb, Amazon S3
  • Network: Ftp, SFtp, Dns, Tcp port, Smtp, Imap
  • MongoDB
  • Kafka
  • Identity Server
  • Uri: single uri and uri groups
  • Consul
  • Hangfire
  • SignalR
  • Kubernetes

Reasumując, jest w czym wybierać z powyższej listy. W dalszej części wpisu zaprezentuję integracje z brokerem wiadomości RabbitMQ. Z NuGet tym razem pobieram dwie paczki: AspNetCore.HealthChecks.Rabbitmq i RabbitMQ.Client. W kolejnym kroku dodaję sekcje z ustawieniami RabbitMQ do appsettings.json. Wykorzystam tu domyślne ustawienia.

"rabbitconnstr": "amqp://guest:guest@<ip>:5672",

Na koniec rejestruję usługę RabbitMQ poprzez wywołanie metody AddRabbitMQ w ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
	var sqlServerConnectionString = Configuration["sqlServerConnectionString"];
	var rabbitConnectionString = Configuration["rabbitconnstr"];

	services.AddControllers();
	services.AddHealthChecks()
		.AddCheck<MyHealthCheck>("My custom health check")
		.AddTypeActivatedCheck<SqlServerHealthCheck>("Sql Server", args: new object[] {sqlServerConnectionString})
		.AddRabbitMQ(rabbitConnectionString: rabbitConnectionString);
}

Z pewnością przyznacie, że rejestracja health check-a z tym pakietem jest dość przyjemna. Żeby nie pobierać paczek w ciemno zobaczmy co wchodzi w skład takiej paczki np. AspNetCore.HealthChecks.Rabbitmq. Przede wszystkim widzimy, że mamy tylko dwie klasy na repozytorium dla powyższego pakietu. Pierwsza to implementacja healt check-a dla RabbitMQ, a druga rozszerza IHealthChecksBuilder o metody do wstrzyknięcia implementacji RabbitMQHealthCheck. Społeczność wypuszcza wiele różnych pakietów dla usług zewnętrznych. Czy z nich będziecie korzystać, to już zależy od Was.

Health Checks UI

Nadchodzi chwila dla miłośników UI. Zaprezentujmy statusy w interfejsie graficznym. W związku z tym musimy zainstalować dwie paczki: AspNetCore.HealthChecks.UI i AspNetCore.HealthChecks.UI.Client. Za pomocą metody AddHealthChecksUI rejestrujemy UI w ConfigureServices.

services.AddHealthChecksUI();

Następnie wywołujemy metodę MapHealthChecksUI w Configure. Należy skonfigurować ResponseWriter, aby używał UIResponseWriter.WriteHealthCheckUIResponse. Poniższa konfiguracja zapewnia nam zwrócenie wyniku odpowiedzi w formacie JSON. Wymagane to jest przez interfejs HealthCheck UI, aby uzyskać szczegółowe informacje o skonfigurowanych health check-ach.

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

	app.UseHttpsRedirection();

	app.UseRouting();

	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapControllers();
		endpoints.MapHealthChecks("/health", new HealthCheckOptions()
		{
			Predicate = _ => true,
			ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
		});

		endpoints.MapHealthChecksUI();
	});
}
health checks json

W ostatnim kroku w ramach uruchamiania UI należy dodać konfiguracje do appsettings.json. Konfiguracja wskazuje, z jakiego punktu końcowego ma UI skorzystać, by uzyskać informacje o stanie API.

"HealthChecks-UI": {
	"HealthChecks": [;
		{
			"Name": "API",
			"Uri": "https://localhost:5001/health"
		}
	],
	"EvaluationTimeOnSeconds": 5,
	"MinimumSecondsBetweenFailureNotifications": 60
}

W celu podglądu UI wchodzimy na endpoint /healthchecks-ui, który jest domyślnym URL dla UI.

Health Checks UI

Podsumowanie

W ASP.NET Core dostaliśmy wbudowane wsparcie do badania stanu aplikacji poprzez Health Checks Middleware. Powyższe przykłady pokazały, że konfiguracja health checks w ASP.NET Core jest łatwa. Podsumowując ten wpis nie wyczerpuje całego tematu monitorowania usług. Zachęcam do lektury dokumentacji oraz dodania monitoringu metryk sprzętowych (dysk, RAM). Mam nadzieję, że ten post był przydatny, do zobaczenia.