Generic ILogger, or through Serilog, add logged property

Viewed 1303

Is there a way to add properties to a given instance of ILogger? (from Microsoft.Extensions.Logging.Abstractions). I am using Serilog and usually injecting ILogger instances into constructors. So it could also be a solution that leverages some Serilog APIs.

I am aware of ILogger.BeginScope and LogContext.PushProperty but these simply add information to the current Async context. I would like to add information to the instance of the logger itself, so that any log produced by that logger has the extra properties.

What I would like to achieve is similar to:

class Foo {
  private ILogger logger;
  public Foo(ILogger logger) {
     logger.AddProperty("Property", "value"); // now every calls using that logger have that extra prop
     _logger = logger;
  }

  public void Dummy() => _logger.LogDebug("logging something..."); // this log event would have the Property=value added to it automatically
}
1 Answers

Update 2021-08-21: You can now use Serilog.Enrichers.GlobalLogContext

Install the Serilog.Enrichers.GlobalLogContext package from NuGet:

Install-Package Serilog.Enrichers.GlobalLogContext

Add the Serilog namespaces to your C# file:

using Serilog;
using Serilog.Context;

Include the global log context enricher in your logger configuration:

Log.Logger = new LoggerConfiguration()
    .Enrich.FromGlobalLogContext()
    // ... other configuration ...
    .CreateLogger();

The FromGlobalLogContext() enricher dynamically adds properties present in the Serilog.Context.GlobalLogContext, to all produced events in your application.

Then, properties can be added and removed from the global log context using GlobalLogContext.PushProperty():

GlobalLogContext.PushProperty("AppVersion", GetThisAppVersion());
GlobalLogContext.PushProperty("OperatingSystem", GetCurrentOS());

After the code above is executed, any log event written to any Serilog sink will carry the properties AppVersion and OperatingSystem automatically.

Source: https://github.com/augustoproiete/serilog-enrichers-globallogcontext


Original answer

One way to achieve that, would be to create a custom Serilog Enricher that you hook up to the Serilog logging pipeline at the start, that you can also access throughout your application (ideally via dependency injection).

This enricher would provide you with a way to add new properties, and it would be responsible in making sure that the properties are applied to all log events.

The example below is a simplified version of this idea (definitely not recommended for production use as-is, but rather as pseudo-code to show the basics of how the main pieces fit together):

// WARNING: This is *NOT* Production code :)

public sealed class GlobalPropertyEnricher : ILogEventEnricher
{
    // TODO: Make it thread-safe
    private static readonly Dictionary<string, object> _properties =
        new Dictionary<string, object>();

    private static readonly Lazy<GlobalPropertyEnricher> _instance =
        new Lazy<GlobalPropertyEnricher>(() => new GlobalPropertyEnricher());

    public static GlobalPropertyEnricher Instance => _instance.Value;

    private GlobalPropertyEnricher()
    {
    }

    public static void SetProperty(string propertyName, object value)
    {
        // TODO: Make it thread-safe
        _properties[propertyName] = value;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        // TODO: Make it thread-safe
        // TODO: Properties should be cached (instead of CreateProperty) every time

        foreach (var p in _properties)
        {
            var property = propertyFactory.CreateProperty(p.Key, p.Value);
            logEvent.AddPropertyIfAbsent(property);
        }
    }
}

NB: For the purposes of this example, I have a static class accessible globally through your app, but you prob. would have that registered as a singleton instance in your DI container.

With the enricher example above, you can add/replace properties from anywhere via GlobalPropertyEnricher.SetProperty. e.g.

GlobalPropertyEnricher.SetProperty("Name", "Augusto");
GlobalPropertyEnricher.SetProperty("Year", DateTime.UtcNow.Year);

Finally, when you include the enricher in the Serilog pipeline, all the properties you set will be added to every event log message you write anywhere in the app:

Log.Logger = new LoggerConfiguration()
    .Enrich.With(GlobalPropertyEnricher.Instance)
    .WriteTo.(...)
    .CreateLogger();
Related