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

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

起因

在 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 才确认了这个猜测:

Serilog.AspNetCore 8.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() 产生的日志事件,转发给真正的 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。它的好处有三点:

  1. 完全脱离了对 Log.Logger 静态属性的依赖,整个日志配置走依赖注入,对单元测试更友好(不会出现"测试之间互相污染静态 logger"的问题)。
  2. 支持两阶段初始化(Two-Stage Initialization),下一节细讲,这是它相对前两种写法最大的实际优势。
  3. 由 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 = ...;          // 全局唯一的静态 Logger
builder.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。

using Serilog;

// === 第一阶段:Bootstrap Logger ===
// 此时还没有 builder.Configuration,只能写最基础的 sink(通常是 Console)
// 作用仅仅是兜底捕获 Program.cs 顶层、Host 构建过程中的异常
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateBootstrapLogger();   // 注意:用 CreateBootstrapLogger,而非 CreateLogger

try
{
   
    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 Logger

    var 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. 结论:给团队的实际建议

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

  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() 之后,按需再微调顺序。

目录
相关文章
|
11天前
|
人工智能 JSON 自然语言处理
让教学更智慧:用阿里云百炼工作流,自动生成中小学教材内容#小有可为#有温度的AI
通过可视化工作流编排,将大模型推理能力转化为标准化的教学内容生成引擎。教师只需输入教材标题和适用学段,即可自动获得结构完整、符合课程标准的章节内容,大幅降低备课门槛,助力教育资源均衡化。
489 126
|
20天前
|
Linux 程序员 数据格式
【2026最新】Notepad++下载、安装和使用一篇搞定(附中文版安装包)
Notepad++ 是一款免费开源、轻量高效的 Windows 文本编辑器,支持 C/Python/HTML 等 80+ 语言语法高亮、代码折叠、正则替换、编码转换及插件扩展,专为程序员与文本处理用户打造,完美替代系统记事本。(239字)
|
5天前
|
人工智能 缓存 安全
Claude Code 封号真实原因曝光,这次彻底不装了,直接针对国内开发者的账号下手?
Claude Code 封号潮背后:逆向扒出客户端隐写区域标记,Anthropic 政策收紧叠加 DeepSeek 7 月涨价,国产替代更紧迫。
|
14天前
|
存储 人工智能 监控
QoderWork完全指南:从入门到精通,把“AI实习生”变成你的全能工作搭档
阿里云2026年推出的桌面端AI工作助手QoderWork,不止聊天,更可动手干活:本地运行、安全可控,支持文件整理、数据分析、PPT生成、网页开发等;内置专家套件、多Agent协作与自定义Skills,让AI真正成为你身边的“AI实习生”。
|
6天前
|
人工智能 安全 程序员
终于,Claude Code 封号的原因被曝光了!竟然针对中国用户,植入隐形代码?!
通俗易懂地揭秘 Claude Code 封号的手段,分享一些自己对 AI 编程困境的思考,Codex、Cursor、DeepSeek、智谱 GLM、甚至是豆包,都有所行动了
380 1
|
7天前
|
人工智能 安全 Cloud Native
Higress 新发布:AI Gateway 能力增强,Gateway API 及其推理扩展持续打磨
增强 AI 网关能力,持续打磨 Gateway API 及其推理扩展。
353 124
|
15天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
867 5
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~