12

There is an answer here: How do I pass a dependency to a Serilog Enricher? which explains you can pass an instance in.

However to do that I would need to move my logger setup after my dependency injection code has ran (in the startup.cs)

This means that startup errors won't be logged because the logger won't be ready yet.

Is there a way to somehow configure serilog to run in my Main() method, but also enrich data with a DI item? The DI item has further dependencies (mainly on database connection) although it is a singleton.

I've googled this and read something about adding things to a context, but I've been unable to find a complete working example that I can adapt.

Most of the examples I've found involve putting code into the controller to attach information, but I want this to be globally available for every single log entry.

My Main starts with :

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(elasticUri))
    {
        AutoRegisterTemplate = true,
    })
    .CreateLogger();

Before going into the .NET Core MVC code

CreateWebHostBuilder(args).Build().Run();

My DI object is basically a "UserData" class that contains username, companyid, etc. which are properties that hit the database when accessed to get the values based on some current identity (hasn't been implemented yet). It's registered as a singleton by my DI.

NibblyPig
  • 51,118
  • 72
  • 200
  • 356
  • 1
    Check out [Inline Initialization](https://github.com/serilog/serilog-aspnetcore#inline-initialization) - I think dependencies can be resolved from `hostingContext` in that case. It means taking the disadvantageous route of having logging depend on DI setup, however (I think you're on the happy path currently, setting up logging before anything else - maybe you should consider grabbing the values from configuration directly?) – Nicholas Blumhardt Aug 12 '19 at 22:16
  • 2
    **Update:** `CreateBootstrapLogger()` (described in [this post](https://nblumhardt.com/2020/10/bootstrap-logger/) ) now fully supports this scenario – Nicholas Blumhardt Dec 16 '20 at 00:12

3 Answers3

13

I would suggest using a simple middleware that you insert in the ASP .NET Core pipeline, to enrich Serilog's LogContext with the data you want, using the dependencies that you need, letting the ASP .NET Core dependency injection resolve the dependencies for you...

e.g. Assuming IUserDataService is a service that you can use to get the data you need, to enrich the log, the middleware would look something like this:

public class UserDataLoggingMiddleware
{
    private readonly RequestDelegate _next;

    public UserDataLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IUserDataService userDataService)
    {
        var userData = await userDataService.GetAsync();

        // Add user data to logging context
        using (LogContext.PushProperty("UserData", userData))
        {
            await _next.Invoke(context);
        }
    }
}

LogContext.PushProperty above is doing the enrichment, adding a property called UserData to the log context of the current execution.

ASP .NET Core takes care of resolving IUserDataService as long as you registered it in your Startup.ConfigureServices.

Of course, for this to work, you'll have to:

1. Tell Serilog to enrich the log from the Log context, by calling Enrich.FromLogContext(). e.g.

Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(Configuration)
    .Enrich.FromLogContext() // <<======================
    .WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
                        "{Properties:j}{NewLine}{Exception}")
    .CreateLogger();

2. Add your middleware to the pipeline, in your Startup.Configure. e.g.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ...

    app.UseMiddleware<UserDataLoggingMiddleware>();

    // ...

    app.UseMvc();
}
C. Augusto Proiete
  • 24,684
  • 2
  • 63
  • 91
  • I like this approach of using a Middleware. At least until Serilog's `.Enrich.With` can handle DI (if ever). Serilog seems to be going the wrong (IMO) direction by providing [access to IServiceCollection in UseSerilog](https://nblumhardt.com/2020/09/serilog-inject-dependencies/), forcing you to call `GetService` manually. – Mathias R Dec 14 '20 at 18:29
  • @MathiasR Yeah... DI in the logging pipeline can be a kind of a chicken-and-egg problem, as it's common to set up logging before setting up the DI container (and be able to log issues arising from the container setup, especially useful in plugin scenarios) but at the same time a more complex logging pipeline also has dependencies... Maybe having two loggers is a way to go... One (simple) for the bootstrapping of the app, and another one (complex) for after the app started successfully (and forward the logs from the simple to the complex when successful) – C. Augusto Proiete Dec 14 '20 at 22:56
  • Is LogContext a static object? Can it be risky to use in multi-threaded environment when one request values can overwrite another? – Michael Freidgeim Jan 02 '21 at 00:28
  • 3
    @MichaelFreidgeim LogContext is a static class but [it is thread-safe via `AsyncLocal`/`ThreadStatic`](https://github.com/serilog/serilog/blob/5e2164756efb6023a548b4eb91a71ed7edc3d9e1/src/Serilog/Context/LogContext.cs#L54-L61). i.e. Each thread has their own LogContext despite it being accessed through a static class. Requests don't share the same LogContext. – C. Augusto Proiete Jan 02 '21 at 00:43
0

UseSerilog has several overrides and among them there is a one with lambda that takes 3 parameters, including IServiceProvider.

It can be used to dynamically inject an object, that would take IServiceProvider in ctor and use GetService to find a context it needs for each Log() action.

andrew.fox
  • 7,435
  • 5
  • 52
  • 75
-1

A slight improvement to the accepted answer would be to use ILogger.BeginScope instead of the static Serilog LogContext.PushProperty. It becomes a bit uglier with the dictionary, but still an improvement, as well as logger-agnostic.

public async Task Invoke(HttpContext context, IUserDataService userDataService, ILogger<UserDataLoggingMiddleware> logger)
{
    var userData = await userDataService.GetAsync();

    // Add user data to logging context
    using (logger.BeginScope(new Dictionary<string, object> { ["UserData"] = userData }))
    {
        await _next.Invoke(context);
    }
}
Mathias R
  • 134
  • 1
  • 4
  • It's definitely subjective, but I wouldn't consider this an improvement, but rather an alternative approach that has pros & cons... [I personally prefer to use Serilog directly within my applications](https://stackoverflow.com/a/61413261) rather than Microsoft's `ILogger` so the `LogContext.PushProperty` is on purpose. I don't see a reason to be logger agnostic within your own app. IMHO it only makes sense for libraries that you're expecting others to consume (so that you don't force Serilog on them) – C. Augusto Proiete Dec 14 '20 at 23:16
  • For `ILogger` to be generic it inevitably needs to expose only features that are common across many logging frameworks, so whenever you need to leverage a feature that is novel in the framework you use, you'll get blocked by the abstraction. `BeginScope` is one example where the price of being logger agnostic means [you have no control over destructuring of objects](https://github.com/serilog/serilog/blob/5e2164756efb6023a548b4eb91a71ed7edc3d9e1/src/Serilog/Context/LogContext.cs#L72-L76). – C. Augusto Proiete Dec 14 '20 at 23:23
  • Use of AsyncLocal LogContext ensures that the value will be passed to different loggers within the same thread. In your code it is not clear, how you create a logger, and different classes can have different DI loggers – Michael Freidgeim Jan 03 '21 at 21:32