You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Serilog LogContext.PushProperty未触发Dispose移除属性问题咨询

问题:LogContext.PushProperty 导致后续日志意外携带 calculationId

我编写了以下代码:

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之后的同一上下文中执行,因此被残留的属性污染。

解决方法

  1. 确保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块内
            // 其他关联日志操作也需放在此处
        }
    }
    
  2. 替换为原生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);
        }
    });
    
  3. 手动清除残留上下文
    如果上述方法无效,可在await calculationsTask后手动清除LogContext属性:

    var calculationResponses = await calculationsTask;
    LogContext.RemoveProperty("calculationId");
    logger.LogInformation("Test log");
    

关键说明

LogContext.PushProperty返回的IDisposable必须在当前异步上下文内释放,一旦异步操作脱离using块范围,AsyncLocal上下文可能无法正确重置,尤其是在并行异步场景下,任务调度器可能复用线程/上下文,导致属性残留。


内容的提问来源于stack exchange,提问作者Terry

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 05:13:16