Injecting services into Serilog filters, enrichers, and sinks
Hey! 👋 You might have missed a change that snuck quietly into Serilog.AspNetCore and Serilog.Extensions.Hosting just recently. It’s now a lot easier to inject services from your dependency injection (DI) container into your Serilog pipeline.
What was the problem?
ASP.NET Core makes extensive use of dependency injection. If a component (other than a controller) needs to interact with ASP.NET Core, it must do so using one of the services provided by the framework through dependency injection.
For example, to enrich Serilog events with the name of the logged-in user, a Serilog ILogEventEnricher
needs an instance of ASP.NET Core’s IHttpContextAccessor
:
class UserNameEnricher : ILogEventEnricher
{
readonly IHttpContextAccessor _httpContextAccessor;
// IHttpContextAccessor supplied through constructor injection
public UserNameEnricher(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
if (!(_httpContextAccessor.HttpContext?.User.Identity.IsAuthenticated ?? false))
return;
// Access the name of the logged-in user
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userNameProperty = factory.CreateProperty("UserName", userName);
logEvent.AddPropertyIfAbsent(userNameProperty);
}
}
To have the IHttpContextAccessor
injected into our UserNameEnricher
, we need to register the enricher type with the dependency injection container:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<UserNameEnricher>();
services.AddHttpContextAccessor();
This raises the question: how can we get an instance of the enricher from the container into our Serilog pipeline?
The inline UseSerilog()
overload
And this is where the recent change comes in.
The Serilog.AspNetCore integration exposes two slightly different ways of adding Serilog to the hosting infrastructure:
- Configure logging at program start-up, and provide an
ILogger
(or configured staticLog
class) toUseSerilog()
, or - Configure logging inside the callback supplied to
UseSerilog()
.
It’s the second overload we need for this:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
// Overload accepting a callback:
.UseSerilog((context, services, configuration) => configuration
.Enrich.FromLogContext()
// Add an instance of the enricher:
.Enrich.With(services.GetService<UserNameEnricher>())
.WriteTo.Console(theme: AnsiConsoleTheme.Code)
.WriteTo.Seq("http://localhost:5341"))
.ConfigureWebHostDefaults(webBuilder => webBuilder
.UseStartup<Startup>());
And it’s that simple! Here are some of ASP.NET Core’s built-in log events, now showing the UserName
property our enricher has added:
The same trick works for Serilog filters (Filter.With(ILogEventFilter)
) and sinks (WriteTo.Sink(ILogEventSink)
).
So, is there a downside?
Yes, unfortunately. By using the inline UseSerilog()
initialization overload, logging won’t be fully-configured until sometime during ASP.NET Core startup. That means that exceptions raised earlier in start-up won’t reach Serilog.
This is a tough trade-off to make; right now there’s no easy answer. I’m working on a new Serilog component that should sort this out; it’s ready to try now, but not thoroughly documented - if it’s useful to you please drop me a line and let me know.
Hope this helps!