Adding healthchecks just got a lot easier in ASP.NET Core 2.2

Setting up and using healthcheck endpoints in ASP.NET Core 2.2

Health checks or health probes are the crucial part for orchestrating micro services. If you are using Kubernetes for orchestrating containerized application instances or you just have multiple ServiceApps or VMS behind the load balancer, you need to define health checks in order for the orchestrator or load balancer to know to which instance to send the traffic and to which not. It does not necessary mean that your application instance is ready to start to receive traffic as soon as it is instantiated.

In real-case scenarios, it takes some time for aplication to start receiving traffic and for this purpose you need health checks to determine if your application instance whether it is container, pod, Service App or VM instance is ready for traffic handling.

New release of .NET Core, version 2.2 has reached it's third preview and .NET Core platform looks more polished than it's predecessor, not so long ago released 2.1 version. One of the gem stones in this release are definitely healtchchecks which now come out of the box with ASP.NET Core 2.2.

Note

.NET Core 2.2 is still in preview stage. If you want to install it on your development environment, you can find binaries/installers at https://www.microsoft.com/net/download/dotnet-core/2.2 />You cannot create projects with installed .NET Core 2.2 SDK and Runtime using Visual Studio 2017, instead you need to install one of the editions of Visual Studio 2017 Preview which you can find at https://visualstudio.microsoft.com/vs/preview/

In previous version, if you wanted to add healthchecks endpoints to your application you needed to build your own interfaces and implementations or use code from HealthChecks repository from GitHub https://github.com/dotnet-architecture/HealthChecks with already developed interfaces and some basic implementations of the most common types of health checks. With new ASP.NET Core 2.2 release it is a thing of past.

Dependency injection configuration

You can inject your healthchecks directly from Startup.cs class of your ASP.NET Core project.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHealthChecks()
            .AddAsyncCheck("Sql", async () =>
                {
                    using (var connection = new SqlConnection("Data Source=.\\SQLEXPRESS;Initial Catalog=ProductsCatalog;Integrated Security=SSPI;"))
                    {
                        try
                        {
                            await connection.OpenAsync();
                        }
                        catch (Exception)
                        {
                            return HealthCheckResult.Failed();
                        }
                        return HealthCheckResult.Passed();
                    }
                })
            .AddAsyncCheck("Redis", async () =>
            {
                using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
                {
                    try
                    {
                        var db = redis.GetDatabase(0);
                    }
                    catch (Exception)
                    {
                        return await Task.FromResult(HealthCheckResult.Failed());
                    }
                }
                return await Task.FromResult(HealthCheckResult.Passed());
            })
                   .AddAsyncCheck("Http", async () =>
                   {
                       using (HttpClient client = new HttpClient())
                       {
                           try
                           {
                               var response = await client.GetAsync("http://localhost:5000");
                               if (!response.IsSuccessStatusCode)
                               {
                                   throw new Exception("Url not responding with 200 OK");
                               }
                           }
                           catch (Exception)
                           {
                               return await Task.FromResult(HealthCheckResult.Failed());
                           }
                       }
                       return await Task.FromResult(HealthCheckResult.Passed());
                   });

            services.AddMvc();
        }

    

Since healthchecks are supposed to be light, it is not even necessary to developed a separate classes as simple Func<HealthStatus> that return HealthStatus is enough to make your healthcheck run. By default, scope of healthcheck instance is intranscient meaning when ever health check is invoked it will do the checks, return results and dispose. How ever, if your healthcheck logic is a bit more complex than just few lines or you have a lot of healthchecks which may make your startup class too big, you can take out your healthcheck out as a separate implementation classes. 

    public class SqlServerHealthcheck : IHealthCheck
    {
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var connection = new SqlConnection("Data Source=.\\SQLEXPRESS;Initial Catalog=ProductsCatalog;Integrated Security=SSPI;"))
            {
                try
                {
                    await connection.OpenAsync();
                }
                catch (Exception)
                {
                    return HealthCheckResult.Failed();
                }
                return HealthCheckResult.Passed();
            }
        }
    }

    public class ReadisHealthcheck : IHealthCheck
    {
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
            {
                try
                {
                    var db = redis.GetDatabase(0);
                }
                catch (Exception)
                {
                    return await Task.FromResult(HealthCheckResult.Failed());
                }
            }
            return await Task.FromResult(HealthCheckResult.Passed());
        }
    }

    public class HttpHealthCheck : IHealthCheck
    {
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (HttpClient client = new HttpClient())
            {
                try
                {
                    var response = await client.GetAsync("http://localhost:5000");
                    if (!response.IsSuccessStatusCode)
                    {
                        throw new Exception("Url not responding with 200 OK");
                    }
                }
                catch (Exception)
                {
                    return await Task.FromResult(HealthCheckResult.Failed());
                }
            }
            return await Task.FromResult(HealthCheckResult.Passed());
        }
    }
    

