一次代码评审引发的困惑:ASP.NET Core 里 Serilog 到底该怎么注册?

📅 2026/7/4 11:45:02
一次代码评审引发的困惑:ASP.NET Core 里 Serilog 到底该怎么注册?
起因在 review 一个同事的 PR 时我发现项目里Program.cs的日志注册方式跟我自己常写的不一样。顺手翻了一下团队里另外几个仓库发现至少存在三种看起来都能跑的写法写法 A显式创建 Logger 实例交给Host.UseSerilogvarloggernewLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);写法 B通过IServiceCollection注册builder.Services.AddSerilog(options{options.ReadFrom.Configuration(configuration);});写法 C写静态Log.Logger再调用无参UseSerilogLog.LoggernewLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();SerilogHostBuilderExtensions.UseSerilog(builder);三种写法跑起来都能输出日志console 里该有的内容一条不少。问题来了它们到底是不是等价的团队里要不要统一成一种这篇文章记录我把这个问题查清楚的过程。1. 第一个误区以为这是风格不同其实是版本不同我最先怀疑的方向是——这三种写法分别对应 Serilog 不同的历史阶段而不是同一时期的三种平行选择。查了serilog-aspnetcore仓库的 Release Note 才确认了这个猜测Serilog.AspNetCore8.0 版本明确移除了IWebHostBuilder.UseSerilog()这个过时的扩展方法官方建议二选一改用IHostBuilder.UseSerilog()或者改用IServiceCollection.AddSerilog()。也就是说写法 A、C 里用到的Host.UseSerilog(...)挂在IHostBuilder上的扩展方法目前还活着但写法 B 的Services.AddSerilog(...)是 Serilog 官方在 8.0 之后明确推荐的新主线。这不是哪种风格更优雅的争论而是库作者已经把方向定了。到这一步我意识到光看能不能跑远远不够得搞清楚这三种写法背后分别接入了哪一套机制。2. 搞清楚 Serilog 接入 ASP.NET Core 的两条管线要理解这三种写法的差异得先弄明白一件事Serilog 本身和Microsoft.Extensions.Logging以下简称 MEL是两套独立的日志系统Serilog 要接管ASP.NET Core 的日志输出靠的是一个适配器你的业务代码 │ 注入 ILoggerT (微软抽象) ▼ Microsoft.Extensions.Logging │ 通过 SerilogLoggerProvider 适配 ▼ Serilog.Core.Logger (实际写 sink 的那个对象) │ ▼ Console / File / Seq / Elasticsearch ...无论走UseSerilog还是AddSerilog本质上都是在做同一件事往 DI 容器里注册一个ILoggerFactory或等价物这个 factory 内部包一层SerilogLoggerProvider把所有通过ILoggerT.LogXxx()产生的日志事件转发给真正的 SerilogILogger去落地。区别在于这个真正的 Serilog ILogger从哪来、谁拥有它、谁负责释放它。这正是三种写法分歧的核心。3. 逐一拆解三种写法写法 A手动 new 一个 logger传给Host.UseSerilog(logger)varloggernewLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);IHostBuilder.UseSerilog的真实签名大致是publicstaticIHostBuilderUseSerilog(thisIHostBuilderbuilder,Serilog.ILoggerloggernull,booldisposefalse)传入一个已经创建好的logger实例时Serilog 不会再帮你管理它的生命周期——dispose参数默认是false。这意味着这个logger局部变量没有被赋给Log.Logger静态属性所以通过Serilog.Log.Information(...)这种静态方式是打不出日志的只有走ILoggerT注入才有效。应用退出时没有人调用这个 logger 的Dispose()如果 sink 里有File、网络型 sink如 Seq等带缓冲/后台线程的资源存在日志丢失或文件未刷盘的风险。写法 Bbuilder.Services.AddSerilog(...)官方现在推荐的主线builder.Services.AddSerilog(options{options.ReadFrom.Configuration(configuration);});这是Serilog.Extensions.Hosting包提供的扩展方法直接挂在IServiceCollection上不依赖IHostBuilder。它的好处有三点完全脱离了对Log.Logger静态属性的依赖整个日志配置走依赖注入对单元测试更友好不会出现测试之间互相污染静态 logger的问题。支持两阶段初始化Two-Stage Initialization下一节细讲这是它相对前两种写法最大的实际优势。由 DI 容器统一管理生命周期应用关闭时会随 host 一起正确释放底层资源。官方仓库现在给出的标准模板基本都是这个形态Log.LoggernewLoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();// 注意这里用的是 CreateBootstrapLogger()不是 CreateLogger()varbuilderWebApplication.CreateBuilder(args);builder.Services.AddSerilog((services,lc)lc.ReadFrom.Configuration(builder.Configuration).ReadFrom.Services(services)// 关键可以读取 DI 容器里注册的服务.Enrich.FromLogContext().WriteTo.Console());写法 C写Log.Logger静态属性再调用无参UseSerilog()Log.LoggernewLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();SerilogHostBuilderExtensions.UseSerilog(builder);这里UseSerilog()不传logger参数logger默认为null。当参数为null时Serilog 内部走的逻辑是自动回退去读取静态的Serilog.Log.Logger。所以写法 C 实际上等价于Log.Logger...;// 全局唯一的静态 Loggerbuilder.Host.UseSerilog();// 隐式使用 Log.Logger这种写法的特点Log.Logger是静态字段进程内全局唯一、随时可用——这也是为什么很多人喜欢在Program.cs顶部、WebApplicationBuilder还没创建之前就先配置好 logger可以用try/catch包住整个启动过程连宿主构建阶段抛出的异常都能被记录下来。副作用Log.Logger是进程级单例如果项目里有集成测试在同一进程里反复WebApplicationFactory启动多个 host多次重复赋值Log.Logger会相互覆盖这是这种写法被诟病比较多的点Serilog 仓库的 issue 区有专门讨论#105。4. 三者横向对比维度写法 A局部 logger Host.UseSerilog(logger)写法 BServices.AddSerilog写法 CLog.Logger 无参UseSerilog()是否依赖静态Log.Logger否否是能否用Log.Information(...)静态调用不能不能除非你额外手动设置Log.Logger能是否支持两阶段初始化ReadFrom.Services不直接支持支持不直接支持进程退出时是否自动释放 sink 资源否dispose默认false需自己管理是由 DI 容器接管生命周期需要手动Log.CloseAndFlush()官方当前推荐程度历史写法仍可用8.0 之后的主推写法经典写法仍广泛使用但有静态状态的副作用适合捕获宿主构建阶段异常一般一般除非配合 Bootstrap Logger强配置发生在WebApplicationBuilder创建之前三种写法都能输出日志是因为它们最终都殊途同归地往 DI 里塞了一个SerilogLoggerProvider。表象一致但生命周期管理、可测试性、能否读取 DI 服务这三件事上有真实差异——这些差异在日常开发里不容易暴露但在进程异常退出导致日志没刷盘“单元测试互相污染”想在 enricher 里注入一个 scoped 服务却拿不到这几类场景里会突然变得很致命。5. 真正值得记住的最佳实践两阶段初始化Two-Stage Initialization查到这里我发现真正应该学的不是三选一而是 Serilog 官方在Serilog.AspNetCore≥ 6.0 之后大力推广的模式——两阶段初始化它解决了一个写法 A、B、C 都没单独解决好的根本矛盾越早配置 Serilog就越能捕获启动早期的异常但配置得越早就越拿不到IConfiguration和 DI 容器里的服务因为它们此时还没构建出来。两阶段初始化的做法是先用一个极简的Bootstrap Logger顶住启动阶段等 host 构建完、配置和服务都齐备了再换成正式 Logger。usingSerilog;// 第一阶段Bootstrap Logger // 此时还没有 builder.Configuration只能写最基础的 sink通常是 Console// 作用仅仅是兜底捕获 Program.cs 顶层、Host 构建过程中的异常Log.LoggernewLoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();// 注意用 CreateBootstrapLogger而非 CreateLoggertry{Log.Information(应用程序正在启动);varbuilderWebApplication.CreateBuilder(args);// 第二阶段正式 Logger // 通过 AddSerilog 的回调形式可以拿到 IServiceProvider(services)// 这意味着自定义 Enricher 如果需要注入 IHttpContextAccessor 等服务// 在这里是可以做到的写法 A / C 都做不到这一点builder.Services.AddSerilog((services,loggerConfig)loggerConfig.ReadFrom.Configuration(builder.Configuration)// 读取 appsettings.json 中的 Serilog 配置节.ReadFrom.Services(services)// 关键可读取 DI 容器里注册的服务/Sink/Enricher.Enrich.FromLogContext()// 支持 LogContext.PushProperty 动态属性.WriteTo.Console());// Bootstrap 阶段的 sink 在这里要重新声明一遍// 因为正式 Logger 会完全替换掉 Bootstrap Loggervarappbuilder.Build();app.UseSerilogRequestLogging();// 记录每个 HTTP 请求的耗时、状态码等信息app.MapGet(/,()Hello World!);app.Run();}catch(Exceptionex){Log.Fatal(ex,应用程序异常终止);}finally{Log.CloseAndFlush();// 确保所有缓冲中的日志事件都被写出再退出进程}这个写法把写法 A早期可用、能捕获启动异常和写法 B能读 DI 服务、生命周期托管给容器的优点结合在了一起是目前 Serilog 官方文档和Serilog.AspNetCoreNuGet 包说明页给出的标准范式。6. 结论给团队的实际建议回到最初的问题——三种写法要不要统一我的结论是新项目统一用「两阶段初始化 Services.AddSerilog」。这是当前官方主推、生态文档最完整、长期维护性最好的方式。写法 AHost.UseSerilog(logger)手动管理实例不建议在新代码里使用dispose默认为false这个细节很容易被忽略遗留的资源释放问题排查成本不低。写法 CLog.Logger 无参UseSerilog()不是错的很多线上项目仍在用且对捕获宿主构建阶段异常确实友好但要清楚它引入了进程级静态状态在写集成测试、或者一个进程里跑多个 host 实例时要格外小心必要时显式调用Log.CloseAndFlush()做清理。如果项目历史悠久、Host.UseSerilog还跑在 7.x 及更早版本的Serilog.AspNetCore上升级到 8.0 时要注意IWebHostBuilder.UseSerilog()这个重载已被移除编译都过不了必须迁移到IHostBuilder.UseSerilog()或IServiceCollection.AddSerilog()。7. 一点延伸UseSerilogRequestLogging()要放在哪顺手记一下查资料时发现的一个容易踩的坑——app.UseSerilogRequestLogging()这个中间件只会记录它之后的中间件管线所消耗的时间。如果把它放在UseStaticFiles()、UseRouting()之后静态文件请求的日志会被排除在外这有时反而是你想要的可以用来给高频但没什么信息量的请求降噪但如果误放在认证、路由中间件之前记录到的耗时和状态码可能不准确。建议默认紧跟在app.Build()之后按需再微调顺序。参考资料serilog/serilog-aspnetcore — GitHubserilog/serilog-aspnetcore Releases8.0.0 Breaking Change 说明Serilog.AspNetCore NuGet 包说明页serilog/serilog Wiki — Lifecycle of LoggersAndrew Lock — Adding Serilog to the ASP.NET Core Generic Host