
1. 项目概述为什么我们需要一份高质量的Serilog中文文档如果你是一名.NET开发者尤其是在构建需要稳定、可观测的后端服务时日志系统绝对是你绕不开的基础设施。Serilog作为.NET生态中最受欢迎的、结构化日志记录库以其强大的输出能力Sinks、灵活的配置和卓越的性能几乎成了现代.NET项目的标配。然而很多开发者尤其是刚接触Serilog的朋友在享受其强大功能的同时也常常被其官方英文文档中一些晦涩的概念、复杂的配置选项所困扰。我自己在团队中推广和使用Serilog超过五年从早期的Web API到现在的微服务架构Serilog都是日志方案的核心。我亲眼见过不少同事因为对Enrich丰富器、Filter过滤器或某些Sink的特定配置理解不透彻要么埋下了性能隐患要么丢失了关键的业务日志排查问题时犹如大海捞针。官方文档固然权威但语言和文化背景的隔阂有时会让学习曲线变得陡峭。这就是为什么我认为一份由一线开发者基于实战经验整理、翻译并深度解读的《Serilog 2.10 中文文档》至关重要。它不仅仅是一次简单的语言转换更是一次知识的重构和经验的注入。我们的目标是让中文开发者能够像读母语技术博客一样快速、准确、深入地掌握Serilog 2.10的核心精髓避开那些我踩过的“坑”直接应用到生产环境中去。这份文档将围绕Serilog 2.10稳定版涵盖从核心概念、快速入门到高级配置、性能优化和实战排错的全链路内容。2. 核心概念深度解析超越“记录文本”很多新手会把Serilog简单理解为一个“更好用的写日志工具”这其实大大低估了它的价值。Serilog的核心在于“结构化日志”和“强类型事件”。理解这两点是玩转Serilog的关键。2.1 结构化日志从字符串拼接到数据字段传统日志我们常这样写log.Info($用户 {userId} 在 {DateTime.Now} 访问了 {pageUrl})。这行日志对人眼阅读是友好的但对日志分析系统如ELK、Seq来说只是一段无法高效检索的文本。你想筛选所有userId为1001的访问记录对不起只能靠写正则表达式去匹配文本效率极低且容易出错。Serilog的结构化日志改变了游戏规则log.Information(用户 {UserId} 访问了 {PageUrl}, userId, pageUrl);注意这里用的是占位符{UserId}和{PageUrl}而不是字符串插值。Serilog在记录时会生成类似这样的结构化数据消息模板用户 {UserId} 访问了 {PageUrl}属性UserId1001, PageUrl/home/index渲染后的消息用户 1001 访问了 /home/index用于控制台等人类可读输出日志收集系统可以直接索引UserId和PageUrl这两个字段实现毫秒级的精准查询和聚合分析。这是质的变化。实操心得养成使用占位符而非字符串插值的习惯。这不仅是为了结构化Serilog会对相同的消息模板进行缓存和优化能显著提升性能。错误示范log.Info($Value is {someObj})正确示范log.Info(Value is {SomeObject}, someObj)。2.2 日志事件与属性一切皆可附加在Serilog中每一条日志都是一个日志事件。除了消息模板和参数你还可以通过Enrich或ForContext为其附加丰富的属性。// 使用 ForContext 为后续一系列日志添加公共属性 var orderLog log.ForContext(OrderId, order.Id).ForContext(CustomerId, order.CustomerId); orderLog.Information(开始处理订单); // ... 处理逻辑 orderLog.Information(订单处理完成);这样所有通过orderLog记录的日志都会自动带上OrderId和CustomerId属性。在排查特定订单的问题时你只需要在Seq或Kibana中过滤OrderIdxxx就能看到这个订单生命周期的所有相关日志上下文一目了然。2.3 日志级别详解不仅仅是“信息”和“错误”Serilog遵循经典的日志级别Verbose,Debug,Information,Warning,Error,Fatal。如何正确使用它们直接关系到日志的有效性和可维护性。Verbose/Debug用于开发调试。记录非常详细的、可能高频发生的内部状态信息例如“进入方法A”、“变量X的值为Y”。在生产环境通常应关闭。Information记录应用程序的正常流程和有意义的状态变更。例如“用户登录成功”、“订单已创建”、“后台任务已启动”。这是生产环境监控的“面包和黄油”。Warning表示异常或意外情况但应用程序仍能继续运行。例如“数据库查询超时已重试”、“配置文件项缺失使用默认值”。需要关注但未必立即处理。Error表示一个操作失败并且影响了当前请求或功能的完整性。例如“保存订单到数据库失败”、“调用外部API返回5xx错误”。必须被监控和及时处理。Fatal表示导致应用程序崩溃的灾难性错误。记录后应用程序通常会终止。例如“数据库连接池耗尽”、“关键配置文件无法读取”。注意事项避免滥用Information级别记录调试信息这会导致生产环境日志量暴增增加存储成本和检索难度。一个简单的原则这条日志对线上问题排查或业务审计是否有长期价值如果没有就考虑用Debug或更低级别。3. 核心配置实战从入门到精通Serilog的配置是其灵活性的体现但也可能是最让人困惑的部分。我们摒弃花哨的写法聚焦最实用、最稳定的配置模式。3.1 基础配置代码配置 vs 配置文件配置代码配置推荐用于灵活性和强类型using Serilog; Log.Logger new LoggerConfiguration() .MinimumLevel.Debug() // 设置全局最小日志级别 .MinimumLevel.Override(Microsoft, LogEventLevel.Warning) // 覆盖特定命名空间的级别 .WriteTo.Console( outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}) .WriteTo.File(logs/myapp-.txt, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .CreateLogger(); try { Log.Information(应用程序启动); // ... 你的业务代码 } catch (Exception ex) { Log.Fatal(ex, 应用程序启动失败); } finally { Log.CloseAndFlush(); // 至关重要确保所有缓冲日志被写出 }JSON配置文件配置appsettings.json 与ASP.NET Core集成时常用{ Serilog: { Using: [ Serilog.Sinks.Console, Serilog.Sinks.File ], MinimumLevel: { Default: Information, Override: { Microsoft: Warning, System: Warning } }, WriteTo: [ { Name: Console, Args: { outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception} } }, { Name: File, Args: { path: logs/log-.txt, rollingInterval: Day, retainedFileCountLimit: 7, outputTemplate: {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception} } } ], Enrich: [ FromLogContext, WithMachineName, WithThreadId ] } }在Program.cs中读取using Serilog; var configuration new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(appsettings.json) .Build(); Log.Logger new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger();配置选择建议对于简单的控制台应用或类库代码配置更直接。对于ASP.NET Core等复杂应用强烈推荐使用JSON配置因为它可以做到环境隔离appsettings.Development.json,appsettings.Production.json且无需重新编译即可调整日志行为。3.2 输出目标详解常用Sink配置指南Sink决定了日志的去向。Serilog社区提供了上百种Sink这里详解最常用的几个。1. 控制台Sink (Serilog.Sinks.Console)用途本地开发调试。关键配置outputTemplate。建议在开发环境使用包含丰富信息的模板生产环境若需控制台输出则简化。.WriteTo.Console( theme: AnsiConsoleTheme.Code, // 使用彩色主题更易读 outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception} )2. 文件Sink (Serilog.Sinks.File)用途将日志写入滚动文件是最基础的持久化方式。关键配置path文件路径可使用-实现滚动如log-.txt。rollingInterval滚动间隔可选Day,Hour,Minute等。按天滚动最常用。retainedFileCountLimit保留的日志文件数量上限用于自动清理旧日志防止磁盘写满。fileSizeLimitBytes单个日志文件大小限制超过则创建新文件。rollOnFileSizeLimit是否在文件大小超限时滚动。shared多进程写入同一日志文件时需设为true。3. 异步Sink (Serilog.Sinks.Async)这是生产环境的黄金法则永远考虑将你的Sink包装在异步Sink中。原理日志事件先被放入一个内存缓冲区队列由后台线程负责写入实际的Sink如文件、网络。这能避免因为磁盘IO、网络延迟阻塞你的主业务线程极大提升应用程序的响应性能。配置// 安装 Serilog.Sinks.Async 包 .WriteTo.Async(a a.File(logs/app-.txt)) // 将文件Sink异步化 .WriteTo.Async(a a.Console())重要参数bufferSize缓冲区大小默认10000。在日志洪峰时提供缓冲。blockWhenFull缓冲区满时是否阻塞默认true。设为false会在缓冲区满时丢弃日志性能更高但可能丢日志需权衡。4. Seq Sink (Serilog.Sinks.Seq)用途将日志发送到Seq服务器一个专为结构化日志设计的可视化分析工具。配置.WriteTo.Seq(serverUrl: http://localhost:5341)优势提供强大的实时搜索、图表、仪表板和告警功能。是中小团队实现日志集中管理和分析性价比极高的方案。3.3 日志级别动态控制生产环境的一个常见需求是在不重启应用的情况下临时调低某个嘈杂组件的日志级别比如从Information调到Warning或者调高某个问题模块的级别比如从Warning调到Verbose以获取更多细节。Serilog自身不直接提供动态热更新但可以通过以下模式实现模式一与配置系统结合如ASP.NET Core在appsettings.json中修改Serilog.MinimumLevel.Override节点然后使用IConfiguration的Reload机制或配合像IOptionsMonitor这样的接口可以实现配置热重载。但这需要应用程序框架的支持。模式二使用LoggingLevelSwitch// 定义一个可动态调整的级别开关 var levelSwitch new LoggingLevelSwitch(LogEventLevel.Information); Log.Logger new LoggerConfiguration() .MinimumLevel.ControlledBy(levelSwitch) // 全局级别受此开关控制 .WriteTo.Console() .CreateLogger(); // 在运行时可以通过API端点、管理命令等方式调整 levelSwitch.MinimumLevel LogEventLevel.Warning; // 动态将全局级别调整为Warning你可以将levelSwitch实例注入到你的配置管理服务中通过一个管理接口来动态调整它。4. 高级特性与性能优化实战当你掌握了基础配置后这些高级特性将让你的日志系统更加强大和高效。4.1 日志上下文与作用域我们之前提到了ForContext但LogContext才是实现请求级、事务级日志关联的利器。它特别适用于Web应用。using (LogContext.PushProperty(TransactionId, Guid.NewGuid())) using (LogContext.PushProperty(UserId, currentUser.Id)) { Log.Information(开始处理请求); // 在这个作用域内记录的所有日志都会自动附加 TransactionId 和 UserId 属性 await ProcessOrderAsync(); Log.Information(请求处理完成); }在ASP.NET Core中通常通过中间件自动为每个请求创建LogContext并推送请求ID、路径等属性。这能让你在日志系统中轻松追踪一个请求的完整生命周期。4.2 自定义Enricher丰富日志信息Enricher用于自动为所有日志事件添加属性。内置的如WithMachineName,WithThreadId很好用但自定义Enricher更能满足业务需求。示例添加应用程序版本和环境信息public class ApplicationInfoEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var appVersion Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? unknown; var environment Environment.GetEnvironmentVariable(ASPNETCORE_ENVIRONMENT) ?? Production; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(AppVersion, appVersion)); logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(Environment, environment)); } } // 配置中使用 Log.Logger new LoggerConfiguration() .Enrich.With(new ApplicationInfoEnricher()) // ... 其他配置这样每条日志都会带有AppVersion和Environment在混合部署多版本、多环境的场景下排查问题效率倍增。4.3 性能优化黄金法则日志记录不当会成为性能瓶颈。以下是几条铁律使用异步Sink如前所述这是最重要的优化。避免在日志语句中进行昂贵的计算或序列化。错误示例Log.Debug(对象状态: {Obj}, JsonConvert.SerializeObject(largeObj))。即使Debug级别被禁用SerializeObject这个方法也会被执行造成无谓的性能损耗。正确做法使用条件日志或惰性求值。// 条件日志 if (log.IsDebugEnabled) { log.Debug(对象状态: {Obj}, JsonConvert.SerializeObject(largeObj)); } // 或使用 Serilog 的惰性结构捕获性能更优 log.Debug(对象状态: {Obj}, largeObj); // Serilog会智能地处理结构化对象谨慎使用结构化解构操作符log.Information(收到订单 {Order}, order)会将整个order对象及其所有属性递归地序列化为日志属性。如果对象很大、很深会显著增加日志体积和处理开销。只记录必要的属性。合理配置日志级别生产环境严格控制Information及以上级别的日志量。将第三方库如Microsoft的日志级别覆盖为Warning可以过滤掉大量框架内部的信息级日志。为文件Sink设置合理的滚动和保留策略避免生成海量小文件或无限增长的单一大文件。5. 与ASP.NET Core深度集成在ASP.NET Core中集成Serilog是目前最主流的场景集成得当可以无缝接管框架自身的日志系统。5.1 标准集成模式首先安装核心集成包Serilog.AspNetCore。在Program.cs中使用UseSerilog来替换默认的日志提供程序using Serilog; var configuration new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(appsettings.json) .AddJsonFile($appsettings.{Environment.GetEnvironmentVariable(ASPNETCORE_ENVIRONMENT) ?? Production}.json, true) .Build(); // 在Host构建之前配置Logger Log.Logger new LoggerConfiguration() .ReadFrom.Configuration(configuration) // 从配置文件读取 .Enrich.FromLogContext() // 必须用于获取ASP.NET Core的请求上下文 .Enrich.WithMachineName() .Enrich.WithThreadId() .CreateLogger(); try { var builder WebApplication.CreateBuilder(args); // 关键使用Serilog作为日志提供程序 builder.Host.UseSerilog(); var app builder.Build(); // ... 配置中间件 app.Run(); } catch (Exception ex) { Log.Fatal(ex, 应用程序启动失败); } finally { Log.CloseAndFlush(); }5.2 捕获请求日志与异常Serilog.AspNetCore包自动为你添加了请求日志中间件。为了更精细地控制你可以手动配置app.UseSerilogRequestLogging(options { options.MessageTemplate HTTP {RequestMethod} {RequestPath} 响应 {StatusCode} 耗时 {Elapsed:0.0000} ms; options.GetLevel (ctx, elapsed, ex) ex ! null ? LogEventLevel.Error : // 有异常则为Error ctx.Response.StatusCode 499 ? LogEventLevel.Error : // 5xx服务器错误 LogEventLevel.Information; // 其他为Information // 丰富日志事件 options.EnrichDiagnosticContext (diagnosticContext, httpContext) { diagnosticContext.Set(RequestHost, httpContext.Request.Host.Value); diagnosticContext.Set(RequestScheme, httpContext.Request.Scheme); diagnosticContext.Set(RemoteIpAddress, httpContext.Connection.RemoteIpAddress); diagnosticContext.Set(UserId, httpContext.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? anonymous); }; });这段配置会为每个请求记录一条日志包含方法、路径、状态码、耗时并附加上下文信息。通过GetLevel回调我们可以智能地决定日志级别避免记录过多成功的请求日志在生产环境可能很嘈杂。5.3 集成中的常见陷阱与解决陷阱一启动和关闭异常未被记录如果异常发生在WebApplication创建或运行之前默认的.NET日志提供程序可能还没被Serilog替换。我们的try-catch-finally块就是为了解决这个问题确保任何启动异常都能被Serilog捕获并记录到配置的Sink中。陷阱二依赖注入容器中的日志在控制器或服务中你应该注入通用的ILoggerT接口而不是Serilog.ILogger。ASP.NET Core的日志抽象层会自动将日志转发给Serilog。public class MyService { private readonly ILoggerMyService _logger; public MyService(ILoggerMyService logger) { _logger logger; // 正确 } }陷阱三LogContext丢失确保配置中包含了.Enrich.FromLogContext()。某些异步操作或后台任务中LogContext可能会丢失需要使用LogContext.PushProperty重新建立或使用Serilog.Context.LogContext的克隆功能。6. 生产环境部署与问题排查将配置好的Serilog应用到生产环境还需要最后几步的打磨。6.1 环境差异化配置使用appsettings.{Environment}.json文件来管理不同环境的配置。appsettings.Development.json配置控制台彩色输出日志级别为Debug或Verbose。appsettings.Production.json关闭控制台输出或仅输出Error日志级别为Information并配置异步文件、Seq等Sink设置合理的文件滚动和保留策略。6.2 关键监控指标日志量突增监控单位时间内日志事件的数量。突然增长可能意味着出现了异常循环、配置错误或遭受攻击。Error/Fatal级别日志设置告警任何此类日志产生都应立即通知如通过Seq的告警功能、邮件、钉钉/企业微信机器人。日志文件磁盘空间监控日志所在磁盘的使用情况确保保留策略生效避免磁盘写满导致服务宕机。日志写入延迟如果使用异步Sink观察其内部队列长度。如果队列持续处于高位说明Sink写入速度跟不上日志产生速度可能需要优化Sink如更换更快的存储或减少日志量。6.3 典型问题排查清单问题现象可能原因排查步骤无日志输出1. 日志级别设置过高。2. Sink配置错误如文件路径无权限。3. 未调用Log.CloseAndFlush()控制台应用常见。1. 检查MinimumLevel及Override设置。2. 检查Sink的路径、网络地址是否可达。3. 确保应用程序正确关闭日志。日志文件巨大1. 日志级别过低如生产环境开了Debug。2. 未配置文件滚动和保留策略。3. 在日志中序列化了过大对象。1. 检查生产环境配置文件。2. 检查rollingInterval和retainedFileCountLimit。3. 审查代码中的日志语句避免记录大型对象。应用程序性能下降1. 未使用异步Sink。2. 在日志语句中执行了昂贵操作。3. 某个Sink阻塞如网络Sink超时。1. 为所有Sink包裹WriteTo.Async。2. 使用条件日志或检查操作符的使用。3. 检查网络Sink的连接状态和超时设置。Seq中看不到日志1. Seq服务器地址错误或不可达。2. API密钥配置错误如果Seq开启了认证。3. 日志级别被过滤。1. 检查Seq Sink的serverUrl。2. 检查apiKey配置。3. 在Seq界面检查是否有对应的应用程序或日志源。日志中缺少关键属性如RequestId1. 未调用.Enrich.FromLogContext()。2. 在异步代码中LogContext丢失。1. 确认配置中包含该Enricher。2. 在异步操作开始时使用LogContext.PushProperty或AsyncLocal传递上下文。6.4 一个生产就绪的配置示例以下是结合了上述所有最佳实践的一个appsettings.Production.json配置示例{ Serilog: { Using: [ Serilog.Sinks.Async, Serilog.Sinks.File, Serilog.Sinks.Seq ], MinimumLevel: { Default: Information, Override: { Microsoft: Warning, System: Warning, Microsoft.Hosting.Lifetime: Information // 保留Hosting生命周期日志 } }, WriteTo: [ { Name: Async, Args: { configure: [ { Name: File, Args: { path: /var/log/myapp/app-.log, rollingInterval: Day, retainedFileCountLimit: 30, fileSizeLimitBytes: 104857600, // 100MB rollOnFileSizeLimit: true, outputTemplate: {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {RequestId} {Message:lj}{NewLine}{Exception} } } ] } }, { Name: Async, Args: { configure: [ { Name: Seq, Args: { serverUrl: https://seq.your-company.com, apiKey: your-production-api-key, controlLevelSwitch: null } } ] } } ], Enrich: [ FromLogContext, WithMachineName, WithThreadId ], Properties: { Application: MyApp.API, Environment: Production } } }这份配置实现了异步写入、按天和按大小滚动日志文件、保留30天、同时写入本地文件和远程Seq、过滤了大部分框架信息日志、并丰富了机器和线程信息。你可以以此为蓝本调整出最适合自己生产环境的方案。日志不是简单的“记录”而是可观测性的基石。一份好的文档加上对工具深入的理解能让你在系统出现问题时不再手足无措而是可以像侦探一样顺着日志留下的清晰线索直击问题根源。希望这份基于Serilog 2.10的深度解读能成为你构建可靠.NET应用的一块坚实拼图。