news 2026/7/4 11:44:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一次代码评审引发的困惑:ASP.NET Core 里 Serilog 到底该怎么注册?

起因

在 review 一个同事的 PR 时,我发现项目里Program.cs的日志注册方式,跟我自己常写的不一样。顺手翻了一下团队里另外几个仓库,发现至少存在三种"看起来都能跑"的写法:

写法 A:显式创建 Logger 实例,交给Host.UseSerilog

varlogger=newLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);

写法 B:通过IServiceCollection注册

builder.Services.AddSerilog(options=>{options.ReadFrom.Configuration(configuration);});

写法 C:写静态Log.Logger,再调用无参UseSerilog

Log.Logger=newLoggerConfiguration().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 的日志输出,靠的是一个适配器:

你的业务代码 │ 注入 ILogger<T> (微软抽象) ▼ Microsoft.Extensions.Logging │ 通过 SerilogLoggerProvider 适配 ▼ Serilog.Core.Logger (实际写 sink 的那个对象) │ ▼ Console / File / Seq / Elasticsearch ...

无论走UseSerilog还是AddSerilog,本质上都是在做同一件事:往 DI 容器里注册一个ILoggerFactory(或等价物),这个 factory 内部包一层SerilogLoggerProvider,把所有通过ILogger<T>.LogXxx()产生的日志事件,转发给真正的 SerilogILogger去落地。

区别在于:这个"真正的 Serilog ILogger"从哪来、谁拥有它、谁负责释放它。这正是三种写法分歧的核心。


3. 逐一拆解三种写法

写法 A:手动 new 一个 logger,传给Host.UseSerilog(logger)

varlogger=newLoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);

IHostBuilder.UseSerilog的真实签名大致是:

publicstaticIHostBuilderUseSerilog(thisIHostBuilderbuilder,Serilog.ILoggerlogger=null,booldispose=false)

传入一个已经创建好的logger实例时,Serilog 不会再帮你管理它的生命周期——dispose参数默认是false。这意味着:

  • 这个logger局部变量没有被赋给Log.Logger(静态属性),所以通过Serilog.Log.Information(...)这种静态方式是打不出日志的,只有走ILogger<T>注入才有效。
  • 应用退出时,没有人调用这个 logger 的Dispose(),如果 sink 里有File、网络型 sink(如 Seq)等带缓冲/后台线程的资源,存在日志丢失或文件未刷盘的风险。

写法 B:builder.Services.AddSerilog(...)(官方现在推荐的主线)

builder.Services.AddSerilog(options=>{options.ReadFrom.Configuration(configuration);});

这是Serilog.Extensions.Hosting包提供的扩展方法,直接挂在IServiceCollection上,不依赖IHostBuilder。它的好处有三点:

  1. 完全脱离了对Log.Logger静态属性的依赖,整个日志配置走依赖注入,对单元测试更友好(不会出现"测试之间互相污染静态 logger"的问题)。
  2. 支持两阶段初始化(Two-Stage Initialization),下一节细讲,这是它相对前两种写法最大的实际优势。
  3. 由 DI 容器统一管理生命周期,应用关闭时会随 host 一起正确释放底层资源。

官方仓库现在给出的"标准模板"基本都是这个形态:

Log.Logger=newLoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();// 注意:这里用的是 CreateBootstrapLogger(),不是 CreateLogger()varbuilder=WebApplication.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.Logger=newLoggerConfiguration().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)写法 B(Services.AddSerilog写法 C(Log.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.Logger=newLoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();// 注意:用 CreateBootstrapLogger,而非 CreateLoggertry{Log.Information("应用程序正在启动");varbuilder=WebApplication.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 Loggervarapp=builder.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. 结论:给团队的实际建议

