Implementing Health Checks in ASP.NET Core

Being able to monitor your ASP.NET Core web applications and APIs and detect any issues early on is crucial in ensuring your sites are up and running and in a healthy state. In a previous article I talked about creating a custom health check solution for ASP.NET Framework applications. In this article I'll show you how to leverage the built-in Health Check framework along with some third-party open-source add-ons available for ASP.NET Core applications. You may want to check out the previous article to better understand health checks and why they are useful. And seeing the custom solution for the .NET Framework may have you appreciate the built-in ASP.NET Core functionality even more. And in this article I'll dive a little deeper into some existing third-party health checks that make monitoring your sites and services super easy and show you how to build your own health checks and plug them into the system.

Implementing health checks in ASP.NET Core comes down to the following:

  • Install the NuGet package Microsoft.Extensions.Diagnostics.HealthChecks
  • Configure your solution to use health checks
  • Configure one or more checks
  • Optionally, override the behavior and output of the health check endpoint
  • Optionally, add the Health Check UI package to monitor your health checks in a visual way

A lot has already been written about health checks in ASP.NET Core, so I won't cover them in detail. However, I will show you some options that I find useful and use in my day-to-day monitoring solutions. I'll first show how to set up the basics, and then make it better by adding some health checks, changing the output and more.

You'll find the full code for the article as a download at the end of this article, as well as in the Github repository for this article.

The following steps assume you have already created a standard ASP.NET Core web application with MVC enabled, targeting ASP.NET Core 5 or 3.1. Other versions may also work, but the steps or your results may differ.

  1. Install the health checks package Microsoft.Extensions.Diagnostics.HealthChecks using the NuGet UI or by executing the following command in the package manager console:

    Install-Package Microsoft.Extensions.Diagnostics.HealthChecks
  2. In the ConfigureServices method in Startup.cs, add the following highlighted line of code to add the health checks middleware:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddHealthChecks();
      services.AddControllersWithViews();
    }
  3. In the same class, in the Configure method, configure the health checks endpoint as follows by updating the existing UseEndpoints call:

    app.UseEndpoints(endpoints =>
    {          
      endpoints.MapHealthChecks("/api/health");
      endpoints.MapControllerRoute(
                  name: "default",
                  pattern: "{controller=Home}/{action=Index}/{id?}");
    });
    
  4. Open the site in your browser and navigate to /api/health. You should see the following comforting , but somewhat boring output:

    The results from the health endpoint showing nothing but the word 'Healthy'

At this point, you have a working, but pretty limited health check endpoint. An external system like Uptime Robot or Azure availability can repeatedly call this endpoint and send you alerts by email or text messages whenever the output is anything other than Healthy. In a real-world scenario, however, you would want to enrich the health check with at least the following:

  • Add at least one (but typically more) additional health check to test a subsystem of your solution like a database server, an SMTP connection or a required remote HTTP web API.
  • Provide richer information from the /api/health endpoint including more detailed statuses, time taken and problem descriptions.

You'll see how to implement this next.

Adding a SQL Server Health check

To replicate the database check to validate if your SQL Server database is up and running as shown in the previous article, follow these steps:

  1. Install the package AspNetCore.HealthChecks.SqlServer using the Nuget package manager UI or console.

    Note: this is a third-party package from Xabaril. Later in this article and in the next one in this series you'll see more packages from Xabaril for health checks.

  2. Update the call to AddHealthChecks as follows; update YourConnectionString so it points to a connection string you have defined in your appsettings.json file:

    services.AddHealthChecks().AddSqlServer(Configuration["ConnectionStrings:YourConnectionString"]);
  3. Save and run the application. With these changes in place, every time you hit the health endpoint, a connection to the configured SQL Server is made and a "SELECT 1" query is executed to ensure the connection is working as expected. If the connection succeeds, it will return a healthy state, otherwise it will return an Unhealthy state. If you want to more closely replicate the behavior described in the previous article by testing the database using an Entity Framework DbContext targeting a specific DbSet, you can create a custom class implementing the IHealthCheck interface as explained in this article.

Improving the output from the health endpoint

The status returned from the health endpoint is currently pretty simplistic: it's either Healthy, Unhealthy or Degraded. That doesn't tell you much about why that is the case, and which (sub) system is causing any issues. Fortunately, you can easily change the output of the health endpoint to return JSON and include more details. One place to implement that is directly in Startup.cs. However, I prefer to create an extension method that takes care of the necessary setup and then call that method from Startup. That keeps your Startup class nice and clean which in turn makes it easy to see what's going on in that class and change its behavior if needed. In the sample app for this article I have added the following code:

using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json;

namespace HealthChecks.Web.Core.Health
{
  public static class HealthCheckExtensions
  {
    public static void MapHealthChecksWithJsonResponse(this IEndpointRouteBuilder endpoints, PathString path)
    {
      var options = new HealthCheckOptions
      {
        ResponseWriter = async (httpContext, healthReport) =>
        {
          httpContext.Response.ContentType = "application/json";

          var result = JsonConvert.SerializeObject(new
          {
            status = healthReport.Status.ToString(),
            totalDurationInSeconds = healthReport.TotalDuration.TotalSeconds,
            entries = healthReport.Entries.Select(e => new
            {
              key = e.Key,
              status = e.Value.Status.ToString(),
              description = e.Value.Description,
              data = e.Value.Data
            })
          });
          await httpContext.Response.WriteAsync(result);
        }
      };
      endpoints.MapHealthChecks(path, options);
    }
  }
}

Note: in the sample application this class lives in the main web project (in the Health folder). However, in most applications, you're better off moving this to a shared library that you can easily reference from multiple applications so you don't have to add the same code over and over again.

To call this new code, I changed the call to AddHealthChecks in Startup.cs to this:

endpoints.MapHealthChecksWithJsonResponse("/api/health");

When you now hit the api/health endpoint you get a nice JSON document like this:

{
  "status": "Unhealthy",
  "totalDurationInSeconds": 0.004926,
  "entries": [
    {
      "key": "sqlserver",
      "status": "Unhealthy",
      "description": null,
      "data": {
        
      }
    }
  ]
}

The HealthReportEntry (accessible as e.Value in the code above) also exposes an Exception property that provides details about the exception - if any - that occurred during the health check. Be careful when adding this to the JSON endpoint as it could expose sensitive data. I typically don't include it in the public endpoint that my monitoring tools calls but make it available in a different endpoint that requires authentication.

Visualizing your health checks

If you want a more visual overview of your health checks, consider using HealthCheckUI, a NuGet package that adds a dynamic UI to your site to provide insights into your health checks. Just like the SQL Server check you saw before, this is a third-party package from Xabaril. Here's how I set up the HealthCheckUI package in the sample app:

  • Run Install-Package AspNetCore.HealthChecks.UI - which adds the UI functionality for your health checks.

  • Run Install-Package AspNetCore.HealthChecks.UI.InMemory.Storage - This is needed to store the results of the health checks in memory. Alternatives, such as SQL Server Storage or My SQL storage are available as well.

  • Run Install-Package AspNetCore.HealthChecks.UI.Client - This provides the WriteHealthCheckUIResponse method which rewrites the health info in a JSON format supported by the UI client, similar to my code in MapHealthChecksWithJsonResponse earlier in this article.

  • Next, make the UI middleware available in ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddHealthChecks().AddSqlServer(Configuration["ConnectionStrings:ContentContext"]);
      services.AddHealthChecksUI().AddInMemoryStorage();
      services.AddControllersWithViews();
    }
  • In Configure, update the code that maps the health endpoint to return JSON instead using UIResponseWriter.WriteHealthCheckUIResponse and set up the UI by calling MapHealthChecksUI:

    app.UseEndpoints(endpoints =>
    {
      endpoints.MapHealthChecks("/api/health", new HealthCheckOptions
      {
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
      });
      endpoints.MapHealthChecksUI();
              
      endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
    }); 

    The HealthCheckOptions class has some further properties that you might want to use. For example, the Predicate property allows you to filter specific health checks so you can include or exclude the checks you want. As an example, this code would filter out any checks that have a Paid tag to avoid incurring costs every time the health check is executed:

    endpoints.MapHealthChecks("/api/health", new HealthCheckOptions
    {
      Predicate = x => !x.Tags.Any(t => t == "Paid"),
      ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

    You can usually add the tags as part of the registration process for the health check using one of the Add* methods like so for the SQL check:

    services.AddHealthChecks().AddSqlServer(Configuration["ConnectionStrings:ContentContext"], tags: new [] { "Paid" })
    

    Using the ResultCodes property you can define the HTTP status code that is returned for the various health states. The defaults are usually fine, but your remote availability checker may have different requirements. Note: when you change the codes, you have to do it for all three members of the HealthStatus enum:

    Predicate = x => !x.Tags.Any(t => t == "Paid"),
    ResultStatusCodes = new Dictionary<HealthStatus, int>
    { 
      { HealthStatus.Degraded, 500 }, 
      { HealthStatus.Healthy, 200 } , 
      { HealthStatus.Unhealthy, 503 }
    },
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
  • Next, add the following section to your appsettings.json file:

    "HealthChecksUI": {
      "HealthChecks": [
        {
          "Name": "HealthCheckExample",
          "Uri": "https://localhost:12345/api/health"
        }
      ],
      "EvaluationTimeInSeconds": 12,
      "MinimumSecondsBetweenFailureNotifications": 60
    },      

    Update https://localhost:12345 to match your site's URL and port number.

    This tells the Health Check UI where to look for the endpoints that return data. You can register multiple checks using different URLs, giving you a nice overview of many different connected (sub)systems in a single UI; great for microservices for example to keep an eye on all your application's subsystems from a single location.

  • Finally, browse to /healthchecks-ui. You should see the following UI indicating the health status of your system: The Health Check UI app in the browser

Next steps

If you're new to health checks in ASP.NET Core, I recommend experimenting with the various health checks, the API endpoint and the user interface. You could also look at services like Uptime Robot or Azure availability tests with Application Insights. This way you get a better feel for what you can monitor, how you can be notified of any issues, and how to use external monitoring tools to watch your sites and services.

In the next article I'll demonstrate a few existing health checks to check an SMTP Server and the local disk space, as well as how to build your own validation checks and plug them into the system.

Downloads


Where to Next?

Wonder where to go next? You can post a comment on this article.

Doc ID 611
Full URL https://imar.spaanjaars.com/611/implementing-health-checks-in-aspnet-core
Short cut https://imar.spaanjaars.com/611/
Written by Imar Spaanjaars
Date Posted 11/08/2020 13:39

Comments

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the Doc ID of the document.

(Plain text only; no HTML or code that looks like HTML or XML. In other words, don't use < and >. Also no links allowed.