Skip to content

Behavior Change: Property Value Overriding in LogEvents Since Serilog.Extensions.Logging 9.0.1 #271

Open
@huoshan12345

Description

@huoshan12345

Description

Since version 9.0.1 of Serilog.Extensions.Logging, there has been a change in how values in a scope override each other in LogEvents. Previously, the value passed directly to the log method would take precedence over values defined in scopes. Now, the innermost scope value takes precedence instead.

Reproduction Code

[Fact]
public async Task PropertyOverride_Scope()
{
    var listener = new LogEventListener();
    Log.Logger = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Sink(listener)
        .CreateLogger();

    var provider = new ServiceCollection()
        .AddLogging(m => m.AddSerilog(Log.Logger, dispose: true))
        .BuildServiceProvider();

    var logger = provider.GetRequiredService<ILogger<ScopeTests>>();
    using (logger.BeginScope(new Dictionary<string, object?> { { "Value", 1 } }))
    using (logger.BeginScope(new Dictionary<string, object?> { { "Value", 2 } }))
    {
        logger.LogInformation("Value: {Value}", 3);
    }

    Assert.True(await listener.WaitAsync(1, TimeSpan.FromSeconds(3)));
    var logEvent = listener.Events.First();
    var value = (logEvent.Properties["Value"] as ScalarValue)?.Value;
    
    // Expected behavior before Serilog.Extensions.Logging 9.0.1
    // Assert.Equal(3, value);
    
    // Actual behavior since Serilog.Extensions.Logging 9.0.1
    Assert.Equal(2, value);
}

public class LogEventListener : ILogEventSink
{
    private readonly SemaphoreSlim _semaphore = new(0);
    public List<LogEvent> Events { get; } = [];

    public virtual void Emit(LogEvent logEvent)
    {
        Events.Add(logEvent);
        _semaphore.Release();
    }

    public async Task<bool> WaitAsync(int count, TimeSpan timeout, CancellationToken cancellationToken = default)
    {
        for (var i = 0; i < count; ++i)
        {
            var flag = await _semaphore.WaitAsync(timeout, cancellationToken);
            if (flag == false)
                return false; // timeout
        }
        return true;
    }
}

Comparing to LogContext

When using Serilog's native LogContext directly (instead of through Microsoft.Extensions.Logging), the behavior is different and matches the previous behavior:

using (LogContext.PushProperty("Value", 1))
using (LogContext.PushProperty("Value", 2))
{
    logger.Information("Value: {Value}", 3);
}
//  the "Value" will be 3.

Expected Behavior

Before version 9.0.1, when logging with a parameter that shares the same name as a property in a scope, the directly passed value (3 in this case) would take precedence over any scope values, which is consistent with how Serilog's native LogContext API works.

Actual Behavior

Since version 9.0.1, the innermost scope value (2 in this case) takes precedence over the value passed directly to the log method (3), creating inconsistency with Serilog's native behavior.

Questions

  • Is this a deliberate change in behavior or an unintended side effect?
  • If unintended, can the previous behavior be restored?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions