Filtering Exception Messages with Serilog Expressions

Khalid Abuhakmeh |

Working with Duende IdentityServer customers, we’ve noticed many developers adopting Serilog as their preferred logging framework, in addition to the ILogger abstractions found in ASP.NET Core. There’s a lot to love about the simple yet powerful logging library built with powerful structured event data in mind: Easy installation, straightforward configuration, multiple target sinks, fantastic documentation, and a large community of .NET developers.

It’s so great that we ship it as part of our templates to help developers adopt what we view as a great approach to logging information, warnings, and, in rare cases, exceptions.

This post highlights an underrated feature of the Serilog family of extensions: Serilog Expressions.

How to use Serilog?

Before diving into the Serilog Expressions library, let’s first look at how you may incorporate Serilog into your existing ASP.NET Core applications, which is similar to how our Duende IdentityServer templates do as well.

You’ll first need to install the Serilog.AspNetCore package to your web application project.

dotnet add package Serilog.AspNetCore

From here, we’ll want to start with a simple Serilog configuration in our appSettings.json file to help set a baseline for all logged messages.

{
	"Serilog": {
    	"MinimumLevel": {
        	"Default": "Debug",
        	"Override": {
            	"Microsoft": "Warning",
            	"Microsoft.Hosting.Lifetime": "Information",
            	"Microsoft.AspNetCore.Authentication": "Debug",
            	"System": "Warning"
        	}
    	}
	}
}

Your configuration needs a top-level Serilog entry, which we’ll use in our Program.cs file.

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((ctx, lc) => lc
	.WriteTo.Console(
    	outputTemplate:
    	"[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}",
    	formatProvider: CultureInfo.InvariantCulture)
	.Enrich.FromLogContext()
	.ReadFrom.Configuration(ctx.Configuration));

With this configuration change, all ILogger instances will now be instances of SerilogLogger. You can change the registration and configuration to meet your application needs and suit your deployment use cases.

Now that we have Serilog, how do we filter messages based on expressions?

Serilog Expressions and Skip Logging Exceptions

Duende IdentityServer customers often deploy to public cloud environments, where their instances may intermittently experience transient errors. These errors are unavoidable for many reasons, including cloud providers experiencing sudden networking issues, bots crawling and disconnecting from public endpoints, and many other non-critical problems that cause bloat in your logs.

Reducing noise in your production logs can help you quickly find meaningful exceptions and take actions to fix those issues. With Serilog Expressions, you can filter out annoying exceptions using a mini-language.

To get started, you’ll first need to install the Serilog.Expressions package.

dotnet add package Serilog.Expressions

Next, confirm that we will log exceptions by adding a new /oops endpoint to our ASP.NET Core application.

app.MapGet("/oops", (ILogger<Program> logger) =>
{
	var ex = new Exception("DUENDEOOPSIES: NOT A REAL ERROR");
	logger.LogError(ex, "oops");
	return Results.Ok("It's All Good!");
});

Running our instance of Duende IdentityServer, we should see the following exception in our console output.

[12:01:02 Error] Program
oops
System.Exception: DUENDEOOPSIES: NOT A REAL ERROR

Let’s filter this out by adding a Filter section to our Serilog configuration. Don’t forget to add the Using element, which allows Serilog to load the library dependency at runtime.

{
  "Serilog": {
	"Using": [ "Serilog.Expressions" ],
	"MinimumLevel": {
  		"Default": "Debug",
  		"Override": {
    			"Microsoft": "Warning",
    			"Microsoft.Hosting.Lifetime": "Information",
    			"Microsoft.AspNetCore.Authentication": "Debug",
    			"System": "Warning"
  		}
	},
	"Filter": [
  		{
    			"Name": "ByExcluding",
    			"Args": {
      				"expression": "Inspect(@x).Message like 'DUENDEOOPSIES:%'"
    			}
  		}
	]
  }
}

The critical part is using the ByExcluding filter method to pass an expression. The mini-language syntax provides an Inspect keyword, which allows us to unwrap the exception and then compare the message to a known string and a suffixed wildcard.

After rerunning the application, you can see that while the code at the endpoint remains unchanged, the console output no longer displays the exception.

Conclusion

While the example in this post is an everyday use case for Duende IdentityServer customers, the Serilog Expression library has an impressive syntax that should help anyone adopt Serilog filter logging messages. For more information, we recommend checking out the Serilog Expressions language reference on the GitHub page.

If you already use Serilog and Serilog Expressions in your production environment, please comment below and let us know what other valuable expressions you’ve deployed.