Serilog.Expressions is a little library that implements expression-based filtering and formatting of structured log events.

// dotnet add package serilog.expressions
// dotnet add package serilog.sinks.console
// using Serilog;

using var log = new LoggerConfiguration()
    .Filter.ByExcluding("@m like 'Hello%' and Name = 'world'")
    .WriteTo.Console()
    .CreateLogger();

// Logged normally
log.Information("Hello, {Name}!", "reader");

// Excluded by the filter
log.Information("Hello, {Name}!", "world");

It’s useful for configuring Serilog in appsettings.json or Web.config files, so special attention is given to how it appears when embedded in C#, JSON, or XML:

  • strings are 'single-quoted', so the quotes don’t need to be \"escaped\" when they appear in double-quoted string literals,
  • backslash isn’t an escape character, so you don’t end up with \\\\ when you need a literal backslash, and
  • logical conjunction is and, so you don’t need && to use it in XML attribute values.

You can read a bit more about it in this introduction and this follow-up on how the language can be extended with user-defined functions.

I thought it might be worth writing a quick update on the project, a), to let you know what’s new, and b), to send a heads-up that I’m still pushing forwards! Serilog.Expressions fills a lot of gaps in the Serilog user experience, and I think it’s placed to become a fundamental part of the Serilog ecosystem.

What’s new in 2.0?

As a formatting language, Serilog.Expressions should be able to produce a wide range of text-based output formats.

Here’s the kind of output you’ll see at the terminal if you run an ASP.NET Core project with default (non-Serilog) logging:

info: Sample.Program
      Starting up
info: Sample.Program
      => Main => SayHello()
      Hello, world!

This is quite different from Serilog’s style. In particular, the event’s scope is unpacked onto its own line, and this line only appears when there’s some scope information available.

If we replace the default logging with Serilog’s, can we keep producing the same output?

Two new features in Serilog.Expressions 2.0 help with this:

  • Repetition using {#each ... in ...} lets us control how the elements of Scope are rendered, and
  • A conditional block using {#if ...} will avoid a blank line when an event has no scope information attached.

Here’s how it looks:

// using Serilog.Templates;

using var log = new LoggerConfiguration()
    .WriteTo.Console(new ExpressionTemplate(
        "{@l:w4}: {SourceContext}\n" +
        "{#if Length(Scope) > 0}" +
        "      {#each s in Scope}=> {s}{#delimit} {#end}\n" +
        "{#end}" +
        "      {@m}\n" +
        "{@x}"))
    .CreateLogger();

ExpressionTemplate(), the formatting of the level with {@l:w4} (level property, lowercase, four characters) and {SourceContext} might be familiar from earlier examples.

How do the new and shiny features work?

Conditional blocks

The {#if Length(Scope) > 0} part is a template directive - you can spot the familiar # used by the C# preprocessor for similar purposes - and these are new in 2.0.

The block controlled by the directive is between {#if ...} and {#end}, and the syntax supports chaining of optional {#else if ...} and {#else} blocks.

Repetition

Inside the conditional block, you can see {#each s in Scope}. The {#each} directive repeats the following block once for every element in an array (or property in an object). The name we give the interation variable, s, can be used within the block the way we’ve used {s} here.

Repetition uses {#delimit} to optionally specify a block of output to include between each element. The example uses this to put a single space between each scope item. Not shown is {#else}, which specifies alternative output for when the collection being iterated over is missing, null, or empty.

This is admittedly quite a lot to digest, but keep in mind that if you did set out to emulate this format by hand, you’d need quite a few more lines of code than the six-line template above.

What’s next?

Here’s the rough roadmap I’m working to on this project:

  1. Runtime operations (like ToString() formatting) should be culture-aware; there’s some API and back-end work in this; currently tracked by #19.
  2. Theme support - it’s no fun having infinite fomatting flexibility without terminal theme support! Tracked by #8.
  3. Trim-down and optimization - runtime performance hasn’t had a lot of attention recently; it should still be acceptable for its use-case, but I’ll bet there are some low-hanging fruit. The package also has a dependency on Superpower; to reduce the overall footprint of Serilog.Expressions, interning this and manually trimming out all of the unused features could be worthwhile.
  4. Direct support in Serilog.Settings.Configuration and Serilog.Settings.AppSettings; it should be possible to define output formats using Serilog.Expressions in JSON or XML without jumping through too many hoops.

I’d love to hear your thoughts on Serilog.Expressions if you’re using it currently, and if you’re keen to lend a hand please drop by and say “hello” on GitHub.

Otherwise, I’ll keep you posted! 🙂 👋