简体   繁体   English

如何对 Serilog 的 LogContext 属性进行单元测试

[英]How to unit test Serilog's LogContext Properties

We have a fairly simple netstandard2.0 project for custom middleware that uses Serilog's static LogContext to copy specified HttpContext headers to the log context.我们有一个相当简单的netstandard2.0项目用于自定义中间件,它使用 Serilog 的 static LogContext 将指定的 HttpContext 标头复制到日志上下文。

I'm trying to write a unit test where I set up a logger that uses a DelegatingSink to write to a variable.我正在尝试编写一个单元测试,在其中设置一个使用DelegatingSink写入变量的记录器。 It then executes the Invoke() middleware method.然后它执行Invoke()中间件方法。 I'm then trying to use the event to assert the properties were added.然后,我尝试使用该事件来断言已添加属性。 So far the properties being added by the middleware aren't showing up, but the property I'm adding in the test does.到目前为止,中间件添加的属性没有显示,但我在测试中添加的属性会显示。 I'm assuming it's dealing with different contexts, but I'm not sure how to fix this.我假设它正在处理不同的上下文,但我不确定如何解决这个问题。 I've tried several different things but none have worked.我尝试了几种不同的方法,但都没有奏效。

Since LogContext is static, I assumed this would be pretty straight forward, but I'm underestimating something.由于LogContext是 static,我认为这很简单,但我低估了一些东西。 This is where I'm at right now (some code omitted for brevity).这就是我现在所处的位置(为简洁起见,省略了一些代码)。 I did confirm the LogContext.PushProperty line of the middleware is being hit when the rest is run.当 rest 运行时,我确实确认了中间件LogContext.PushProperty行。

The test:考试:

...
[Fact]
public async Task Adds_WidgetId_To_LogContext()
{
    LogEvent lastEvent = null;

    var log = new LoggerConfiguration()
        .Enrich.FromLogContext()
        .WriteTo.Sink(new DelegatingSink(e => lastEvent = e))
        .CreateLogger();
         // tried with and without this, also tried the middleware class name
        //.ForContext<HttpContextCorrelationHeadersLoggingMiddlewareTests>(); 

    var context = await GetInvokedContext().ConfigureAwait(false);

    LogContext.PushProperty("MyTestProperty", "my-value");

    log.Information("test");

    // At this point, 'lastEvent' only has the property "MyTestProperty" :(
}

private async Task<DefaultHttpContext> GetInvokedContext(bool withHeaders = true)
{
    RequestDelegate next = async (innerContext) =>
        await innerContext.Response
            .WriteAsync("Test response.")
            .ConfigureAwait(false);

    var middleware = new MyCustomMiddleware(next, _options);

    var context = new DefaultHttpContext();

    if (withHeaders)
    {
        context.Request.Headers.Add(_options.WidgetIdKey, _widgetId);
    }

    await middleware.Invoke(context).ConfigureAwait(false);

    return context;
}

The middleware (test project references this project):中间件(测试项目引用本项目):

...
public async Task Invoke(HttpContext context)
{
    if (context == null || context.Request.Headers.Count == 0) { await _next(context).ConfigureAwait(false); }

    var headers = context.Request.Headers;

    foreach (var keyName in KeyNames)
    {
        if (headers.ContainsKey(keyName))
        {
            LogContext.PushProperty(keyName, headers[keyName]);
        }
    }

    await _next(context).ConfigureAwait(false);
}
...

This is the delegating sink I stole from the Serilog test source:这是我从 Serilog 测试源中窃取的委托接收器:

public class DelegatingSink : ILogEventSink
{
    readonly Action<LogEvent> _write;

    public DelegatingSink(Action<LogEvent> write)
    {
        _write = write ?? throw new ArgumentNullException(nameof(write));
    }

    public void Emit(LogEvent logEvent)
    {
        _write(logEvent);
    }

    public static LogEvent GetLogEvent(Action<ILogger> writeAction)
    {
        LogEvent result = null;

        var l = new LoggerConfiguration()
            .WriteTo.Sink(new DelegatingSink(le => result = le))
            .CreateLogger();

        writeAction(l);

        return result;
    }
}

I also had to unit test the pushed properties of my logged events.我还必须对我记录的事件的推送属性进行单元测试。 Assuming you got are pushing your property as follow :假设您按如下方式推送您的财产:

public async Task<T> FooAsync(/*...*/)
{
     /*...*/
     using (LogContext.PushProperty("foo", "bar"))
     {
         Log.Information("foobar");
     }
     /*...*/
}

