Using Standard Health Checks and Building your Own 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 previous articles I talked about creating a custom health check solution for ASP.NET Framework applications and implementing ASP.NET Core health checks with standard functionality. In this article I'll show you how to add and configure some existing, open-source health checks to monitor an SMTP server and the web server's disk space (these are just examples; there are many more health checks available that you can plug in to your own web site). And if the standard health checks aren't sufficient, you can build your own; something you'll see how to do in the second part of this article.

In the previous articles about health checks you saw how to monitor the current web site (is it up or not), check a (remote) SQL Server and monitor the disk space of the local server. But there are plenty of other checks you may want to carry out to ensure your system is in a healthy state. For example, what if you wanted to ensure your SMTP server is up and reachable? Or what if you want to make sure your Azure storage account can be accessed? Or maybe you want to check if your Cosmos DB or any of your other HTTP web services are online. While you could create health checks for this yourself, there's a better solution. At https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks you find a great open source project (licensed under the Apache-2.0 license ) with plenty of health checks for a variety of systems. In this article, I'll show you how to install and configure two of them (the SMTP health check and the disk space check) but I encourage you to check out the Github repo and take a look at what other health checks are available.

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.

Installing and configuring the SMTP health check

It's quite likely your site depends on the presence of an SMTP server. For example, you may be running an Ecommerce site that sends out order confirmations or back-in-stock notifications. Or maybe your login section sends password reset e-mails to users that forget their credentials. Whatever the reason you send out e-mails, it's likely that your system depends heavily on it. Therefore, it's a good idea to monitor your SMTP server as part of your overall health checks. You can do ths using the SMTP Health Check from one of the Xabaril health packages.

The SMTP health check is located in the AspNetCore.HealthChecks.Network package. To use it, install the package and then configure it in Startup.cs. The configuration needs your SMTP details which you could configure in Startup.cs directly. However, that becomes messy and hard to maintain rather quickly. In the next section I'll first show you how to configure the health check with hard coded SMTP details and then in a later section you see how to improve the situation using Dependency Injection and the ASP.NET Core configuration framework.

  • Start by installing the AspNetCore.HealthChecks.Network package using the following command:

    Install-Package AspNetCore.HealthChecks.Network
  • Next, in Startup.cs, expand the code that calls AddHealthChecks with a call to AddSmtpHealthCheck like this, updating the details to match your SMTP setup:

    services.AddHealthChecks().AddSqlServer(Configuration["ConnectionStrings:ContentContext"])
      .AddSmtpHealthCheck(setup =>
      {
        setup.Host = "YourMailServer";
        setup.Port = 587;
        setup.ConnectionType = SmtpConnectionType.TLS;
        setup.LoginWith("you@example.com", "Passw0rd");
        setup.AllowInvalidRemoteCertificates = true;
      }, tags: new [] { "smtp" }, failureStatus: HealthStatus.Degraded);
    

    You need to decide what a broken mail server means to your system and specify a matching HealthStatus. If e-mail functionality is crucial, then you probably want to return Unhealthy when the SMTP server is down. If it's OK for the mail server to be temporarily unavailable (for example because you have a service-bus architecture and you don't send out e-mails directly from the app but place them as future messages on some kind of queue), then setting it to Degraded is fine.

  • With these changes in place, the health UI at /healthchecks-ui (which I discussed in an earlier article) now shows something like this:

    The Health Check UI showing the SMTP Server check

    If your mail server goes down, or the health check can't connect with the specified user name or password, you'll see something like this:

    The Health Check UI showing the SMTP Server check in a degraded state

Improving configuration of the SMTP health check

I really like how simple it is to set up this health check. However, I think the code can be improved a bit by removing the hardcoded settings to the site's JSON configuration file and sharing it with your main e-mail functionality. To pull information from the site's config file, you have a few choices. One option is to use the IConfiguration instance that is already available in Startup and read your settings with something like Configuration["SmtpSettings:MailServer"]. However, I usually prefer to use strongly typed configuration classes for stuff like this as it gives you more control and makes reuse easier. As an example of reuse, I often have a MailSender class in my projects responsible for sending e-mail. That class needs the same SMTP configuration in order to send e-mail, so it makes sense to use the Dependency Injection facilities of .NET Core and inject a configuration class that is shared by the health check as well as the mail sender. I'll focus on the health check now, and deal with the mail sender in a future article.

