10 Years ago I was working on Hospital Information Systems (HIS) with 1000s of clients and Applications dependencies are Oracle Database, and 2 Web Services Provided by Government, Laboratory Information System (LIS) has some serial port and USB connection requirements, and PACS some other but as long as Database is Up and Web Services are running we were gold.
Now running hundreds of web services (some Micro, some Macro) and each talking to other services (some are in the same data center and some are in the other side of the world), Databases, Queues, Logging tools, Analytics Tools, Distributed caches, Key-vaults, etc… On top of this, there are also internal issues to check, like having the right files on the file system, Not ceiling CPU and Memory, using the right Configuration – App Settings, making sure all dependencies in dependency injection can create an instance, etc..
This can become an effort for a single app but when we put all the Micro-Services all together for all the environments (dev, test, sit, uat, perf, pre-prod prod, etc…), this easily becomes very hard question to answer “Is environment or API healthy” or if we know it is not, what is causing the problem. Providing the right answer to “It works on my machine” can be a bonus too. this example is for .Net Core 2.2 or Above if you are using an older version I put a note at the end of the Post
How Health Check works in Asp.Net Core
“Microsoft.AspNetCore.Diagnostics.HealthChecks” Nuget package is the main Nuget package you need to add to your project before anything else.
After adding the Package and restoring your project, we need to add middleware and dependencies to startup.cs file.
services.AddHealthChecks() Adds dependencies required by Healthchecks middleware and returns HealthChecksBuilder which we use to chain our checks.
HealthChecksBuilder have Add Method, which adds HealthCheckRegistration to a List (using HealthCheckServiceOptions).
There are several extension methods accepts different parameters to provide options to add HealthCheck
public void ConfigureServices(IServiceCollection services) { // ... services.AddHealthChecks(); // ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHealthChecks("/Health/isAliveAndWell"); // ... }
Adding AddHealthChecks() and UseHealthChecks(URL) will be enough to start HealthCheck,
Let’s start the Web App and hit the URL specified in UseHealthChecks (/Health/isAliveAndWell).
As there is nothing to check, we will get a Healthy message with 200 status code.
Time to Add things to check.
There are several extension methods allow us to add health checks. Let’s start simple with “AddCheck” Method, provide a unique name and Function to return HealthCheckResult and you have a check (as below)
- If everything works as expected, you will get a Healthy message with 200 status code
- If any check you added returns Unhealthy you should get Unhealthy Message with 503 status code
- If there is a server error on handling your health check request you should get status code 500
public void ConfigureServices(IServiceCollection services) { // ... services.AddHealthChecks() .AddCheck("client", () => { using (var client = new HttpClient()) { try { var responseTask = client.GetAsync("https://auth.myrcan.com"); responseTask.Wait(); responseTask.Result.EnsureSuccessStatusCode(); } catch (Exception ex) { return HealthCheckResult.Unhealthy(ex.Message, ex); } } return HealthCheckResult.Healthy(); }); // ... }
This technique solved our problem but creates a problem of a messy startup.cs file, next step moving the check to another class (as below) we are not changing much on the logic but just separating in its class and adding an extension method to IHealthChecksBuilder for easy chaining.
(I added the URL to the name of the as name needed to be unique and I might want to use this check for multiple URLs)
This technique solved our problem but creates a problem of a messy startup.cs file, next step moving the check to another class (as below) we are not changing much on the logic but just separating in its class and adding an extension method to IHealthChecksBuilder for easy chaining.
(I added the URL to the name of the as name needed to be unique and I might want to use this check for multiple URLs)
This technique solved our problem but creates a problem of a messy startup.cs file, next step moving the check to another class (as below) we are not changing much on the logic but just separating in its class and adding an extension method to IHealthChecksBuilder for easy chaining.
(I added the URL to the name of the as name needed to be unique and I might want to use this check for multiple URLs)
//HttpGetCheck.cs // ... public static partial class HealthCheckBuilderExtensions { public static IHealthChecksBuilder AddHttpGetCheck(this IHealthChecksBuilder builder, string url) { return builder.AddCheck($"DIHealthCheck {url}", new DIHealthCheck(url)); } } public class HttpGetCheck: IHealthCheck { private string url; public DIHealthCheck(string url) { this.url= url; } public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var client = new HttpClient()) { try { var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); } catch (Exception ex) { return HealthCheckResult.Unhealthy(ex.Message, ex); } } return HealthCheckResult.Healthy(); } } }
public void ConfigureServices(IServiceCollection services) { // ... services.AddHealthChecks() .AddHttpGetCheck("https://portal.myrcan.com/Health/IsAlive") .AddHttpGetCheck("https://product.myrcan.com/Health/IsAlive"); // ... }
a single line in Startup is much better and we can move this class to a library and share across projects now.
This is almost done except DI is a big part of our development practice and I don’t want to create a new instance if I can use DI instead, I also want to add additional details to the Health Check Result using Data object
let’s change HttpGetCheck.cs class to use dependency injection and add additional data.
AddTypeActivatedCheck has a bug with 2 params method on current version that’s why I used 4 params version but keep params null.
//HttpGetCheck.cs // ... public static partial class HealthCheckBuilderExtensions { public static IHealthChecksBuilder AddHttpGetCheck(this IHealthChecksBuilder builder, string url) { return builder.AddTypeActivatedCheck<HttpGetCheck>($"UrlGetCheck {url}", null, null, url); } } public class HttpGetCheck : IHealthCheck { private string url; private ILogger<HttpGetCheck> logger; public HttpGetCheck(ILogger<HttpGetCheck> logger, string url) { this.url = url; this.logger = logger; logger.LogCritical("HttpGetCheck Init"); } public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { IDictionary<string, Object> data = new Dictionary<string, object>(); using (var client = new HttpClient()) { try { var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); logger.LogCritical("HttpGetCheck Healthy"); } catch (Exception ex) { logger.LogCritical("HttpGetCheck Unhealthy"); return HealthCheckResult.Unhealthy(ex.Message, ex); } } ReadOnlyDictionary<string, Object> rodata = new ReadOnlyDictionary<string, object>(data); return HealthCheckResult.Healthy($"{url} is Healthy", rodata); } } }
We can create more checks or use Nuget packages to add them Xabaril has an extensive set of health check NuGet packages you can access them from https://www.nuget.org/profiles/Xabaril
you can also check the source code of them from https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/tree/master/src
I also have some of mine (I don’t have NuGet packages for them yet tho) https://github.com/mmercan/sentinel/tree/master/HealthChecks
generally seeing only Healthy or Unhealthy message won’t be enough especially if you are checking a broad set of things you may want to see what is failing.UseHealthChecks have options to configure and one of them is ResponseWriter which can be configured to produce more detailed outputs
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHealthChecks("/Health/isAliveAndWell") { ResponseWriter = WriteListResponse, }); // ... } public static Task WriteListResponse(HttpContext httpContext, HealthReport result) { httpContext.Response.ContentType = "application/json"; var json = new JObject( new JProperty("status", result.Status.ToString()), new JProperty("duration", result.TotalDuration), new JProperty("results", new JArray(result.Entries.Select(pair => new JObject( new JProperty("name", pair.Key.ToString()), new JProperty("status", pair.Value.Status.ToString()), new JProperty("description", pair.Value.Description), new JProperty("duration", pair.Value.Duration), new JProperty("type", pair.Value.Data.FirstOrDefault(p => p.Key == "type").Value), new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value)))), new JProperty("exception", pair.Value.Exception?.Message) ) )))); return httpContext.Response.WriteAsync(json.ToString(Newtonsoft.Json.Formatting.Indented)); }
we are almost done, rest is just optional things you can do to make it nicer.
Add IsAlive Middleware to have a common URL between APIs to check their accessibilities
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHealthChecks("/Health/isAliveAndWell") { ResponseWriter = WriteListResponse, }); app.Map("/Health/IsAlive", (ap) => { ap.Run(async context => { context.Response.ContentType = "application/json"; await context.Response.WriteAsync("{\"IsAlive\":true}"); }); }); // ... }
Add Swagger Paths for IsAlive and IsAliveAndWell for API Management or AutoRest etc..
app.UseSwagger(e => { e.PreSerializeFilters.Add((doc, req) => { doc.Definitions.Add("HealthReport", HealthReport); doc.Paths.Add("/Health/IsAliveAndWell", new PathItem { Get = new Operation { Tags = new List<string> { "HealthCheck" }, Produces = new string[] { "application/json" }, Responses = new Dictionary<string, Response>{ {"200",new Response{Description="Success", Schema = new Schema{Items=HealthReport}}}, {"503",new Response{Description="Failed"}} } } }); doc.Paths.Add("/Health/IsAlive", new PathItem { Get = new Operation { Tags = new List<string> { "HealthCheck" }, Produces = new string[] { "application/json" }, Responses = new Dictionary<string, Response>{ {"200",new Response{Description="Success", Schema = new Schema{}}}, {"503",new Response{Description="Failed"}} } } }); }); });
What if I want to Secure the health check with Authentication?
Authentication is not supported out of the box, but nothing is blocking us to modify the Middleware and check if the user authenticated or not.
I already created one for one my project except checking user IsAuthenticated with httpContext.User.Identity.IsAuthenticated and throwing 401 if not everything else same with original class.
https://github.com/mmercan/sentinel/tree/master/HealthChecks/Mercan.HealthChecks.Common/Builder
What if have to use an older .Net Core or Framework?
If you are using .Net Core 2.2 or Above you are lucky you can start running the Health-Check today,
If not, think is it hard to update to 2.2 (3.0 is coming too), I know “if it ain’t broke, don’t fix it” but you also accept the vulnerabilities you are not aware when you write your code.
https://www.cvedetails.com/vulnerability-list/vendor_id-26/product_id-42998/Microsoft-Asp.net-Core.html
After seeing the vulnerabilities and still want to stay with this (like one of my internal project) you can retrofit Health-Check libraries to Full Framework or .Net Core 1.0 – 2.1 (if you are interested with retrofit please write a comment I might add it to GitHub after some clean up )
if you are interested in source code of Health Check, you can find them in 2 GitHub repositories
https://github.com/aspnet/Extensions/tree/master/src/HealthChecks/HealthChecks (Main Repo)
https://github.com/aspnet/AspNetCore/tree/master/src/Middleware/HealthChecks/src (Asp.Net Core Middleware)