TL;DR: Everything you need to know about formatting plain text with Serilog.Expressions, by example.

There are endless ways to format log output. With Serilog’s built-in “output templates”, you can choose the fields and text to include in log output, and use .NET format string-style alignment and width syntax, but that’s about it.

Serilog.Expressions is a fairly new library that plugs in to enable everything else. This post collects the plain text formatting questions I’ve fielded over the years, and their solutions using Serilog.Expressions. I’ve tried to keep everything short and pithy, so that I can add more examples here as they come up.

First things first…

To use the code in this post, you’ll need to add the Serilog.Expressions NuGet package:

dotnet add package Serilog.Expressions

Then, add a using statement for the Serilog.Templates namespace, which contains the types we’re interested in:

using Serilog.Templates;

You’ll also need to install the corresponding NuGet packages for the Serilog sink(s) that will output the text.

ITextFormatter and Serilog sinks

All Serilog sinks that output plain text have an overload accepting an ITextFormatter. The examples use the ExpressionTemplate class, which implements this interface.

Here’s how it looks with Serilog.Sinks.Console:

    .WriteTo.Console(new ExpressionTemplate("..."))

And here’s how it looks with Serilog.Sinks.File:

    .WriteTo.File(new ExpressionTemplate("..."), "log.txt")

Similar overloads exist for Email(), Debug(), and many others.

JSON and XML configuration doesn’t support ExpressionTemplate just yet, so you’ll need to configure your sink in C# code to use it.

Examples

The examples all use built-in properties like @t (timestamp), literal values like 'hello' (a string), and so-on. If you’re keen to dig further into the templating language, you can find a lot of info in the full language reference.

📝 Include SourceContext in log output

Loggers created with .ForContext<T>() (or using MEL’s ILogger<T>) include the type T as a property on the event. To show this, use the property name SourceContext directly:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3} {SourceContext}] {@m}\n{@x}"))

Produces:

[11:36:40 INF MyApp.Services.Service1] Connecting on port 10211

📝 Formatting SourceContext as a simple class name

SourceContext is usually the namespace-qualified name of the type that created a log event, e.g. MyApp.Services.Service1. If you want to show the source as just the class name Service1, use the Substring() and LastIndexOf() functions:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3} " + 
        "{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}] {@m}\n{@x}"))

Produces:

[11:36:40 INF Service1] Connecting on port 10211

The arguments to Substring() are the string to slice, the zero-based start index, and an optional length (not used in this example).

📝 Showing a default value when a property is missing

If a property like SourceContext that appears in the output may sometimes be missing, you can show a placeholder like <none> using the Coalesce() function:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3} {Coalesce(SourceContext, '<none>')}] {@m}\n{@x}"))

Produces:

[11:36:41 INF <none>] Application shutting down

The Coalesce() function accepts a variable number of arguments, and will return the first of them that is not null or undefined.

📝 Conditionally including some formatted text

If a section that appears in the output may sometimes be missing, you can conditionally show it using {#if} and {#end}:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}" +
        "{#if SourceContext is not null} ({SourceContext}){#end}] {@m}\n{@x}"))

Produces:

[11:36:40 INF (MyApp.Services.Service1)] Connecting on port 10211
[11:36:41 INF] Application shutting down

Conditional {#if} supports {#else if} and {#else}, too.

📝 Including a sub-property from structured data

If a value you want to show in the output is nested inside an object like ASP.NET Core’s EventId, use . to access it:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3} {EventId.Name}] {@m}\n{@x}"))

Produces:

[11:36:41 INF ShutDown] Application shutting down

Templates support paths with dotted accessors and bracketed [] indexers, just like C#.

📝 Showing a UTC timestamp

Serilog uses local timestamps with an offset from UTC (DateTimeOffset). To show a timestamp in its UTC represenation, call UtcDateTime():

    .WriteTo.Console(new ExpressionTemplate(
        "[{UtcDateTime(@t):o} {@l:u3}] {@m}\n{@x}"))

