Excluding health check endpoints from Serilog request logging: Using Serilog.AspNetCore in ASP.NET Core 3.0 - Part 4
Tags: aspnetcore, logging
Published: 2020-01-27 22:54:29

---
title: Excluding health check endpoints from Serilog request logging: Using Serilog.AspNetCore in ASP.NET Core 3.0 - Part 4
published: true
date: 2020-01-21 10:00:00 UTC
tags: ASP.NET Core,Logging
canonical_url: https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-excluding-health-check-endpoints-from-serilog-request-logging/
---

 ![Excluding health check endpoints from Serilog request logging](https://andrewlock.net/content/images/2019/serilog_banner4.png)

In previous posts in this series I have described how to configure Serilog's RequestLogging middleware to add additional properties to Serilog's request log summary such as the request hostname or the selected endpoint name. I have also showed how to add MVC or RazorPage-specific properties to the summary log using Filters.

In this post I show how to _skip_ adding the summary log message completely for specific requests. This can be useful when you have an endpoint that is hit a lot, where logging every request is of little value.

## Health checks are called a _lot_

The motivation for this post comes from behaviour we've seen when running applications in Kubernetes. Kubernetes uses two types of "health checks" (or "probes") for checking applications are running correctly - liveness probes and readiness probes. You can configure a probe to make an HTTP request to your application as an indicator that your application is running correctly.

> As of Kubernetes version 1.16 there is a third type of probe, [startup probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes).

The [Health Check endpoints](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) available in ASP.NET Core 2.2+ are ideally suited for these probes. You can set up a simple, dumb, health check that returns a `200 OK` response to every request, to let Kubernetes know your app hasn't crashed.

In ASP.NET Core 3.x, you can configure your health checks using endpoint routing. You must add the required services in _Startup.cs_ by calling `AddHealthChecks()`, and add a health check endpoint using `MapHealthChecks()`:

```
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ..other service configuration

        services.AddHealthChecks(); // Add health check services
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // .. other middleware
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHealthChecks("/healthz"); //Add health check endpoint
                endpoints.MapControllers();
            });
        }
}

```

In the example above, sending a request to `/healthz` will invoke the health check endpoint. As I didn't configure any health checks to run, the endpoint will always return a `200` response, as long as the app is running:

![Health check response](https://andrewlock.net/content/images/2019/serilog_health_check.png)

The only problem with this is that Kubernetes will call this endpoint a _lot_. The exact frequency is up to you, but every 10s is common. You want a relatively high frequency so Kubernetes can restart faulty pods quickly.

That's not a problem in of itself; Kestrel can handle millions of requests a second, so it's not a performance concern. More irritating is the number of logs generated by each request. It's [not as many as I showed for a request to the MVC infrastructure](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-reducing-log-verbosity/), but even 1 log per request (as we get with Serilog.AspNetCore) can get irritating.

The main problem is that the logs for successful health check requests don't actually tell us anything useful. They're not related to any business activities, they're purely infrastructural. It would be nice to be able to skip the Serilog request summary logs for these. In the next section I introduce the approach I came up with that relies on the changes from the previous posts in this series.

## Customising the log level used for Serilog request logs

In [a previous post](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-logging-the-selected-endpoint-name-with-serilog/) I showed how to include [the selected endpoint](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.0#endpoint-routing-differences-from-earlier-versions-of-routing) in your Serilog request logs. [My approach was to provide a custom function](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-logging-the-selected-endpoint-name-with-serilog/#setting-values-in-idiagnosticcontext) to the `RequestLoggingOptions.EnrichDiagnosticContext` property when registering the Serilog middleware:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts
        // EnrichFromRequest helper function is shown in the previous post
        => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest); 

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz"); //Add health check endpoint
        endpoints.MapControllers();
    });
}

```

`RequestLoggingOptions` has another property, `GetLevel`, which is a `Func<>` used to decide the logging level that should be used for a given request log. By default, [it's set to the following function](https://github.com/serilog/serilog-aspnetcore/blob/dev/src/Serilog.AspNetCore/SerilogApplicationBuilderExtensions.cs#L31-L36):

```
public static class SerilogApplicationBuilderExtensions
{
    static LogEventLevel DefaultGetLevel(HttpContext ctx, double _, Exception ex) =>
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : LogEventLevel.Information;
}

```

This function checks if an exception was thrown for the request, or if the response code is a `5xx` error. If so, it create an `Error` level summary log, otherwise it creates an `Information` level log.

Lets say you wanted the summary logs to be logged as `Debug` instead of `Information`. First, you would create a helper function like the following which has the required logic:

```
public static class LogHelper
{
    public static LogEventLevel CustomGetLevel(HttpContext ctx, double _, Exception ex) =>
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : LogEventLevel.Debug; //Debug instead of Information
}

```

Then you can set the level function when you call `UseSerilogRequestLogging()`:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts => {
        opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest;
        opts.GetLevel = LogHelper.CustomGetLevel; // Use custom level function
    });

    //... other middleware
}

```

