Customizing Serilog text output
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.