Filter Serilog Events By Multiple EventIds In C#
Hey everyone! 👋 Today, we're diving into a common scenario when working with Serilog in C#: filtering log events based on multiple EventId
values. You might have specific events you want to capture or exclude, and knowing how to filter by EventId
is super useful.
The Challenge: Filtering by EventId
So, you're using Serilog, which is awesome! You've got your logging all set up, and you're generating events with EventId
properties. But now, you need to filter these events. Maybe you want to send certain events to a specific sink (like a database or a file), or perhaps you want to suppress noisy events. The challenge is, how do you efficiently filter when you need to match either of two (or more) EventId
values?
The Old Way (and Why It Doesn't Work)
You might stumble upon older solutions that suggest using Filter.ByIncludingOnly("EventId.Id = 2003")
. However, this approach is outdated and won't work with the latest Serilog versions. Serilog's filtering mechanism has evolved, and we need to use a more modern approach.
Modern Solutions for Filtering by Multiple EventIds
Okay, let's get into the good stuff! There are a couple of excellent ways to filter Serilog events by multiple EventId
values. We'll explore two primary methods:
1. Using Filter.ByIncludingOnly()
with a Lambda Expression
This is the recommended and most flexible approach. We'll use a lambda expression within Filter.ByIncludingOnly()
to define our filtering logic. This allows us to check if the EventId.Id
matches any of our desired values.
Let's break down how to do it. Imagine you want to filter events with EventId
101 or 202. Here's the C# code:
using Serilog;
using Serilog.Events;
public class Example
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Filter.ByIncludingOnly(e => e.Properties.ContainsKey("EventId") &&
(e.Properties["EventId"] as StructureValue)?.Properties
.Any(p => p.Name == "Id" && (p.Value as ScalarValue)?.Value is int id && (id == 101 || id == 202)) == true)
.WriteTo.Console()
.CreateLogger();
Log.Information("This is an informational message.");
Log.Information(new EventId(101, "ImportantEvent"), "This is an important event with Id 101");
Log.Information(new EventId(202, "AnotherImportantEvent"), "This is another important event with Id 202");
Log.Information(new EventId(303, "UnrelatedEvent"), "This event should not be included.");
Log.CloseAndFlush();
}
}
Dissecting the Code
Let's walk through this code snippet step by step:
Log.Logger = new LoggerConfiguration()
: We start by configuring our Serilog logger..MinimumLevel.Information()
: We set the minimum log level toInformation
, meaning we'll captureInformation
,Warning
,Error
, andFatal
events..Filter.ByIncludingOnly(e => ...)
: This is where the magic happens! We useFilter.ByIncludingOnly()
to specify a filter based on a lambda expression (e => ...
). Thee
represents aLogEvent
.e.Properties.ContainsKey("EventId")
: First, we check if the log event has anEventId
property. This is crucial because not all log events will have anEventId
.(e.Properties["EventId"] as StructureValue)?.Properties ...
: Here, we're digging into the structure of theEventId
property.EventId
is typically stored as aStructureValue
in Serilog. We need to access its properties..Any(p => p.Name == "Id" && (p.Value as ScalarValue)?.Value is int id && (id == 101 || id == 202))
: This is the core of our filtering logic. We use theAny()
method to check if any of the properties within theEventId
structure meet our criteria. Let's break this down further:p.Name == "Id"
: We're looking for the property named "Id" within theEventId
structure (this is where the numeric ID is stored).(p.Value as ScalarValue)?.Value is int id
: We cast the value of the "Id" property to aScalarValue
and then extract its underlying value. We also check if it's an integer and store it in theid
variable.(id == 101 || id == 202)
: Finally, we check if theid
matches either 101 or 202. If it does, theAny()
method returnstrue
, and the log event passes the filter.
== true
: This explicit comparison totrue
ensures that we're only including events where theAny()
method returnedtrue
..WriteTo.Console()
: We're writing the filtered events to the console. You can replace this with any other Serilog sink (e.g., file, database, etc.)..CreateLogger()
: We create the logger instance.Log.Information(...)
: We log various messages, including those withEventId
101, 202, and 303, to demonstrate the filtering.Log.CloseAndFlush()
: We ensure all log events are written before the application exits.
Why This Approach Rocks
- Flexibility: Lambda expressions give you incredible flexibility. You can create complex filtering logic, including checking multiple properties and using various conditions.
- Readability: While the lambda expression might look a bit daunting at first, it's quite readable once you understand the structure of Serilog's
LogEvent
andEventId
representation. - Maintainability: This approach is easy to maintain and modify. If you need to add or remove
EventId
values, you can simply update the(id == 101 || id == 202)
part of the expression.
2. Creating a Custom Filter Class
For more complex scenarios or when you want to reuse your filtering logic, creating a custom filter class is an excellent option. This approach involves implementing the ILogEventFilter
interface.
Here's how you can create a custom filter class for matching multiple EventId
values:
using Serilog.Core;
using Serilog.Events;
using System.Collections.Generic;
using System.Linq;
public class EventIdFilter : ILogEventFilter
{
private readonly HashSet<int> _eventIds;
public EventIdFilter(IEnumerable<int> eventIds)
{
_eventIds = new HashSet<int>(eventIds);
}
public bool IsEnabled(LogEvent logEvent)
{
if (logEvent.Properties.TryGetValue("EventId", out var eventIdValue) &&
eventIdValue is StructureValue structureValue)
{
var idProperty = structureValue.Properties.FirstOrDefault(p => p.Name == "Id");
if (idProperty?.Value is ScalarValue scalarValue && scalarValue.Value is int id)
{
return _eventIds.Contains(id);
}
}
return false;
}
}
Diving into the Custom Filter
public class EventIdFilter : ILogEventFilter
: We define a class namedEventIdFilter
that implements theILogEventFilter
interface. This interface has a single method:IsEnabled()
.private readonly HashSet<int> _eventIds;
: We use aHashSet<int>
to store theEventId
values we want to match. AHashSet
provides efficient lookups (O(1) complexity).public EventIdFilter(IEnumerable<int> eventIds)
: The constructor takes anIEnumerable<int>
ofEventId
values and initializes the_eventIds
set.public bool IsEnabled(LogEvent logEvent)
: This is the heart of our filter. It's called for each log event, and it returnstrue
if the event should be included (i.e., it matches our filter criteria) andfalse
otherwise.if (logEvent.Properties.TryGetValue("EventId", out var eventIdValue) && eventIdValue is StructureValue structureValue)
: Similar to the lambda expression approach, we first check if the log event has anEventId
property and if its value is aStructureValue
.var idProperty = structureValue.Properties.FirstOrDefault(p => p.Name == "Id");
: We try to find the property named "Id" within theEventId
structure.if (idProperty?.Value is ScalarValue scalarValue && scalarValue.Value is int id)
: If we found the "Id" property, we check if its value is aScalarValue
and if its underlying value is an integer.return _eventIds.Contains(id);
: Finally, we check if the extractedid
is present in our_eventIds
set. If it is, we returntrue
(the event should be included); otherwise, we returnfalse
.return false;
: If any of the checks fail (e.g., the log event doesn't have anEventId
, or the "Id" property is not an integer), we returnfalse
.
Using the Custom Filter
To use this custom filter, you'll need to incorporate it into your Serilog configuration:
using Serilog;
using Serilog.Events;
using System.Collections.Generic;
public class Example
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Filter.ByIncludingOnly(new EventIdFilter(new List<int> { 101, 202 }))
.WriteTo.Console()
.CreateLogger();
Log.Information("This is an informational message.");
Log.Information(new EventId(101, "ImportantEvent"), "This is an important event with Id 101");
Log.Information(new EventId(202, "AnotherImportantEvent"), "This is another important event with Id 202");
Log.Information(new EventId(303, "UnrelatedEvent"), "This event should not be included.");
Log.CloseAndFlush();
}
}
We simply create an instance of our EventIdFilter
, passing in the list of EventId
values we want to match, and then use it within Filter.ByIncludingOnly()
.
Benefits of a Custom Filter
- Reusability: You can reuse this filter across multiple logging configurations.
- Testability: Custom filters are easier to unit test because you can directly test the
IsEnabled()
method. - Complexity Handling: For very complex filtering logic, a custom filter class can make your configuration cleaner and more organized.
Choosing the Right Approach
So, which approach should you use? Here's a quick guide:
- Lambda Expression: Use this for simple filtering logic that's specific to a single logging configuration. It's quick and easy for straightforward scenarios.
- Custom Filter Class: Use this for more complex filtering logic, when you need to reuse the filter across multiple configurations, or when you want to improve testability.
Key Takeaways
- Filtering by
EventId
in Serilog is essential for managing your logs effectively. - The outdated
Filter.ByIncludingOnly("EventId.Id = 2003")
approach no longer works. - Lambda expressions within
Filter.ByIncludingOnly()
provide a flexible and readable way to filter by multipleEventId
values. - Custom filter classes offer reusability, testability, and better organization for complex filtering scenarios.
Wrapping Up
Filtering Serilog events by multiple EventId
values is a powerful technique for controlling which events are processed by your sinks. Whether you choose the lambda expression approach or create a custom filter class, you now have the tools to efficiently manage your logs. Happy logging, guys! 🎉