Produces:

[2021-06-04T01:36:40.4030003Z] Connecting on port 10211

The :o in the timestamp token above is just a regular .NET date time format string; o produces ISO-8601 timestamps.

📝 Padding a value

To give a property with variable length a consistent width in the output, use ,N to pad it:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}] {SourceContext,26} {@m}\n{@x}"))

Produces:

[11:36:40 INF] MyApp.Services.Service1    Connecting on port 10211

📝 Right-aligning a value

To right-align a padded value, prefix the number of padding spaces with -:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}] {SourceContext,-26} {@m}\n{@x}"))

Produces:

[11:36:40 INF]    MyApp.Services.Service1 Connecting on port 10211

📝 Showing levels as upper- or lowercase one, two, three, four, or five-character names

The level token ({@l}) can display shortened level names in lower (inf), upper (WRN), and title (Err) case. To do this, add a format string with the number of characters, and an optional u for uppercase, w for lowercase (title case is the default):

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:w4}] {@m}\n{@x}"))

Produces:

[11:36:40 info] Connecting on port 10211

📝 Listing each member of an object on its own line

Use {#each} to repeat part of the template for all of the members of an object like @p:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}] {@m}\n" +
        "{#each name, value in @p}   {name} = {value}\n{#end}{@x}"))

Produces:

[11:36:40 INF] Connecting on port 10211
    Application = MyApp
    Port = 10211
    SourceContext = MyApp.Services.Service1

Notice that the {#each} directive gives names to variables that hold each member name and value inside the repeated block.

📝 Listing only “hidden” properties not otherwise present in the message or template

The Rest() function returns an object that contains all properties of the event that aren’t mentioned elsewhere in the template:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3} {SourceContext}] {@m} {Rest()}\n{@x}"))

Produces:

[11:36:40 INF MyApp.Services.Service1] Connecting on port 10211 {"Application":"MyApp"}

The SourceContext property is not included in the object because it appears in the expression template. The Port property is not included either, because it appears in the event’s message.

📝 Adding a delimiter between elements of an array

If a property like Scope contains an array of values, the {#delimit} directive can specify a block of text to render between each value:

    .WriteTo.Console(new ExpressionTemplate(
        "{@l:w4}: {SourceContext}\n" +
        "{#if Scope is not null}" +
        "      {#each s in Scope}=> {s}{#delimit} {#end}\n" +
        "{#end}" +
        "      {@m}\n" +
        "{@x}"))

Produces:

info: MyApp.Api.GreetingsController
      => Invoke Action => Say Hello
      HTTP GET /api/hello responded 200 in 1.230 ms

📝 Including a property with an inconsistently-cased name

If a property like ThreadId is sometimes misspelled as ThreadID or threadId, use ElementAt() and ci to find it:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}] {@m} ({ElementAt(@p, 'threadid') ci})\n{@x}"))

Produces:

[11:36:40 INF] Connecting on port 10211 (293)

📝 Formatting numbers and dates in a specific locale

If log output should always use one locale regardless of the user’s locale setting, pass the corresponding CultureInfo when constructing the ExpressionTemplate:

    .WriteTo.Console(new ExpressionTemplate(
        "[{@t:HH:mm:ss} {@l:u3}] {@m}\n{@x}",
        formatProvider: CultureInfo.GetCultureInfoByIetfLanguageTag("fr-FR")))

Produces:

[11:36:45 INF] Total execution time was 11,2 ms

Learning more

Believe it or not, there’s a lot more to expression templates than we’ve covered, here! If you can dream it, ExpressionTemplate can probably format it 😄.

The best place to ask questions is Stack Overflow’s serilog tag. I keep an eye on that tag, and will update this post with new examples as I spot them.

If you think you have found a bug in Serilog.Expressions, or would like to request a missing feature, the GitHub project is the place to go.