Serilog LogContext.PushProperty未触发Dispose移除属性问题咨询
我编写了以下代码:
var calculationsTask = request.CalcEngines.ForEachAsync( new ParallelOptions { MaxDegreeOfParallelism = int.MaxValue, CancellationToken = cancellationToken }, async ( item, ct ) => { var calculationId = Guid.NewGuid(); var calculationStopwatch = Stopwatch.StartNew(); LogContext.PushProperty( "calculationId", calculationId ); await Task.Delay( 1, ct ); // simulate async work } ); var calculationResponses = await calculationsTask; logger.LogInformation( "Test log" );
我的ForEachAsync()实现来自Stack Overflow相关帖子。
无论我采取何种操作,logger.LogInformation的日志中始终包含calculationId。我已尝试:
- 使用
using var _ = LogContext.PushProperty( .. ); - 使用
using ( var _ = LogContext.PushProperty( .. ) ) { .. } - 将整个lambda体移至独立函数,仅将参数指定为body:
RunCalculation
不过,当前API处理程序之后运行的中间件,其日志详情中却不包含calculationId。
请问这是PushProperty的预期行为吗?
这不是LogContext.PushProperty的预期行为,问题出在自定义ForEachAsync实现结合异步代码时的AsyncLocal上下文泄漏。
核心原因
LogContext依赖AsyncLocal<T>存储上下文属性,该上下文会在异步操作中自然延续。但你使用的ForEachAsync实现如果基于并行任务调度(比如用Parallel.ForEach包装异步委托),会导致某个迭代的LogContext属性未被正确释放,进而污染了后续的同步/异步上下文。
后续中间件日志不带calculationId,是因为中间件的执行上下文和API处理程序的上下文完成了异步边界切换(比如请求结束后的中间件在新的异步上下文中执行);而logger.LogInformation是在await calculationsTask之后的同一上下文中执行,因此被残留的属性污染。
解决方法
确保
PushProperty的IDisposable被正确释放
必须用using块包裹所有依赖该上下文的异步操作,不让异步操作逃逸出using范围:async ( item, ct ) => { var calculationId = Guid.NewGuid(); var calculationStopwatch = Stopwatch.StartNew(); using (LogContext.PushProperty("calculationId", calculationId)) { await Task.Delay(1, ct); // 所有依赖上下文的操作必须在using块内 // 其他关联日志操作也需放在此处 } }替换为原生
ForEachAsync实现
避免使用基于并行任务池的自定义实现,改用.NET 6+原生的Parallel.ForEachAsync,它能正确处理异步上下文隔离:await Parallel.ForEachAsync(request.CalcEngines, new ParallelOptions { MaxDegreeOfParallelism = int.MaxValue, CancellationToken = cancellationToken }, async (item, ct) => { var calculationId = Guid.NewGuid(); using (LogContext.PushProperty("calculationId", calculationId)) { await Task.Delay(1, ct); } });手动清除残留上下文
如果上述方法无效,可在await calculationsTask后手动清除LogContext属性:var calculationResponses = await calculationsTask; LogContext.RemoveProperty("calculationId"); logger.LogInformation("Test log");
关键说明
LogContext.PushProperty返回的IDisposable必须在当前异步上下文内释放,一旦异步操作脱离using块范围,AsyncLocal上下文可能无法正确重置,尤其是在并行异步场景下,任务调度器可能复用线程/上下文,导致属性残留。
内容的提问来源于stack exchange,提问作者Terry