To implement a strongly typed configuration class, you'll need to do something like this:

  1. Create a class to hold the configuration settings
  2. Create matching configuration settings in the site's json configuration file
  3. Register the class in ConfigureServices
  4. Retrieve an instance of the class in the same ConfigureServices method
  5. Use the configuration class to properly set up and configure the SMTP health check

Number 3 and 4 on this list are a bit tricky because you need to get an instance of the configuration class inside the Configure method which has some complications. You'll see how to implement all of this next.

  • Start by creating the configuration class and create properties for all the pieces of information the SMTP health check needs:

    public class SmtpSettings
    {
      public string MailServer { get; private set; }
      public int Port { get; private set; }
      public string UserName { get; private set; }
      public string Password { get; private set; }
    }    

    Since configuration data shouldn't be changed by calling code anymore, I want this class to be immutable so I am declaring all the property setters as private.

  • Next, add the following configuration to the site's appsettings.json file:

    "SmtpSettings": {
      "MailServer": "YourMailServer",
      "Port": 587,
      "UserName": "you@example.com",
      "Password": "Passw0rd"
    },  

    Update the settings to match your SMTP setup.

    Note: storing secrets like this directly in the config file is not a best practice. Consider using environment variables instead or look into something like Azure Key Vault.

  • The next step is to register this class in the Configure method so it becomes available as an injectable dependency. Normally when I need to make configuration available in other classes in my application, I would use the ASP.NET Core Options pattern and register the configuration class like this:

    services.Configure<SmtpSettings>(Configuration.GetSection("SmtpSettings"));
    

    And then in other places in my code I could accept an IOptions<SmtpSettings> in a constructor and access its Value property like so:

    public class MailSender
    {
      private readonly MailSettings _mailSettings;
      public MailSender(IOptions<MailSettings> mailSettings)
      {
        _mailSettings = mailSettings.Value;
      }
    }
    

    However, there are a few issues with this solution: since I made the SmtpSettings class immutable, this code will no longer assign the values from the configuration file to the class's properties. Secondly, I also need an instance of this class inside the Configure method so I can use it to set up the SMTP health check.

    To solve the issue with read-only properties, you can use the Bind method and specify BindNonPublicProperties = true like so:

    public void ConfigureServices(IServiceCollection services)
    {
      services.Configure<SmtpSettings>(options => Configuration.GetSection("SmtpSettings")
              .Bind(options, c => c.BindNonPublicProperties = true));
      ...
    }
    

    With that code, the class is now populated correctly so the code for the MailSender class from above would now work again.

    Getting access to an instance of the SmtpSettings class inside Configure is a little trickier. To understand the issues and the solutions, I recommend you read this post by Andrew Lock. In that post he suggests a few solutions of which I picked the "rebind" one as it seems the simplest solution to the problem at a very low cost (a bit of duplication once at startup). To bind the settings again, after calling services.Configure as shown above, I have added the following code:

    var smtpSettings = new SmtpSettings();
    Configuration.GetSection("SmtpSettings").Bind(smtpSettings, 
                  c => c.BindNonPublicProperties = true);
    

    This creates a new instance of SmtpSettings which is then populated using Bind to allow read-only properties to be updated. This way, I have an immutable configuration class that can be injected anywhere but also have access to the same underlying data in Startup. The smtpSettings instance is then used to register the SMTP health check as shown next.

    As an alternative to the rebind, this could also have worked if you use a class that has read/write properties:

    var mySettings = new SmtpSettings();
    new ConfigureFromConfigurationOptions<SmtpSettings>(
                   Configuration.GetSection("SmtpSettings")).Configure(mySettings);
    services.AddSingleton(mySettings);
    

    But since I wanted read-only properties, I didn't use this option.

  • The smtpSettings instance can now be used to update the health check code in Startup you saw earlier, like this:

      setup.Host = smtpSettings.MailServer;
      setup.Port = smtpSettings.Port;
    

    However, now that we have a nice settings class that we can pass on, it's a good idea to abstract this code out to an extension method like this:

    public static IHealthChecksBuilder AddSmtpHealthCheck(this IHealthChecksBuilder builder, SmtpSettings smtpSettings)
    {
      builder.AddSmtpHealthCheck(setup =>
      {
        setup.Host = smtpSettings.MailServer;
        setup.Port = smtpSettings.Port;
        setup.ConnectionType = SmtpConnectionType.TLS;
        setup.LoginWith(smtpSettings.UserName, smtpSettings.Password);
        setup.AllowInvalidRemoteCertificates = true;
      }, tags: new[] { "smtp" }, failureStatus: HealthStatus.Degraded);
      return builder;
    }
    

    And then call that method from Startup:

    services.AddHealthChecks()
        .AddSqlServer(Configuration["ConnectionStrings:ContentContext"])
        .AddSmtpHealthCheck(smtpSettings);
    

    That's it; the health check now uses the SMTP settings from the configuration file while the same settings are also available to other classes. I'll show an example of that in a future article where I discuss fallback solutions for SMTP servers.

