多串口项目中如何为同一服务注入不同Logger?
需求梳理
我正在开发一个多串口通信项目,每个Procedure对应一个串口,调用相同服务时,希望日志能按Procedure(串口)输出到不同文件——比如Procedure 1调用服务的日志进文件1,Procedure 2的进文件2。现有两个方案都有明显缺点,想找更优解或者判断哪个方案更合适。
现有方案问题分析
方案1:每个Procedure创建带专属Logger的服务实例
代码如下:
public IProcedure CreateProcedure(int portNumber) { Serilog.ILogger logger = CreateLogger(portNumber); Service1 service1 = ServiceFactory1.Create(logger); Service2 service2 = ServiceFactory2.Create(logger); Service3 service3 = ServiceFactory3.Create(logger); Service4 service4 = ServiceFactory4.Create(logger); Service5 service5 = ServiceFactory5.Create(logger); List<IStage> stages = new() { new Stage1(logger.As<Stage1>(), service1), new Stage2(logger.As<Stage2>(), service1), new Stage3(logger.As<Stage3>(), service1), new Stage4(logger.As<Stage4>(), service1), new Stage5(logger.As<Stage5>(), service1, service2, service3), new Stage6(logger.As<Stage6>(), service1, service4), new Stage7(logger.As<Stage7>(), service1, service2), new Stage8(logger.As<Stage8>(), service1, service5), }; return new Procedure(logger.As<Procedure>(), service1, stages); }
问题:每个服务都要写工厂类,重复代码冗余;串口数量多的时候,服务实例会线性增长,资源开销变大,可能影响性能。
方案2:单服务实例+LoggerCollection路由
代码如下:
public class Service1 : IService1 { ILoggerCollection _loggers; public Service1(ILoggerCollection loggers) { _loggers = loggers; } public void Method1(string value, int comport) { loggers.Get(comport).LogWarning(comport); // Do thing. } }
问题:服务里混入了日志路由逻辑,违反单一职责;每个方法都要额外传串口参数,污染了服务API,后续维护成本高。
最优方案:用日志上下文(Log Context)实现无侵入隔离
利用Serilog的LogContext(其他主流日志框架也有类似功能),在Procedure的执行上下文里打上串口标识,日志框架自动根据这个标识把日志分到对应文件,完美解决前两个方案的痛点。
具体实现步骤
配置Serilog,按上下文字段分文件输出
先在日志配置里,指定根据ComPortNumber这个上下文字段来路由日志文件:Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() // 启用上下文 enrichment .WriteTo.Map( keyPropertyName: "ComPortNumber", // 按这个字段的值分文件 defaultKey: "default", // 默认文件(无上下文时使用) (key, writeTo) => writeTo.File($"Logs/ComPort-{key}.txt") // 每个key对应独立文件 ) .CreateLogger();在Procedure执行前注入上下文
每个Procedure启动执行时,用LogContext.PushProperty把串口号注入到当前上下文,这个上下文会覆盖整个Procedure的执行过程,包括调用的所有服务、Stage:public class Procedure { private readonly int _portNumber; private readonly IService1 _service1; private readonly ILogger<Procedure> _logger; public Procedure(int portNumber, IService1 service1, ILogger<Procedure> logger) { _portNumber = portNumber; _service1 = service1; _logger = logger; } public async Task ExecuteAsync() { // 注入串口上下文,using块确保执行完自动清理 using (LogContext.PushProperty("ComPortNumber", _portNumber)) { _logger.LogInformation("Procedure 开始执行"); await _service1.Method1("test value"); // 其他Stage的执行逻辑... } } }服务保持纯净,无需修改
服务里只需要注入普通的Logger,完全不用管日志要输出到哪个文件,业务逻辑和日志逻辑彻底分离:public class Service1 : IService1 { private readonly ILogger<Service1> _logger; public Service1(ILogger<Service1> logger) { _logger = logger; } public async Task Method1(string value) { _logger.LogWarning("Service1.Method1 被调用,参数:{Value}", value); // 正常业务逻辑... } }
这个方案的优势
- 资源开销最小:所有Procedure共享同一个服务实例,不会因串口数量增加导致实例膨胀。
- 代码最干净:服务API无需添加多余参数,也不用写日志路由逻辑,完全符合单一职责原则。
- 上下文自动传递:只要在Procedure的作用域内注入了标识,所有嵌套调用的代码都会自动继承这个上下文,日志自动分到对应文件,无需手动传递参数。
- 扩展性强:后续如果要按其他维度分日志(比如用户、任务类型),只需修改上下文字段和日志配置,无需改动业务代码。
方案选择建议
如果你的串口数量很少(比如个位数),方案1其实也能凑合用,毕竟实现简单、逻辑直观;但如果串口数量多,或者你追求代码整洁和性能最优,日志上下文方案绝对是最佳选择,完美解决了前两个方案的所有问题。
内容的提问来源于stack exchange,提问作者Adam