Now your request summary logs will all be logged as `Debug`, except when an error occurs ([screenshot from Seq](https://datalust.co/seq/)):

![Seq showing debug request logs](https://andrewlock.net/content/images/2019/seq_debug.png)

But how does this help our verbosity problem?

When you configure Serilog, you typically define the minimum request level. For example, the following simple configuration sets the default level to `Debug()`, and writes to a console Sink:

```
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .CreateLogger();

```

So the simple way for a log to be filtered is for the _log level_ to be lower than the `MinimumLevel` specified in the logger configuration. Generally speaking, if you use the lowest level available, `Verbose`, it will pretty much always be filtered out.

The difficulty is that we don't want to _always_ use `Verbose` as the log level for our summary logs. If we do that, we won't get any request logs for non-errors, and the Serilog middleware becomes a bit pointless!

Instead, we want to set the log level to `Verbose` _only_ for requests that hit the health check endpoint. In the next section I'll show how to identify those requests while leaving other requests unaffected.

## Using a custom log level for Health Check endpoint requests

The key thing we need is the ability to identify a health-check request at the point the log is written. As shown previously, the `GetLevel()` method takes the current `HttpContext` as a parameter, so theoretically there's a few options. The most obvious choices to me are:

- Compare the `HttpContext.Request` path to a known list of health-check paths
- Use the selected Endpoint metadata to identify when a health-check endpoint was called

The first option is the most obvious, but it's not really worth the hassle. Once you get into the weeds of it, you find you have to start duplicating request paths around and handling various edge cases, so I'm going to skip over that one here.

The second option uses a similar approach to the one [in my previous post](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-logging-the-selected-endpoint-name-with-serilog/#setting-values-in-idiagnosticcontext) where we obtain the `IEndpointFeature` that was selected by the `EndpointRoutingMiddleware` for a given request. This feature (if present) provides details such as the display name and route data for the selected Endpoint.

If we assume that health checks are [registered using their default Display Name](https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs#L18) of `"Health checks"`, then we can identify a "health check" request using the `HttpContext` as follows:

```
public static class LogHelper
{
    private static bool IsHealthCheckEndpoint(HttpContext ctx)
    {
        var endpoint = ctx.GetEndpoint();
        if (endpoint is object) // same as !(endpoint is null)
        {
            return string.Equals(
                endpoint.DisplayName, 
                "Health checks",
                StringComparison.Ordinal);
        }
        // No endpoint, so not a health check endpoint
        return false;
    }
}

```

We can use this function, combined with a custom version of [the default `GetLevel` function](https://github.com/serilog/serilog-aspnetcore/blob/dev/src/Serilog.AspNetCore/SerilogApplicationBuilderExtensions.cs#L31-L36) to ensure that summary logs for health check requests use a `Verbose` level, while errors use `Error` and other requests use `Information`:

```
public static class LogHelper
{
    public static LogEventLevel ExcludeHealthChecks(HttpContext ctx, double _, Exception ex) => 
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : IsHealthCheckEndpoint(ctx) // Not an error, check if it was a health check
                    ? LogEventLevel.Verbose // Was a health check, use Verbose
                    : LogEventLevel.Information;
        }
}

```

This nested ternary has an extra level to it - for non-errors we check if an endpoint with the display name `"Health check"` was selected, and if it was, we use the level `Verbose`, otherwise we use `Information`.

> You could generalise this code further, to allow passing in other display names, or customising the log levels used. I didn't do that here for simplicity, but [the associated sample code on GitHub shows how you could do this](https://github.com/andrewlock/blog-examples/tree/master/SerilogRequestLogging).

All that remains is to update the Serilog middleware `RequestLoggingOptions` to use your new function:

```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts => {
        opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest;
        opts.GetLevel = LogHelper.ExcludeHealthChecks; // Use the custom level
    });

    //... other middleware
}

```

When you check your logs after running your application you'll see your normal request logs for standard requests, but no sign of your health checks (unless an error occurs!). In the following screenshot I configured Serilog to also record `Verbose` logs so you can see the health check requests - normally they will be filtered out!

![Seq screenshot showing Health Check requests using Verbose level](https://andrewlock.net/content/images/2019/seq_verbose.png)

## Summary

In this post I showed how to provide a custom function to the Serilog middleware's `RequestLoggingOptions` that defines what `LogEventLevel` to use for a given request's log. I showed how this can be used to change the default level to `Debug` for example. If the level you choose is lower than the minimum level, it will be filtered out completely and not logged.

I showed that you could use this approach to filter out the common (but low-interest) request logs generated by calling health check endpoints. Generally speaking these requests are only interesting if they indicate a problem, but they will typically generate a request log when they succeed as well. As these endpoints are called very frequently they can significantly increase the number of logs written.

The approach in this post is to check the selected `IEndpointFeature` and check if it has the display name `"Health checks"`. If it does, the request log is written with the `Verbose` level, which is commonly filtered out. For more flexibility you could customise the logs shown in this post to handle multiple endpoint names, or any other criteria you have.