Checking the health of the local disk

Using the other health checks available from the AspNetCore.HealthChecks packages is very similar. To get feature parity with the custom health checks for ASP.NET Framework applications I presented in part 1 of this series, I'll show you how to set up a disk monitoring health check. For all other checks, be sure to check out the health checks Github repo at https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks

To set up disk monitoring, follow these steps:

  1. Install the AspNetCore.HealthChecks.System package using the command:

    Install-Package AspNetCore.HealthChecks.System
    
  2. Add a health check for the disk storage using AddDiskStorageHealthCheck. That method takes a DiskStorageOptions object that in turn lets you add a drive by calling AddDrive and specify a name and the minimum size in megabytes that the system needs to be considered healthy. The drive name is the drive letter followed by a colon and a backslash. You can call DiskStorageOptions more than once to set up different thresholds for different health statuses. In the sample application you find the following code:
    .AddSmtpHealthCheck(smtpSettings)
    .AddDiskStorageHealthCheck(x=> x.AddDrive("C:\\", 10_000), "Check primary disk - warning", HealthStatus.Degraded)
    .AddDiskStorageHealthCheck(x=> x.AddDrive("C:\\", 2_000), "Check primary disk - error", HealthStatus.Unhealthy);
    
  3. With this setup, you get a Degraded result when the drive is below ~10 gigabytes, and an Unhealthy state when it drops below ~2. Here's what that would look like in the Health Checks UI: The health check UI showing a warning for the disk space being below the configured minimum of 10,000 megabytes.

    For your own setup, you should carefully decide what drives you want to monitor and what the various thresholds should be. You don't want to be alerted too often, but you also don't want to end up with a full drive on a production server.

Building a custom health check