All you have to do with your healthcheck implementation classes is to add the to DI container in Startup.cs file ConfigureServices method.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHealthChecks()
                .AddCheck<SqlServerHealthcheck>("Sql")
                .AddCheck<ReadisHealthcheck>("Redis")
                .AddCheck<HttpHealthCheck>("Http");

            services.AddMvc();
        }
    

Pipeline configuration

Once you have your healthcheck added to ASP.NET Core dependency injection, you just need to add it to the pipeline with the route you want to use to check statuses of the healthchecks.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseHealthChecks("/healthcheck");
            app.UseMvc();
        }
    

I do not have the database for the dummy connection string neither do I have REDIS instance on my localhost so both of my tests will fail with text response message and HTTP status code of 500 Internal Server Error.

Healthcheck

Customizing the response

Most of the healthcheck clients will consider anything other than 200 OK HTTP status code as unhealthy, but you also hae a freedom to change the status code for each HealthStatus type of response.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var options = new HealthCheckOptions();
            options.ResultStatusCodes[HealthStatus.Failed] = StatusCodes.Status503ServiceUnavailable;

            app.UseHealthChecks("/healthcheck",options);

            app.UseMvc();
        }
    

If you run POSTMAN against your healthcheck endpoint, instead of default 500 Internal Server Error HTTP status code, you will get 503 Service Not Available status code in the response. Same way you can define custom HTTP status code for every HealthStatus value option.

Healthcheck 503

Failed or Success message in a response is pretty generic and does not give any description to the client which healthcheck is failing, it is just giving the overall healthchecks status. Apart from the status code we can also modify the response payload sent on the healthcheck endpoint.

Same way we are configuring custom the status code for each HealthStatus option, we can set new ResponseWriter for our HealthcheckOptions object

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

            var options = new HealthCheckOptions();
            options.ResultStatusCodes[HealthStatus.Failed] = StatusCodes.Status503ServiceUnavailable;

            options.ResponseWriter = async (ctx, rpt) =>
            {
                var result = JsonConvert.SerializeObject(new
                {
                    status = rpt.Status.ToString(),
                    errors = rpt.Entries.Select(e => new { key = e.Key, value = Enum.GetName(typeof(HealthStatus), e.Value.Status) })
                },Formatting.None,new JsonSerializerSettings()
                {
                    NullValueHandling = NullValueHandling.Ignore
                });
                ctx.Response.ContentType = MediaTypeNames.Application.Json;
                await ctx.Response.WriteAsync(result);
            };

            app.UseHealthChecks("/healthcheck", options);

            app.UseMvc();
        }
    

Now if we try to invoke the helthcheck endpoint from POSTMAN we'll get totally different response where we can see which are the healthchecks that are failing. This makes tracing and acting on the ongoing issue a lot easier as you have the problem narrowed down and indicated by the healtcheck response.

 Custom Response

As addition you can use pre-defined health check from the following repository on Github https://github.com/xabaril/AspNetCore.Diagnostics.HealthChecks or you can also install them from the NuGet package manager

Install-Package AspNetCore.HealthChecks.System
Install-Package AspNetCore.HealthChecks.Network
Install-Package AspNetCore.HealthChecks.SqlServer
Install-Package AspNetCore.HealthChecks.MongoDb
Install-Package AspNetCore.HealthChecks.Npgsql
Install-Package AspNetCore.HealthChecks.Redis
Install-Package AspNetCore.HealthChecks.AzureStorage
Install-Package AspNetCore.HealthChecks.AzureServiceBus
Install-Package AspNetCore.HealthChecks.MySql
Install-Package AspNetCore.HealthChecks.DocumentDb
Install-Package AspNetCore.HealthChecks.SqLite
Install-Package AspNetCore.HealthChecks.Kafka
Install-Package AspNetCore.HealthChecks.RabbitMQ
Install-Package AspNetCore.HealthChecks.IdSvr
Install-Package AspNetCore.HealthChecks.DynamoDB
Install-Package AspNetCore.HealthChecks.Oracle
Install-Package AspNetCore.HealthChecks.Uris
    

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article