如何对 Serilog 的 LogContext 属性进行单元测试
How to unit test Serilog's LogContext Properties
我们有一个相当简单的 netstandard2.0
自定义中间件项目,它使用 Serilog 的静态 LogContext 将指定的 HttpContext headers 复制到日志上下文。
我正在尝试编写一个单元测试,我在其中设置了一个使用 DelegatingSink
写入变量的记录器。然后它执行 Invoke()
中间件方法。然后我尝试使用该事件来断言属性已添加。到目前为止,中间件添加的属性没有显示,但我在测试中添加的 属性 显示了。我假设它正在处理不同的上下文,但我不确定如何解决这个问题。我尝试了几种不同的方法,但 none 奏效了。
由于 LogContext
是静态的,我认为这会很简单,但我低估了一些东西。这就是我现在所在的位置(为简洁起见省略了一些代码)。我确实确认中间件 的 LogContext.PushProperty
行是 被击中,而其余的是 运行.
测试:
...
[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;
}
中间件(测试项目引用本项目):
...
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);
}
...
这是我从 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;
}
}
我认为您的单元测试正在捕获此处代码的真正错误。
Serilog 的 LogContext
将状态应用于 ExecutionContext 之后的 "logical call context"(请参阅精彩的文章 here)。
您在这里看到的违反直觉的结果是因为应用于逻辑调用上下文的 "state" 仅适用于进行 LogContext.PushProperty
调用的上下文。外部上下文由内部上下文继承,但内部上下文的变化不会影响外部上下文。您的异步方法正在创建额外的上下文(您不知道),并且当您 return 到您的原始上下文时,在内部上下文中所做的更改将丢失。
如果您看一个更简单的示例来演示相同的问题,而不必担心 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
}
为了得到你想要的,你将不得不将 属性 推送到与调用 log.Information
的逻辑调用上下文相同的(或外部)逻辑调用上下文中.
此外,您可能想在 PushProperty
的 return 值上调用 Dispose
。它 return 是一个 IDisposable
以便您可以将逻辑调用上下文恢复到其原始状态。如果不这样做,您可能会看到一些奇怪的行为。
P.S。如果你想测试你的代码产生的日志事件,我可以建议 TestCorrelator sink.
我还必须对已记录事件的推送属性进行单元测试。假设您正在推动 属性 如下:
public async Task<T> FooAsync(/*...*/)
{
/*...*/
using (LogContext.PushProperty("foo", "bar"))
{
Log.Information("foobar");
}
/*...*/
}
你也可以在这里用 Serilog.Sinks.TestCorrelator as a Serilog sink dedicated to tests (also I'm using NUnit and FluentAssertion 像这个例子一样对它进行单元测试):
[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");
}
}
我们有一个相当简单的 netstandard2.0
自定义中间件项目,它使用 Serilog 的静态 LogContext 将指定的 HttpContext headers 复制到日志上下文。
我正在尝试编写一个单元测试,我在其中设置了一个使用 DelegatingSink
写入变量的记录器。然后它执行 Invoke()
中间件方法。然后我尝试使用该事件来断言属性已添加。到目前为止,中间件添加的属性没有显示,但我在测试中添加的 属性 显示了。我假设它正在处理不同的上下文,但我不确定如何解决这个问题。我尝试了几种不同的方法,但 none 奏效了。
由于 LogContext
是静态的,我认为这会很简单,但我低估了一些东西。这就是我现在所在的位置(为简洁起见省略了一些代码)。我确实确认中间件 的 LogContext.PushProperty
行是 被击中,而其余的是 运行.
测试:
...
[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;
}
中间件(测试项目引用本项目):
...
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);
}
...
这是我从 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;
}
}
我认为您的单元测试正在捕获此处代码的真正错误。
Serilog 的 LogContext
将状态应用于 ExecutionContext 之后的 "logical call context"(请参阅精彩的文章 here)。
您在这里看到的违反直觉的结果是因为应用于逻辑调用上下文的 "state" 仅适用于进行 LogContext.PushProperty
调用的上下文。外部上下文由内部上下文继承,但内部上下文的变化不会影响外部上下文。您的异步方法正在创建额外的上下文(您不知道),并且当您 return 到您的原始上下文时,在内部上下文中所做的更改将丢失。
如果您看一个更简单的示例来演示相同的问题,而不必担心 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
}
为了得到你想要的,你将不得不将 属性 推送到与调用 log.Information
的逻辑调用上下文相同的(或外部)逻辑调用上下文中.
此外,您可能想在 PushProperty
的 return 值上调用 Dispose
。它 return 是一个 IDisposable
以便您可以将逻辑调用上下文恢复到其原始状态。如果不这样做,您可能会看到一些奇怪的行为。
P.S。如果你想测试你的代码产生的日志事件,我可以建议 TestCorrelator sink.
我还必须对已记录事件的推送属性进行单元测试。假设您正在推动 属性 如下:
public async Task<T> FooAsync(/*...*/)
{
/*...*/
using (LogContext.PushProperty("foo", "bar"))
{
Log.Information("foobar");
}
/*...*/
}
你也可以在这里用 Serilog.Sinks.TestCorrelator as a Serilog sink dedicated to tests (also I'm using NUnit and FluentAssertion 像这个例子一样对它进行单元测试):
[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");
}
}