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

基于NLog为Foo/Bar实例添加结构化日志ID前缀的最优方案

基于NLog的实例级结构化日志实现方案

需求说明

现有对象模型如下:

public class FooManager {
    private ILogger _logger;
    private Dictionary<string, Foo> _instances = ...

    // CRUD interface
}

public class Foo {
    public string Id { get; private set; }
    private ILogger _logger;
    private Dictionary<string, Bar> _bars = ...

    // 有状态多线程逻辑
}

public class Bar {
    public string Id { get; private set; }
    private ILogger _logger;

    // 有状态多线程逻辑
}

日志格式要求:

  • 所有Foo实例的日志需携带实例ID前缀:... [<foo_id>] <message>
  • 所有Bar实例的日志需携带Foo和Bar实例ID前缀:... [<foo_id>] [<bar_id>] <message>

当前环境:使用NLog,通过DI注入ILogger,初始化代码如下:

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddNLog();

此前尝试的问题

  • 使用NLog的Logger.WithProperty创建自定义Logger,但无法适配ILogger接口
  • 考虑切换为NLog原生Logger类,但担心无法Mock,影响测试
  • 使用ILogger.BeginScope在构造函数中绑定实例ID,但因ILogger单例注入,Scope是线程绑定的,多线程场景下会出现日志ID混乱(比如Foo日志出现Bar ID)

最优实现方案

方案一:基于NLog MDLC的异步安全上下文绑定

利用NLog的MappedDiagnosticsLogicalContext(MDLC)实现异步/多线程安全的上下文传递,同时保留ILogger的DI注入和可测试性。

1. 代码实现

Foo类:

public class Foo
{
    public string Id { get; private set; }
    private readonly ILogger _logger;

    public Foo(ILogger<Foo> logger, string id)
    {
        Id = id;
        _logger = logger;
    }

    public void DoWork()
    {
        // 绑定Foo ID到异步上下文,自动在多线程/异步流中传递
        using (MappedDiagnosticsLogicalContext.SetScoped("foo_id", Id))
        {
            _logger.LogInformation("Foo执行核心逻辑");
            // 创建Bar时传递Foo ID,Bar内部绑定自身ID
            var bar = new Bar(_logger, "bar_001", Id);
            bar.DoWork();
        }
    }
}

Bar类:

public class Bar
{
    public string Id { get; private set; }
    public string FooId { get; private set; }
    private readonly ILogger _logger;

    public Bar(ILogger<Bar> logger, string id, string fooId)
    {
        Id = id;
        FooId = fooId;
        _logger = logger;
    }

    public void DoWork()
    {
        // 绑定Bar ID到异步上下文
        using (MappedDiagnosticsLogicalContext.SetScoped("bar_id", Id))
        {
            _logger.LogInformation("Bar执行核心逻辑");
        }
    }
}

2. NLog配置修改

调整日志布局,自动读取MDLC中的实例ID:

<targets>
    <target name="console" xsi:type="Console">
        <layout>
            ${longdate} 
            [${mdlc:item=foo_id}] 
            ${when:when=${mdlc:item=bar_id}!=null:inner=[${mdlc:item=bar_id}]} 
            ${message}
        </layout>
    </target>
</targets>

方案二:ILogger扩展方法封装Scope创建

通过扩展方法封装实例ID的Scope绑定,简化代码同时兼容Mock。

1. 扩展方法定义

public static class LoggerInstanceExtensions
{
    public static IDisposable BeginFooScope(this ILogger logger, string fooId)
    {
        return logger.BeginScope(new Dictionary<string, object> { ["foo_id"] = fooId });
    }

    public static IDisposable BeginBarScope(this ILogger logger, string fooId, string barId)
    {
        return logger.BeginScope(new Dictionary<string, object>
        {
            ["foo_id"] = fooId,
            ["bar_id"] = barId
        });
    }
}

2. 业务类使用

Foo类:

public void DoWork()
{
    using (_logger.BeginFooScope(Id))
    {
        _logger.LogInformation("Foo执行核心逻辑");
        var bar = new Bar(_logger, "bar_001", Id);
        bar.DoWork();
    }
}

Bar类:

public void DoWork()
{
    using (_logger.BeginBarScope(FooId, Id))
    {
        _logger.LogInformation("Bar执行核心逻辑");
    }
}

3. NLog布局配置

使用event-properties读取Scope中的属性:

<layout>
    ${longdate} 
    [${event-properties:item=foo_id}] 
    ${when:event-properties:item=bar_id}!=null:inner=[${event-properties:item=bar_id}]} 
    ${message}
</layout>

可测试性保障

两种方案均保留ILogger接口注入,测试时可直接Mock:

[Test]
public void Foo_DoWork_LogsWithCorrectId()
{
    var mockLogger = new Mock<ILogger<Foo>>();
    var foo = new Foo(mockLogger.Object, "foo_123");

    foo.DoWork();

    // 验证日志调用次数
    mockLogger.Verify(l => l.LogInformation(
        It.IsAny<EventId>(),
        It.IsAny<FormattedLogValues>(),
        It.IsAny<Exception>()), 
        Times.Once);

    // 可选:验证Scope是否被正确创建
    mockLogger.Verify(l => l.BeginScope(It.Is<Dictionary<string, object>>(d => 
        d.ContainsKey("foo_id") && d["foo_id"].ToString() == "foo_123")), 
        Times.Once);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 20:05:55