回到最初的问题——三种写法要不要统一?我的结论是:

  1. 新项目,统一用「两阶段初始化 +Services.AddSerilog。这是当前官方主推、生态文档最完整、长期维护性最好的方式。
  2. 写法 A(Host.UseSerilog(logger),手动管理实例)不建议在新代码里使用dispose默认为false这个细节很容易被忽略,遗留的资源释放问题排查成本不低。
  3. 写法 C(Log.Logger+ 无参UseSerilog())不是错的,很多线上项目仍在用,且对捕获"宿主构建阶段异常"确实友好;但要清楚它引入了进程级静态状态,在写集成测试、或者一个进程里跑多个 host 实例时要格外小心,必要时显式调用Log.CloseAndFlush()做清理。
  4. 如果项目历史悠久、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 — GitHub
  • serilog/serilog-aspnetcore Releases(8.0.0 Breaking Change 说明)
  • Serilog.AspNetCore NuGet 包说明页
  • serilog/serilog Wiki — Lifecycle of Loggers
  • Andrew Lock — Adding Serilog to the ASP.NET Core Generic Host
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/4 11:44:42

Python与CNN实战:从零搭建图像识别系统

1. 项目概述&#xff1a;当Python遇上图像识别 十年前我第一次接触图像识别时&#xff0c;需要手动提取HOG特征再用SVM分类&#xff0c;准确率勉强达到70%。如今借助CNN卷积神经网络&#xff0c;同样的猫狗分类任务轻松突破95%准确率。这次我们就用Python搭建一个完整的CNN图像…

作者头像 李华
网站建设 2026/7/4 11:39:04

STM32智能散热系统设计:DRV8213驱动与PID温控

1. 项目背景与核心需求在嵌入式系统开发中&#xff0c;散热管理一直是工程师面临的关键挑战之一。特别是在汽车电子、工业控制和医疗设备等对可靠性要求极高的领域&#xff0c;过热可能导致系统性能下降、元件寿命缩短甚至硬件损坏。传统散热方案往往体积庞大或控制逻辑简单&am…

作者头像 李华
网站建设 2026/7/4 11:38:49

SQL注入漏洞检测与防御:从原理到实战的完整指南

1. 项目概述&#xff1a;从“万能钥匙”到“安全门锁”SQL注入&#xff0c;这个名字对于任何接触过Web开发或网络安全的人来说&#xff0c;都如雷贯耳。它不像某些高深莫测的APT攻击那样遥不可及&#xff0c;而是像一把藏在代码缝隙里的“万能钥匙”&#xff0c;攻击者用它尝试…

作者头像 李华
网站建设 2026/7/4 11:38:45

直流有刷电机控制方案与TC78H653FTG驱动器应用

1. 直流有刷电机控制方案概述 在工业自动化和消费电子领域&#xff0c;直流有刷电机因其结构简单、成本低廉和控制方便等优势&#xff0c;仍然是许多应用场景的首选驱动方案。TC78H653FTG作为东芝推出的新一代H桥驱动器&#xff0c;配合PIC18LF26K42微控制器&#xff0c;能够构…

作者头像 李华
网站建设 2026/7/4 11:36:15

抖音直播数据抓取:零基础掌握实时弹幕监控技术

抖音直播数据抓取&#xff1a;零基础掌握实时弹幕监控技术 【免费下载链接】DouyinLiveWebFetcher 抖音直播间网页版的弹幕数据抓取&#xff08;2025最新版本&#xff09; 项目地址: https://gitcode.com/gh_mirrors/do/DouyinLiveWebFetcher 你是否曾好奇热门抖音直播间…

作者头像 李华
网站建设 2026/7/4 11:35:04

2026年AI量化入门,概念代码回测模拟别跳步

量化开发不是越快进入代码越好。对已有经验者来说&#xff0c;更重要的是让推进顺序清楚&#xff1a;概念先成形&#xff0c;代码再表达&#xff0c;之后再进入回测和模拟层面的检验。AI 可以帮助每一步拆开&#xff0c;但不应打乱这些层次。让 AI 先帮你把问题问清楚概念阶段的…

作者头像 李华