基于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
相关产品推荐
相关产品推荐

