详情

首页手游攻略 一次代码评审带来的困惑:ASP.NET Core 里的 Serilog 到底该怎么注册?

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

佚名 2026-07-08 09:36:24

起因

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

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

var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);

写法 B:通过 IServiceCollection 注册

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

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

Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();SerilogHostBuilderExtensions.UseSerilog(builder);

三种写法跑起来都能输出日志,console 里该有的内容一条不少。问题来了:它们到底是不是等价的?团队里要不要统一成一种?

这篇文章记录我把这个问题查清楚的过程。

1. 第一个误区:以为这是"风格不同",其实是"版本不同"

我最先怀疑的方向是——这三种写法分别对应 Serilog 不同的历史阶段,而不是同一时期的三种平行选择。查了 serilog-aspnetcore 仓库的 Release Note 才确认了这个猜测:

也就是说,写法 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() 产生的日志事件,转发给真正的 Serilog ILogger 去落地。

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

3. 逐一拆解三种写法

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

var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();builder.Host.UseSerilog(logger);

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

public static IHostBuilder UseSerilog(this IHostBuilder builder,Serilog.ILogger logger = null,bool dispose = 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。它的好处有三点:

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

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

Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger(); // 注意:这里用的是 CreateBootstrapLogger(),不是 CreateLogger()var builder = 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 = new LoggerConfiguration().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 创建之前)

5. 真正值得记住的最佳实践:两阶段初始化(Two-Stage Initialization)

查到这里,我发现真正应该学的不是"三选一",而是 Serilog 官方在 Serilog.AspNetCore ≥ 6.0 之后大力推广的模式——两阶段初始化,它解决了一个写法 A、B、C 都没单独解决好的根本矛盾:

两阶段初始化的做法是:先用一个极简的 Bootstrap Logger 顶住启动阶段,等 host 构建完、配置和服务都齐备了,再换成正式 Logger。

using Serilog;// === 第一阶段:Bootstrap Logger ===// 此时还没有 builder.Configuration,只能写最基础的 sink(通常是 Console)// 作用仅仅是兜底捕获 Program.cs 顶层、Host 构建过程中的异常Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger(); // 注意:用 CreateBootstrapLogger,而非 CreateLoggertry{ Log.Information("应用程序正在启动");var builder = 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 Loggervar app = builder.Build();app.UseSerilogRequestLogging(); // 记录每个 HTTP 请求的耗时、状态码等信息app.MapGet("/", () => "Hello World!");app.Run();}catch (Exception ex){ Log.Fatal(ex, "应用程序异常终止");}finally{ Log.CloseAndFlush(); // 确保所有缓冲中的日志事件都被写出,再退出进程}

这个写法把写法 A(早期可用、能捕获启动异常)和写法 B(能读 DI 服务、生命周期托管给容器)的优点结合在了一起,是目前 Serilog 官方文档和 Serilog.AspNetCore NuGet 包说明页给出的标准范式。

6. 结论:给团队的实际建议

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

新项目,统一用「两阶段初始化 Services.AddSerilog」。这是当前官方主推、生态文档最完整、长期维护性最好的方式。 写法 A(Host.UseSerilog(logger),手动管理实例)不建议在新代码里使用:dispose 默认为 false 这个细节很容易被忽略,遗留的资源释放问题排查成本不低。 写法 C(Log.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() 之后,按需再微调顺序。

相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载