Often in applications you need to be able to write files to disk, such as logs, or user-uploaded images. To ensure your app can write to the designated folder, you can write a custom health check that attempts to write a file to disk and then erase it again. You can implement this by creating a class that implements IHealthCheck that has one required method that needs to be implemented: CheckHealthAsync. Inside that method you validate you can write to the designated folder and return a Healthy or Unhealthy state depending the outcome of the write attempt. Finally. you plug that class into the health checks pipeline using an extension method. Here's how to implement that:

  1. Create a class that inherits the IHealthCheck interface and implement the CheckHealthAsync method. To make it easy to test various folders with the same health class, it's a good idea to accept the folder to test in the constructor of the class. Here's the barebones version of the health check class:

    public class VerifyWritePermissionsHealthCheck : IHealthCheck
    {
      private readonly string _folder;
    
      public VerifyWritePermissionsHealthCheck(string folder)
      {
        _folder = folder;
      }
    
      public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, 
                            CancellationToken cancellationToken = new CancellationToken())
      {
        // TODO
      }
    }
  2. To test the permissions, you can try to write a file to the designated folder and then delete it again. You should do this in a try/catch block so you can return Healthy on success, and Unhealthy when something doesn't work as planned. Here's what that code could look like:

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
                            CancellationToken cancellationToken = new CancellationToken())
    {
      try
      {
        var fileName = Path.Combine(_folder, $"{Guid.NewGuid()}.txt");
        File.WriteAllText(fileName, "Test from health check");
        File.Delete(fileName);
        return Task.FromResult(HealthCheckResult.Healthy());
      }
      catch
      {
        return Task.FromResult(HealthCheckResult.Unhealthy());
      }
    }

    This code simply writes a text file to the folder that was passed in and then deletes the file immediately again. The catch block ensures that the check returns Unhealthy if writing or deleting the file fails.

  3. To wire this up, I have created an extension method on the IHealthChecksBuilder that accepts the folder to test, new up an instance of the VerifyWritePermissionsHealthCheck class and registers it by calling AddCheck on the builder:

    public static class HealthCheckExtensions
    {
      public static IHealthChecksBuilder AddFileWritePermissionsCheck(this IHealthChecksBuilder builder, string folderToTest)
      {
        var check = new VerifyWritePermissionsHealthCheck(folderToTest);
        builder.AddCheck("Check folder write permissions", check);
    
        return builder;
      }
    
      ... Other extensions here
    }
  4. The last thing to do is call this extension method from Startup. But how do you determine the folder to test? One option would be to hardcode the path but that would break pretty quickly when you deploy the app to another server. You could also define the folder in the configuration file so it can be different between, say, your development and production environments. Yet another alternative is to get the folder at runtime from the environment. For example, if you wanted to test the wwwroot folder, you can pull its path from the IWebHostEnvironment that is available in Startup. You can't access its instance in ConfigureServices directly, but you can grab it in Startup's constructor, save it in private field and then use it inside ConfigureServices. That's what I implemented in the sample app, and here's how it looks:

    public class Startup
    {
      private readonly IWebHostEnvironment _environment;
    
      public Startup(IConfiguration configuration, IWebHostEnvironment environment)
      {
        _environment = environment;
        Configuration = configuration;
      }
    
      public IConfiguration Configuration { get; }
    
      ... Other code here
    }

    And then inside ConfigureServices you can access _environment like follows when registering the health check:

    services.AddHealthChecks()
      .AddSqlServer(Configuration["ConnectionStrings:ContentContext"])
      .AddSmtpHealthCheck(smtpSettings)
      .AddDiskStorageHealthCheck(x => x.AddDrive("C:\\", 10_000), "Check primary disk - warning", HealthStatus.Degraded)
      .AddDiskStorageHealthCheck(x => x.AddDrive("C:\\", 2_000), "Check primary disk - error", HealthStatus.Unhealthy)
      .AddFileWritePermissionsCheck(_environment.WebRootPath);
    

    That's nice and clean, with all logic abstracted out to the extension method and health check class.

With this health check in place, the UI now shows up as healthy when the web process can write to the designated folder:

The Health Check UI showing a healthy state for the file permissions check

And as soon as the account loses its proper permissions, or the folder is deleted or otherwise inaccessible, the state changes to unhealthy:

The Health Check UI showing an unhealthy state for the file permissions check

Note: since this code writes to disk every time it's called, it may lead to a disk hot spot if it's called often. To mitigate, you could add a tag when registering the health check and then use that tag to filter out the check on the high-frequency checks, and only include it in checks that are called once in a while.

Next steps

I recommend checking out the list of health checks in the Github repo here: https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks. There are lots of great ready-made checks available. Also, the Health Checks UI is very helpful to jumpstart your monitoring solutions.

If none of the existing health checks suit your needs, you can always build your own by creating a class that implements IHealthCheck as I have shown in this article.

Happy monitoring!

Downloads


Where to Next?

Wonder where to go next? You can read existing comments below or you can post a comment yourself on this article.

Doc ID 612
Full URL https://imar.spaanjaars.com/612/using-standard-health-checks-and-building-your-own-in-aspnet-core
Short cut https://imar.spaanjaars.com/612/
Written by Imar Spaanjaars
Date Posted 11/17/2020 13:16

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.