Hi! 👋

Chances are you’ve found your way here because Serilog.Settings.Configuration’s runtime reconfiguration logic only supports minimum levels, and not any other sink parameters. This stems from a strong preference for immutability (and hence clean concurrency) in the Serilog design, but the question remains: if I need to update a sink parameter at runtime, how can I do that?

Sinks and configuration

Serilog sinks often need to be configured with some information about where a log event should be sent. The Seq sink, for example, accepts a URL and optional API key for the target Seq server:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Seq("https://seq.example.com")
    .CreateLogger();

Whether it’s a URL like the one above, a connection string, SMTP server, or some other detail, if it needs to change at runtime without reloading the app, this post has you covered.

Introducing Serilog.Sinks.Map

This great little package is normally used to route log events to different destinations based on event properties (custom ones, and typical ones like the level). To do that, Serilog.Sinks.Map tracks a collection of sinks, initializing, flushing, and disposing of them as required.

We’ll configure it to keep only a single Seq sink open, but replace it whenever the value of the server URL changes:

// dotnet add package serilog.sinks.map

Log.Logger = new LoggerConfiguration()
    .WriteTo.Map(
        _ => GetServerUrl(),
        (url, writeTo) => writeTo.Seq(url),
        sinkMapCountLimit: 1)
    .CreateLogger();

GetServerUrl() in this example is your code, a function string GetServerUrl() that returns the current address to configure the sink with. Whenever the return value of the function changes, Serilog.Sinks.Map will invoke the callback in the second argument to reconfigure the sink.

Via JSON configuration

If the configuration you need is coming from appsettings.json, you’ll need to do a little more work to hook into the change detection behavior implemented by the Microsoft.Extensions.Configuration system.

This example assumes that you’re using ASP.NET Core 7:

// In Program.cs

var builder = WebApplication.CreateBuilder(args);

// <snip>

var configVersion = 0L;

builder.Host.UseSerilog((host, lc) => lc
    .WriteTo.Map(
        _ => Interlocked.Read(ref configVersion),
        (_, writeTo) => writeTo
            .Logger(sub => sub.ReadFrom.Configuration(host.Configuration)),
        sinkMapCountLimit: 1));

var app = builder.Build();

ChangeToken.OnChange<object?>(
    () => app.Configuration.GetReloadToken(),
    _ => Interlocked.Increment(ref configVersion),
    null);

And remarkably, that’s enough to have your entire Serilog pipeline reloaded whenever a configuration change occurs. 🙌

Update: this does not work for MinimumLevel, because levels have already been processed by the time they reach the sink. So… almost your entire Serilog pipeline 😉.

Thanks to Brad Lindberg for raising this question recently on GitHub.