You can unit test it like this example with Serilog.Sinks.TestCorrelator as a Serilog sink dedicated to tests (also I'm using NUnit and FluentAssertion too here):您可以使用Serilog.Sinks.TestCorrelator作为专用于测试的 Serilog 接收器,像这个示例一样对其进行单元测试(我也在此处使用NUnitFluentAssertion ):

[Test]
public async Task Should_assert_something()
{
    ///Arrange
    // I had issues with unit test seeing log events from other tests running at the same time so I recreate context in each test now
    using (TestCorrelator.CreateContext())
    using (var logger = new LoggerConfiguration().WriteTo.Sink(new TestCorrelatorSink()).Enrich.FromLogContext().CreateLogger())
    {
        Log.Logger = logger;
        /*...*/
        /// Act
        var xyz = await FooAsync(/*...*/)
        /*...*/

        /// Assert 
        TestCorrelator.GetLogEventsFromCurrentContext().Should().ContainSingle().Which.MessageTemplate.Text.Should().Be("foobar");
    }
}

I think your unit test is catching a real bug with the code here.我认为您的单元测试在这里的代码中发现了一个真正的错误。

Serilog's LogContext applies state to the "logical call context" which follows the ExecutionContext (see a great write-up here ). Serilog 的LogContext将状态应用于跟在 ExecutionContext 之后的“逻辑调用上下文”(请参阅此处的精彩文章)。

The counterintuitive result you're seeing here is due to the fact that the "state" applied to the logical call context only applies to context in which the LogContext.PushProperty call is made.您在此处看到的违反直觉的结果是由于应用于逻辑调用上下文的“状态”仅适用于进行LogContext.PushProperty调用的上下文。 Outer contexts are inherited by inner contexts, but changes within inner contexts do not affect the outer contexts.外部上下文由内部上下文继承,但内部上下文中的变化不影响外部上下文。 Your async methods are creating additional contexts (unbeknownst to you), and when you return to your original context the changes made in the inner contexts are lost.您的异步方法正在创建额外的上下文(您不知道),当您返回原始上下文时,在内部上下文中所做的更改将丢失。

It might be more clear if you look at a simpler example that demonstrates the same problem without having to worry about the async/await task continuation stuff.如果您查看一个更简单的示例,该示例演示了相同的问题,而不必担心 async/await 任务继续的问题,则可能会更清楚。

void ContextExample()
{
    LogContext.PushProperty("MyOuterProperty", "Foo"); // Apply this property to all log events *within this logical call context*

    await Task.Run(() =>
    {
        LogContext.PushProperty("MyInnerProperty", "Bar"); // Apply this property to all log events *within this logical call context*

        log.Information("MyFirstLog"); // This log event will contain both MyOuterProperty and MyInnerProperty
    }); // We leave the inner call context, destroying the changes we made to it with PushProperty

    log.Information("MySecondLog"); // This log event will contain only MyOuterProperty
}

To get what you want, you're going to have to push the property in the same (or an outer) logical call context as the logical call context in which you call log.Information .为了得到你想要的东西,你将不得不在与你调用log.Information的逻辑调用上下文相同(或外部)的逻辑调用上下文中log.Information

Also, you probably want to call Dispose on the return values of PushProperty .此外,您可能希望对PushProperty的返回值调用Dispose It returns an IDisposable so that you can revert the logical call context back to its original state.它返回一个IDisposable以便您可以将逻辑调用上下文恢复到其原始状态。 You may see some strange behavior if you don't.如果不这样做,您可能会看到一些奇怪的行为。

PS If you want to test log events produced by your code, might I suggest the TestCorrelator sink . PS如果你想测试你的代码产生的日志事件,我可以建议TestCorrelator sink

I know this is an old question but I had to do the same thing and I used the following solution, extending @FooBar's solution:我知道这是一个老问题,但我不得不做同样的事情,我使用了以下解决方案,扩展了@FooBar 的解决方案:

public async Task<T> FooAsync(/*...*/)
{
    /*...*/
    using (LogContext.PushProperty("foo", "bar"))
    {
        Log.Information("foobar");
    }
    /*...*/
}

[Test]
public async Task Should_assert_something()
{
    //Arrange
    // I had issues with unit test seeing log events from other tests running at the same time so I recreate context in each test now
    using (TestCorrelator.CreateContext())
    using (var logger = new LoggerConfiguration().WriteTo.Sink(new TestCorrelatorSink()).Enrich.FromLogContext().CreateLogger())
    {
        Log.Logger = logger;
        /*...*/
        /// Act
        var xyz = await FooAsync(/*...*/);
        /*...*/

        var logEventsArray = TestCorrelator.GetLogEventsFromCurrentContext().ToArray(); 

        foreach (var logEvent in logEventsArray)
        {
            // Assert 
            logEvent.Properties.Should().ContainKeys("foo");
            logEvent.Properties.TryGetValue("foo", out LogEventPropertyValue? result).Should().Be(true);
            result?.ToString().Replace("\"", "").Should().Be("bar"); // replace the quote because it's coming like "bar".
        }

        // Assert 
        TestCorrelator.GetLogEventsFromCurrentContext().Should().ContainSingle().Which.MessageTemplate.Text.Should().Be("foobar");
    }
}

In the test I check for the correlator's properties to contain the needed key and also to see if I can get the value and the actual string value.在测试中,我检查相关器的属性以包含所需的键,并查看是否可以获得值和实际的字符串值。 Be aware that you need to unwrap the value from the PropertyValue and to remove all the extra quotes in order to check for the actual value.请注意,您需要从 PropertyValue 中解开值并删除所有额外的引号以检查实际值。

I don't know if this is the best solution but it's the only one i could come up with.我不知道这是否是最好的解决方案,但这是我唯一能想到的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM