During application development on our system, we can easily test external services or databases that we work with to see if we can access the database or external service or not. However, this becomes a bit challenging after uploading to the main servers. Therefore, it's better to use a tool to check the availability status of external services, databases, or any other service. For this purpose, we can use the HealthChecks package. HealthChecks provides us with the capability to be informed about the status of all the services we need by calling an API. In this article, we will discuss implementing HealthCheck for Redis and SQL databases and checking access to an external service. First, we implement a project in Asp.Net Core and install the following packages:

Next, we create a class named HealthCheckExtension, and implement the health checks for Redis, SQL, and an external service:

public static class HealthCheckExtension
{
    public static void AddAdvancedHealthCheck(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var sqlConnection = configuration["ConnectionStrings:SqlServer"];
        var redisConnection = configuration["ConnectionStrings:Redis"];
        var externalServiceUri = configuration["someService:uri"];

        services
            .AddHealthChecks()
            .AddRedis(redisConnection, "redis")
            .AddSqlServer(new SqlServerHealthCheckOptions
            {
                ConnectionString = sqlConnection,
            })
            .AddTcpHealthCheck(option =>
            {
                var uri = new Uri(externalServiceUri);
                option.AddHost(uri.Host, uri.Port);
            });
    }
}

In the first part, we first read the desired addresses from appsettings.json using IConfiguration, and then we added the HealthCheck for each service. Now, we need to add the middleware for HealthCheck:

app.UseHealthChecks("/health");

If you run the program now and go to the /health address, you should receive the value Healthy or Degraded.

 However, this text is not sufficient to determine which of the services is having an issue. To address this, we can customize the output according to our needs. To do this, we can create a method to modify the health output and add it to the middleware.

public static Task WriteResponse(
    HttpContext context,
    HealthReport report)
{
    var jsonSerializerOptions = new JsonSerializerOptions
    {
        WriteIndented = false,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    string json = JsonSerializer.Serialize(
        new
        {
            Status = report.Status.ToString(),
            Duration = report.TotalDuration,
            Info = report.Entries
                .Select(e =>
                    new
                    {
                        Key = e.Key,
                        Description = e.Value.Description,
                        Duration = e.Value.Duration,
                        Status = Enum.GetName(
                            typeof(HealthStatus),
                            e.Value.Status),
                        Error = e.Value.Exception?.Message,
                        Data = e.Value.Data
                    })
                .ToList()
        },
        jsonSerializerOptions);

    context.Response.ContentType = MediaTypeNames.Application.Json;
    return context.Response.WriteAsync(json);
}

Then you should add the created method to the middleware as follows:

app.UseHealthChecks("/health", new HealthCheckOptions()
{
    ResponseWriter = HealthCheckExtension.WriteResponse
});

Now, if you go to the /health address, all the services you specified will be displayed to you in JSON format.

{
    "status": "Healthy",
    "duration": "00:00:00.5887094",
    "info":
    [
        {
            "key": "redis",
            "duration": "00:00:00.5152658",
            "status": "Healthy",
            "data":
            {}
        },
        {
            "key": "sqlserver",
            "duration": "00:00:00.5718492",
            "status": "Healthy",
            "data":
            {}
        },
        {
            "key": "tcp",
            "duration": "00:00:00.1394934",
            "status": "Healthy",
            "data":
            {}
        }
    ]
}

If one of the services encounters a problem and you cannot connect to it, the output will be as follows:

{
    "status": "Unhealthy",
    "duration": "00:00:13.4584684",
    "info":
    [
        {
            "key": "redis",
            "duration": "00:00:13.4060867",
            "status": "Unhealthy",
            "error": "The message timed out in the backlog attempting to send because no connection became available (5000ms) - Last Connection Exception: It was not possible to connect to the redis server(s). ConnectTimeout, command=PING, timeout: 5000, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, rs: NotStarted, ws: Initializing, in: 0, last-in: 0, cur-in: 0, sync-ops: 0, async-ops: 1, serverEndpoint: localhost:6479, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: DESKTOP-QR3CNC3(SE.Redis-v2.7.4.20928), IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=0,Free=32767,Min=20,Max=32767), POOL: (Threads=7,QueuedItems=0,CompletedItems=164,Timers=7), v: 2.7.4.20928 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)",
            "data":
            {}
        },
        {
            "key": "sqlserver",
            "duration": "00:00:00.2903827",
            "status": "Healthy",
            "data":
            {}
        },
        {
            "key": "tcp",
            "duration": "00:00:00.1094343",
            "status": "Healthy",
            "data":
            {}
        }
    ]
}

You can also create a custom HealthCheck for yourself. To do this, you need to implement the IHealthCheck interface and add it to the HealthCheck services using the AddCheck method. For example:

public class UserApiHealthCheck : IHealthCheck
{
    private readonly IHttpClientFactory _httpClientFactory;

    public UserApiHealthCheck(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        var client = _httpClientFactory.CreateClient();

        var response = await client.GetAsync("/api/v1.0/users");

        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            return HealthCheckResult.Healthy();
        }

        return HealthCheckResult.Degraded();

    }
}

And add it to the HealthCheck services:

services
    .AddHealthChecks()
    .AddRedis(redisConnection, "redis")
    .AddSqlServer(new SqlServerHealthCheckOptions
    {
        ConnectionString = sqlConnection,
    })
    .AddTcpHealthCheck(option =>
    {
        var uri = new Uri(externalServiceUri);
        option.AddHost(uri.Host, uri.Port);
    })
    .AddCheck<UserApiHealthCheck>("UserApi");

appsettings.json file:

{
  "ConnectionStrings": {
    "Redis": "localhost:6379,abortConnect=false",
    "SqlServer": "Data Source=127.0.0.1,1433; Initial Catalog=master; User Id=sa; Password=example_123; TrustServerCertificate=True;"
  },
  "SomeService": {
    "Uri": "https://google.com"
  }
}

You can download this project from Github.

References:

;)

Powered by Froala Editor

Comments