Search code examples
c#.net-coreloggingserilog.net-core-logging

.NET Core MEL logging with syntax similar to Serilog


I'm using Serilog, but I prefer the MEL (Microsoft.Extensions.Logging) abstractions.

Some functionality in Serilog's Serilog.ILogger is not available in MEL's ILogger , or is available but extremely verbose.

I want to enrich a log event. With Serilog I'd use:

_logger
 .ForContext("Foo", "abc")
 .ForContext("Bar", 123)
 .ForContext("Baz", true)
 .Information("Process returned {Result}", 42);

But the MEL equivalent is:

using (_logger.BeginScope(new Dictionary<string, object?> { 
 { "Foo", "abc" },
 { "Bar", 123 },
 { "Baz", true }
})) {
 _logger.LogInformation("Process returned {Result}", 42);
}

That is not only ugly, but I always forget the syntax. And that applies even for one log event.

I've noticed there are various syntaxes for these sort of things, in addition to the ones above. Is there a simpler option?


Solution

  • I couldn't find something simpler, so I created some adapters and extension methods.

    To enrich multiple log events

    An extension method with a signature closely reminiscent to that of Serilog.

    namespace Microsoft.Extensions.Logging;
    
    public static class MultipleLogEventEnrichment
    {
    
     public static IDisposable? Enrich(this ILogger logger, params (string, object?)[] properties)
     {
       ArgumentNullException.ThrowIfNull(logger, nameof(logger));
       ArgumentNullException.ThrowIfNull(properties, nameof(properties));
    
       var state = properties.ToDictionary(k => k.Item1, v => v.Item2);
       return logger.BeginScope(state);
     }
    
    }
    

    A callsite would look like this:

    using (_logger.Enrich(
     ("Foo", "abc"),
     ("Bar", 123),
     ("Baz", true)
    )) {
     _logger.LogInformation("Process returned {Result}", 42);
    }
    

    Less ugly than the MEL syntax, and easier to remember.

    To enrich a single log event

    I learnt that ordinarily I enrich a single log event only. Yet even for that simple case one must use the ugly MEL syntax.

    An alternative, as an adapter:

    namespace Microsoft.Extensions.Logging;
    
    public sealed class SingleLogEventAdapter
    {
    
      private readonly ILogger _logger;
      private readonly Dictionary<string, object?> _state = [];
    
      public SingleLogEventAdapter(ILogger logger) =>
        // if you place these classes in a separate common/utils project, then make this ctor internal
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    
      public SingleLogEventAdapter Enrich(string key, object? value)
      {
        ArgumentNullException.ThrowIfNullOrWhiteSpace(key, nameof(key));
        _state.Add(key, value);
        return this;
      }
    
      private void Log(LogLevel logLevel, string message, params object?[] args)
      {
        if (!Enum.IsDefined(logLevel)) throw new ArgumentOutOfRangeException(nameof(logLevel));
        ArgumentNullException.ThrowIfNullOrWhiteSpace(message, nameof(message));
        ArgumentNullException.ThrowIfNull(args, nameof(args));
        using (_logger.BeginScope(_state)) _logger.Log(logLevel, message, args);
      }
    
      private void Log(LogLevel logLevel, Exception exception, string message, params object?[] args)
      {
        if (!Enum.IsDefined(logLevel)) throw new ArgumentOutOfRangeException(nameof(logLevel));
        ArgumentNullException.ThrowIfNull(exception, nameof(exception));
        ArgumentNullException.ThrowIfNullOrWhiteSpace(message, nameof(message));
        ArgumentNullException.ThrowIfNull(args, nameof(args));
        using (_logger.BeginScope(_state)) _logger.Log(logLevel, exception, message, args);
      }
    
      public void LogTrace      (             string message, params object?[] args) => Log(LogLevel.Trace,          message, args);
      public void LogDebug      (             string message, params object?[] args) => Log(LogLevel.Debug,          message, args);
      public void LogInformation(             string message, params object?[] args) => Log(LogLevel.Information,    message, args);
      public void LogWarning    (             string message, params object?[] args) => Log(LogLevel.Warning,        message, args);
      public void LogError      (             string message, params object?[] args) => Log(LogLevel.Error,          message, args);
      public void LogError      (Exception e, string message, params object?[] args) => Log(LogLevel.Error,       e, message, args);
      public void LogCritical   (             string message, params object?[] args) => Log(LogLevel.Critical,       message, args);
      public void LogCritical   (Exception e, string message, params object?[] args) => Log(LogLevel.Critical,    e, message, args);
    
    }
    
    
    public static class SingleLogEventAdapterExtensions
    {
    
      public static SingleLogEventAdapter Enrich(this ILogger logger, string key, object? value)
      {
        var adapter = new SingleLogEventAdapter(logger);
        adapter.Enrich(key, value);
        return adapter;
      }
    
    }
    

    A callsite would look like this:

    _logger
      .Enrich("Foo", "abc")
      .Enrich("Bar", 123)
      .Enrich("Baz", true)
      .LogInformation("Process returned {Result}", 42);
    

    Much better!

    The problem is that in a high-throughput path, that results in allocations just for aesthetic purposes. A native option would be much better.

    Final note

    These are workarounds for an unwieldy native syntax. I opened an issue on the repo requesting some syntactic sugar. Please upvote that issue if you'd like a friendlier API.