
先把话放前面Serilog 这玩意儿我在 .NET 工程里已经用了快五年从最开始只是“不想再用字符串拼日志”到后来靠它救回过几次线上事故的排查效率算是把结构化日志这条路从头到尾走了一遍。标题里那个“侣”字虽然像笔误但我倒觉得挺妙——日志系统本来就是开发者的伴侣平时不起眼出了问题才知道它有多重要。这篇文章我不打算给你堆一堆文档翻译而是从“结构化日志为什么值得用”开始再深入到 Serilog 在真实 .NET 工程里的配置、踩坑和调优全程按我自己的实操经验来写希望能帮你少走点弯路。1. 先想清楚我们要的到底是一份日志还是一组可检索的数据1.1 传统文本日志为什么越来越不够用我上个月排查一个服务内存暴涨的问题打开老项目里的日志文件看到的全是这种内容2025-01-15 10:23:45 INFO 用户ID: 10086 执行下单操作 success 2025-01-15 10:23:45 INFO 订单号: 202501151023450001 已创建说实话这种日志在单体小项目里不是不能用但一旦服务实例超过三个、请求量上来之后问题就非常现实没法按字段检索。你想查某个订单号相关的所有日志只能靠 grep 硬匹配一旦日志里有人手滑把订单号格式写成“订单号xxx”而不是“订单号: xxx”你就漏数据了。上下文信息会丢。用户 ID、请求 ID、机器 IP、线程 ID 这些信息要么不打印要么散落在不同日志里想把一次请求的完整链路串起来全靠肉眼。格式不可控。每个开发写日志的习惯不一样有的用逗号分隔有的用竖线有的直接拼一个长字符串后续想接日志平台做分析解析规则能写到怀疑人生。无法做统计分析。你想统计一下最近一小时下单失败率文本日志只能靠人工数行数或者再写一个临时脚本去正则匹配效率极低。可能有人会说“我用日志平台不就行了”但问题是传统文本日志即便推送到 ELK 等平台也只是一堆无结构的字符串解析规则依然很脆弱。真正的问题出在日志写入的那一刻——日志本身不具备结构性后面再怎么处理都是亡羊补牢。1.2 结构化日志的“数据化”逻辑结构化日志的核心思想很简单不要把日志当成一行字符串而是当成一个事件event。这个事件自带一组命名字段常见的公共字段包括Timestamp事件发生时间精确到毫秒甚至微秒。Level日志级别比如 Information、Warning、Error。MessageTemplate日志模板比如User {UserId} created order {OrderId}。RenderedMessage渲染后的完整文本人类可读。Properties附加的上下文内容比如 UserId、OrderId、MachineName、ThreadId 等等。Exception异常对象完整信息包括堆栈。这就带来一个巨大转变日志从“给人看”变成“给机器看 给人看”。当你往 Serilog 里写一条结构化日志时它写出去的不只是那行文字而是带有明确语义的一组键值对。举个例子你在代码里这么写Log.Information(User {UserId} created order {OrderId}, userId, orderId);Serilog 底层会把UserId和OrderId作为独立字段保存。传给 Seq、Elasticsearch 等平台后你直接就能做类似 SQL 的查询UserId 10086 and Level Error而不是用正则去匹配“用户ID: 10086 错误”。如果需要统计同一模板的出现次数直接按MessageTemplate聚合就行这在定位系统性故障时非常有用。一句话总结这个转变传统日志是“写一段话”结构化日志是“记一条数据”。后面的所有分析和告警都建立在这条数据之上。1.3 为什么偏偏是 Serilog.NET 生态里的日志库不少老牌的 NLog、微软自带的Microsoft.Extensions.Logging还有 Serilog我都用过。最终在绝大部分 .NET 项目里选 Serilog理由是这几点和 .NET 生态融合得最好。Serilog 提供UseSerilog()扩展可以直接挂到 ASP.NET Core 的 Host 上接管ILoggerT的输出原有代码几乎不用改。Sink 生态极其丰富。Console、File、Seq、Elasticsearch、MSSqlServer、ClickHouse、Kafka、HTTP 等等都有官方或社区维护的 Sink基本覆盖了主流日志落地场景。性能可靠。Serilog 的 MessageTemplate 有缓存机制日志参数不会被反复格式化在异步 sink 的配合下对业务线程的阻塞很小。社区活跃资料多。相比于 NLogSerilog 在结构化日志这块的文档、博客、Issue 讨论都要更完善踩坑之后很容易找到解决方案。当然NLog 的字典、布局渲染器也很强大如果你已经深度使用 NLog 并且团队没有结构化的强需求也可以不换。但如果你现在还在用Console.WriteLine拼日志或者刚接手一个想接日志平台的新项目Serilog 是一个不会后悔的起点。2. Serilog 的五个核心零件搞懂就能组装任意日志流水线Serilog 虽然功能多但核心模型并不复杂。我习惯把它理解成一条日志流水线Logger 是入口Sink 是出口Enricher 负责在中间加工Formatter 决定输出长什么样LogContext 用来给一批日志打上同一个标签。把这五个零件搞懂就掌握了 80% 的日常使用。2.1 Logger日志管道的起点Logger 是 Serilog 的根对象所有的日志事件都从它开始。最简单的创建方式是这样using Serilog; Log.Logger new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); Log.Information(Hello, Serilog!);这段代码创建了一个只输出到控制台的 Logger。LoggerConfiguration是 Serilog 的配置入口后面所有.WriteTo、.Enrich、.MinimumLevel都是链式拼接。CreateLogger()之后你就有了一个可用的Serilog.ILogger实例。我建议在程序一启动时就初始化静态的Log.Logger并在关闭时优雅释放Log.Logger new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); try { // 业务代码 Log.Information(Service started); } catch (Exception ex) { Log.Fatal(ex, Service terminated unexpectedly); throw; } finally { Log.CloseAndFlush(); }CloseAndFlush()很重要它会把还没写完的日志缓冲区刷新掉避免进程退出时丢日志。这个我在后面“常见问题”里还会再提。2.2 Sink日志到底写到哪里去Sink 就是 Serilog 的“输出目标”。官方默认提供 Console、File、Debug、EventLog 这些更多常用的需要单独装 NuGet 包目标NuGet 包名适合场景控制台Serilog.Sinks.Console本地调试、Docker 容器 stdout文件Serilog.Sinks.File常规服务器日志留存SeqSerilog.Sinks.Seq集中日志平台适合中大型项目ElasticsearchSerilog.Sinks.Elasticsearch对接 ELKMSSqlServerSerilog.Sinks.MSSqlServer数据库存储适合已有 SQL Server 的团队HTTPSerilog.Sinks.Http自定义接口接收日志我见过很多项目第一个接的 Sink 就是Console和File这是一个非常合理的起点。但如果你有多个实例日志散落在每台机器上排查问题时要挨个登服务器翻文件那就太痛苦了。所以我建议只要项目不是单机玩具就尽早接一个集中式日志 SinkSeq 也好ELK 也好哪怕是简单地往一个共享 HTTP 接口推都比“登服务器 grep”强得多。多个 Sink 可以同时使用例如Log.Logger new LoggerConfiguration() .WriteTo.Console() .WriteTo.File(logs/app-.log, rollingInterval: RollingInterval.Day) .WriteTo.Seq(http://seq-host:5341) .CreateLogger();这样本地调试时看控制台生产环境看文件和 Seq一条日志同时落多个目标互不干扰。2.3 Enricher给日志自动加上“上下文备注”Enricher 的作用是给每一条日志事件自动附加一些上下文信息不用你每次手动写。比如你想知道每条日志来自哪台机器、哪个进程、哪个线程就可以这样配置Log.Logger new LoggerConfiguration() .Enrich.WithMachineName() .Enrich.WithProcessId() .Enrich.WithThreadId() .WriteTo.Console() .CreateLogger();上面的几个 Enricher 都来自Serilog.Enrichers.Environment、Serilog.Enrichers.Thread和Serilog.Enrichers.Process这些包NuGet 上都有。还有几个比较常用的WithExceptionDetails来自Serilog.Exceptions能记录更完整的异常信息结构而不仅仅是ToString()。WithCorrelationId配合CorrelationId中间件把一个请求里的所有日志都打上同一个 CorrelationId。WithHttpRequestId、WithClientIp在 ASP.NET Core 场景里很实用能帮你把日志和具体请求关联起来。Enricher 最大的价值是不用靠开发人员记住。人是最不可靠的你没法要求每个人都记得往日志里塞机器名。直接用 Enricher 统一处理一劳永逸。2.4 Formatter控制落盘和输出的长相Formatter 决定日志输出成什么格式。默认情况下Serilog 输出的是文本模板类似[10:23:45 INF] User 10086 created order 202501151023450001本地调试时这种格式看着舒服但如果你想接日志平台最好直接用 JSON 格式输出。Serilog 提供了CompactJsonFormatter来自Serilog.Formatting.Compact用法如下.WriteTo.File(new CompactJsonFormatter(), logs/app-.json, rollingInterval: RollingInterval.Day)输出大概长这样{t:2025-01-15T10:23:45.123Z,mt:User {UserId} created order {OrderId},l:Information,UserId:10086,OrderId:202501151023450001}t是时间戳mt是消息模板l是日志级别后面的字段全是结构化属性。这种格式对 Seq、Elasticsearch 等平台特别友好因为它们可以自动识别字段类型。我自己在接 Seq 时基本都会选择 Compact JSON Formatter能避免不少类型映射问题。如果你不需要接平台只想控制文本模板也可以用outputTemplate调整显示样式.WriteTo.Console(outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception})不过我的建议是线上文件尽量用 JSON本地调试再看文本两者不冲突可以分别用两个 Sink。2.5 LogContext一个 request 里的日志互相认亲在 Web 应用里一个请求会经历很多层代码如果每一层都写日志但没有把请求 ID 串起来那排查起来就是灾难。Serilog 的LogContext就是为了解决这个问题。你可以在请求入口处压入一个属性让这个请求及其后续产生的所有日志都带上这个属性using Serilog.Context; app.Use(async (context, next) { using (LogContext.PushProperty(RequestId, context.TraceIdentifier)) { await next(); } });这样在这个请求周期内哪怕是深层服务里打印的一条Log.Warning(xxx)也会自动带上RequestId。后面在日志平台里查RequestId xxx就能把这个请求经过的所有日志一次性捞出来。需要注意LogContext和普通的 Enricher 不同它是有作用域的。using块结束时PushProperty 的属性就会被移除不会污染其他请求。所以一定要确保PushProperty在正确的作用域内。ASP.NET Core 中间件的写法天然保证了这一点。3. 从零到一在 .NET 工程里把 Serilog 真正用起来理论说完了进入正题。这一节我给出一套从控制台到 ASP.NET Core 的完整落地流程每一步都有代码和配置你照着做基本就能跑通。3.1 最小接入控制台程序三步跑通假设你新建了一个 .NET 控制台项目想最快看到效果可以按这几步走。第一步安装 NuGet 包。在项目文件里添加以下包引用以 .NET 8 为例PackageReference IncludeSerilog Version4.2.0 / PackageReference IncludeSerilog.Sinks.Console Version6.0.0 / PackageReference IncludeSerilog.Sinks.File Version6.0.0 /版本号建议以 NuGet 上的最新稳定版为准只要 API 没有大改下面的代码都适用。第二步配置 Logger 并写日志。using Serilog; Log.Logger new LoggerConfiguration() .MinimumLevel.Information() .Enrich.WithMachineName() .Enrich.WithThreadId() .WriteTo.Console() .WriteTo.File(logs/app-.log, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .CreateLogger(); try { var userId 10086; var orderId 202501151023450001; Log.Information(User {UserId} created order {OrderId}, userId, orderId); throw new InvalidOperationException(sample exception); } catch (Exception ex) { Log.Error(ex, Something went wrong while processing order {OrderId}, orderId); } finally { Log.CloseAndFlush(); }第三步运行并查看输出。控制台会显示类似这样的文本[10:23:45 INF] User 10086 created order 202501151023450001 [10:23:45 ERR] Something went wrong while processing order 202501151023450001同时logs目录下会出现当天的日志文件。这里我特意用了rollingInterval: RollingInterval.Day表示每天一个文件文件名会带上日期比如app-20250115.log。retainedFileCountLimit: 7表示最多保留 7 个文件自动清理旧文件防止磁盘被日志塞满。3.2 在 ASP.NET Core 里集成 DI 与 ILoggerWeb 项目里推荐用UseSerilog扩展方法把 Serilog 挂到通用主机上。这样你既可以在代码里用ILoggerT也能让 ASP.NET Core 框架自身的日志比如 MVC 的请求日志走 Serilog 输出。先安装包PackageReference IncludeSerilog.AspNetCore Version8.0.0 /然后加装你需要的 SinkPackageReference IncludeSerilog.Sinks.Console Version6.0.0 / PackageReference IncludeSerilog.Sinks.File Version6.0.0 / PackageReference IncludeSerilog.Sinks.Seq Version8.0.0 /Program.cs 里这样写using Serilog; var builder WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, services, configuration) { configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.WithMachineName() .WriteTo.Console(); }); builder.Services.AddControllers(); var app builder.Build(); app.MapControllers(); app.Run();这里ReadFrom.Configuration会读取appsettings.json里Serilog节点下的配置ReadFrom.Services允许 Serilog 从依赖注入容器里解析一些自定义服务和 Enricher属于高级用法但建议一开始就保留。然后在 Controller 里直接注入ILoggerT[ApiController] [Route(api/orders)] public class OrdersController : ControllerBase { private readonly ILoggerOrdersController _logger; public OrdersController(ILoggerOrdersController logger) { _logger logger; } [HttpPost] public IActionResult Create(OrderRequest request) { _logger.LogInformation(Creating order for user {UserId}, request.UserId); // 业务处理 return Ok(); } }注意这里用的是 .NET 自带的ILoggerT不是 Serilog 的ILogger。因为有UseSerilogDI 里的ILoggerT底层输出已经被接管到 Serilog所以业务代码里的日志同样会享受结构化输出、Enricher、Sink 等能力。3.3 用 appsettings.json 管配置别把配置写死在代码里把日志配置写死在代码里对于一个小工具没问题但对真正要上线的服务并不友好——你没法在不重新发布的情况下调整日志级别、切换日志平台、处理敏感信息等。Serilog 官方提供了一个配置包Serilog.Settings.Configuration可以直接从appsettings.json读取配置。安装包PackageReference IncludeSerilog.Settings.Configuration Version9.0.0 /然后在appsettings.json里写{ Serilog: { MinimumLevel: { Default: Information, Override: { Microsoft.AspNetCore: Warning, System: Warning } }, Enrich: [ WithMachineName, WithThreadId ], WriteTo: [ { Name: Console }, { Name: File, Args: { path: logs/app-.log, rollingInterval: Day, retainedFileCountLimit: 7, outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception} } }, { Name: Seq, Args: { serverUrl: http://seq-host:5341, apiKey: your-api-key } } ] } }这样你可以发布之后改配置文件直接调整日志级别或者换日志平台不需要动代码。MinimumLevel.Override用来压低框架自带的噪音日志比如Microsoft.AspNetCore的信息日志我一般会调到Warning否则请求一来整个日志刷屏根本看不清楚业务日志。需要注意的是如果你用ReadFrom.Configuration读取配置那么appsettings.json里的Serilog节点就是把LoggerConfiguration的链式调用 JSON 化。Args对应你代码里的参数名比如path、rollingInterval大小写要和你配置的 Sink API 参数匹配这点经常有人踩坑。3.4 关键参数怎么选RollingInterval、RetainedFileCountLimit、Shared、Buffered文件 Sink 的配置不只是路径和一个滚动间隔那么简单这里有几个参数很关键。RollingInterval表示日志文件按什么周期滚动。可选值有Infinite、Year、Month、Day、Hour、Minute。我推荐生产环境至少用Day如果日志量特别大可以按Hour滚动。按天滚动的好处是一天一个文件排查问题时可以直接去翻某一天的文件清理也方便。RetainedFileCountLimit保留多少个历史日志文件。默认是 31也就是大约一个月的日志。如果磁盘空间充足可以设大一点如果服务日志非常频繁建议根据磁盘容量估算一下。公式很简单单文件大小 x 保留文件数 磁盘可用空间比如一天产生 2GB 日志保留 7 天就是 14GB你要确保磁盘有这个余量。Shared这个参数很多人没注意。在多进程同时写同一个日志文件时如果不开shared: true可能会遇到文件被占用、写入失败的问题。尤其是在 IIS 宿主环境或者多个 worker 进程的场景下建议打开.WriteTo.File(logs/app-.log, rollingInterval: RollingInterval.Day, shared: true)不过开启shared会有少量性能开销单进程场景不需要开。Bufferedbuffered: true时Serilog 会把日志先写入缓冲区满了再刷到磁盘。这个参数能提升吞吐但缺点是如果进程崩溃或断电缓冲区里的日志会丢失。我自己的建议是默认不要开 buffered。日志追求的是可靠不是极限性能。真到并发高到必须开缓冲的时候你应该优先考虑异步 sink 而不是文件缓冲。另外提一句如果你是容器环境建议文件日志输出到 stdout 而不是文件由容器引擎统一收集这样避免容器内文件轮转和宿主机日志收集之间产生冲突。Serilog 的 Console Sink 写入 stdout是最省心的选择。4. 真正上线后才会踩到的坑含自查清单这一节是全文的干货核心。我把自己在多个项目里真实踩过的坑以及常见的排查经验整理出来希望对你有用。4.1 性能陷阱MessageTemplate 里别做字符串插值Serilog 的MessageTemplate是它的灵魂但很多人没意识到写日志时如果用了字符串插值就等于绕过了 MessageTemplate 的结构化优势。反例Log.Information($User {userId} created order {orderId});这样写Serilog 收到的消息已经是一个完整字符串userId和orderId不会被拆成独立字段后续没法按字段检索也失去了模板缓存的好处。正例Log.Information(User {UserId} created order {OrderId}, userId, orderId);这样写有几个好处模板可缓存。相同模板只解析一次性能更高。字段可检索。UserId和OrderId是独立属性日志平台可以直接索引。聚合分析方便。按MessageTemplate统计相同日志出现次数定位异常频率非常方便。如果你在一段性能敏感的代码里打日志更要注意即使日志级别是 Verbose只要你写了Log.Information(...)参数表达式是会被计算的。比如Log.Information(User {UserId} paid {Amount}, userId, CalculateAmount());即使日志被级别过滤掉CalculateAmount()也会执行。要避免这种开销可以用Log.IsEnabled(LogEventLevel.Information)先判断或者把计算放到分支里。这些细节在 QPS 高的服务里差别很大。4.2 敏感信息日志脱敏必须提前做日志里最容易翻车的就是把密码、手机号、身份证、Token 打出来。Serilog 本身不会自动脱敏必须自己处理。我常用的方案是一个自定义 Enricher在日志事件发送到 Sink 之前把敏感字段替换成掩码。简单实现如下public class SensitiveDataEnricher : ILogEventEnricher { private readonly string[] _sensitiveProperties { Password, Token, PhoneNumber, CreditCard }; private readonly string _maskedValue ***; public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var propertiesToMask logEvent.Properties .Where(p _sensitiveProperties.Contains(p.Key, StringComparer.OrdinalIgnoreCase)) .ToList(); foreach (var property in propertiesToMask) { var maskedProperty new LogEventProperty(property.Key, new ScalarValue(_maskedValue)); logEvent.AddOrUpdateProperty(maskedProperty); } } }然后在配置里加一行.Enrich.WithSensitiveDataEnricher()另外不要只依赖 Enricher更根本的办法是在入口处就把敏感信息过滤掉。比如收到请求 DTO 时不要把 Password 字段放到日志里。我的经验是日志脱敏永远不能只指望一个过滤器事前的谨慎比事后的掩盖更可靠。4.3 日志落库和 ES/Seq 接入时的格式坑接 Seq 一般很顺因为 Serilog 和 Seq 同属一家公司格式天然兼容。但接 Elasticsearch 时我踩过几个坑时间戳格式不统一。Serilog 默认的t是自定义格式而 Elasticsearch 期望 ISO 8601 格式。如果你用CompactJsonFormatter通常没问题但如果自己写了自定义 Formatter很容易出现日期解析失败。Level 字段映射。ES 里日志级别最好映射成 keyword 类型否则做 terms 聚合时类型不一致会报错。索引模板。用Serilog.Sinks.Elasticsearch时建议提前配置好索引生命周期管理ILM否则日志索引会无限增长最终把磁盘塞满。批量写入与重试。ES Sink 默认是批量写入但网络抖动时会有堆积。你需要关注BatchPostingLimit、Period参数以及失败后的重试机制。如果 ES 不可用日志会积压在内存里极端情况下可能造成 OOM。我的建议是如果团队没有专门的 ES 运维能力优先用 Seq 或者云厂商的日志服务省心很多。ES 的玩法很多但坑也很多需要投入人力才能维护好。4.4 几个常见异常和排查速查表我在实际使用中遇到过这些问题整理成表格方便你自查现象可能原因解决方法日志完全没有输出没配置MinimumLevel或 Sink检查 LoggerConfiguration 是否至少有WriteTo和一个有效级别文件没有生成路径不存在或权限不足检查运行账号是否有写权限路径目录是否存在控制台中文乱码控制台代码页不是 UTF-8在 Program.cs 开头加Console.OutputEncoding Encoding.UTF8;或在部署环境设置DOTNET_SYSTEM_GLOBALIZATION_INVARIANT1日志文件不按天滚动文件名没加日期占位符路径必须包含-.log这样的日期占位符如app-.logSeq 收不到日志网络不通、API Key 错误、Seq 服务器磁盘满用 curl 测一下 Seq 的/api/events/raw接口检查 API Key日志量巨大磁盘爆掉retainedFileCountLimit设置太大或没设置估算单文件大小合理设置保留天数异步日志丢日志buffered: true或异步 sink 导致进程退出时来不及刷新确保进程退出前调用Log.CloseAndFlush()多进程写同一文件失败文件被占用开启shared: true或改用 Console Sink 容器日志收集这里再强调一下Log.CloseAndFlush()。在 ASP.NET Core 里UseSerilog已经自动帮你处理了宿主关闭时的 flush 逻辑但在控制台、Windows 服务或其他自定义宿主里你必须自己调用。我见过不止一次程序莫名退出后日志文件是空的就是因为漏了这一步。4.5 我编排生产日志流水线时的一组推荐模板最后给一套我在生产环境经常用的配置模板包含控制台、文件和 Seq 三种 Sink兼顾本地调试、文件留存和集中检索。appsettings.json{ Serilog: { MinimumLevel: { Default: Information, Override: { Microsoft.AspNetCore: Warning, Microsoft.EntityFrameworkCore: Warning, System: Warning } }, Enrich: [ WithMachineName, WithThreadId, WithExceptionDetails ], WriteTo: [ { Name: Console, Args: { outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception} } }, { Name: File, Args: { path: logs/app-.log, rollingInterval: Day, retainedFileCountLimit: 14, shared: true } }, { Name: Seq, Args: { serverUrl: http://seq-host:5341, apiKey: your-api-key, compact: true } } ] } }对应的代码里builder.Host.UseSerilog((context, services, configuration) { configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.WithExceptionDetails(); });Enrich.WithExceptionDetails()来自Serilog.Exceptions包可以把异常内部的属性、Data、InnerException 结构化输出排查问题时信息量比默认的ToString()大得多。我强烈建议加上这个。还有一点如果服务和日志平台都在 Kubernetes 集群里文件 Sink 其实可以去掉直接 Console 输出到 stdout由容器运行时统一采集。但如果是传统虚机部署文件 Sink 是必需的因为运维一般会根据文件路径配置采集规则。这里没有标准答案看你的部署环境。最后分享一个小习惯我一般会在Program.cs顶部先创建 Logger然后确保UseSerilog的配置能覆盖到 Startup 阶段。这样即使 DI 容器初始化失败也有一条 Fatal 日志能帮助定位。尤其是注册服务时抛异常的情况如果没有提前初始化 Logger你连错误日志都看不见只能看到程序闪退。用 Serilog 这几年我最大的体会是它不只是换了一个日志库而是改变了你思考日志的方式。以前我排查问题是“登录服务器、翻文件、grep 关键字”现在我打开日志平台直接用字段查询和聚合统计几分钟就能定位到问题范围。这种效率提升刚开始感觉不明显等到真正经历一次几百万请求量的线上故障时你会庆幸自己当初做了这个选择。如果你也在 .NET 工程里遇到过日志难查、格式混乱、告警困难的问题Serilog 这套方案值得你花一晚上试一遍。