Serilog与.NET结构化日志:从入门到生产实践
2026/9/11 21:22:45 网站建设 项目流程

先把话放前面: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 底层会把UserIdOrderId作为独立字段保存。传给 Seq、Elasticsearch 等平台后,你直接就能做类似 SQL 的查询:

UserId = 10086 and Level = Error

而不是用正则去匹配“用户ID: 10086 错误”。如果需要统计同一模板的出现次数,直接按MessageTemplate聚合就行,这在定位系统性故障时非常有用。

一句话总结这个转变:传统日志是“写一段话”,结构化日志是“记一条数据”。后面的所有分析和告警,都建立在这条数据之上。

1.3 为什么偏偏是 Serilog

.NET 生态里的日志库不少,老牌的 NLog、微软自带的Microsoft.Extensions.Logging,还有 Serilog,我都用过。最终在绝大部分 .NET 项目里选 Serilog,理由是这几点:

  1. 和 .NET 生态融合得最好。Serilog 提供UseSerilog()扩展,可以直接挂到 ASP.NET Core 的 Host 上,接管ILogger<T>的输出,原有代码几乎不用改。
  2. Sink 生态极其丰富。Console、File、Seq、Elasticsearch、MSSqlServer、ClickHouse、Kafka、HTTP 等等都有官方或社区维护的 Sink,基本覆盖了主流日志落地场景。
  3. 性能可靠。Serilog 的 MessageTemplate 有缓存机制,日志参数不会被反复格式化;在异步 sink 的配合下,对业务线程的阻塞很小。
  4. 社区活跃,资料多。相比于 NLog,Serilog 在结构化日志这块的文档、博客、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对接 ELK
MSSqlServerSerilog.Sinks.MSSqlServer数据库存储,适合已有 SQL Server 的团队
HTTPSerilog.Sinks.Http自定义接口接收日志

我见过很多项目第一个接的 Sink 就是ConsoleFile,这是一个非常合理的起点。但如果你有多个实例,日志散落在每台机器上,排查问题时要挨个登服务器翻文件,那就太痛苦了。所以我建议,只要项目不是单机玩具,就尽早接一个集中式日志 Sink,Seq 也好,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.EnvironmentSerilog.Enrichers.ThreadSerilog.Enrichers.Process这些包,NuGet 上都有。还有几个比较常用的:

  • WithExceptionDetails:来自Serilog.Exceptions,能记录更完整的异常信息结构,而不仅仅是ToString()
  • WithCorrelationId:配合CorrelationId中间件,把一个请求里的所有日志都打上同一个 CorrelationId。
  • WithHttpRequestIdWithClientIp:在 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 Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.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.logretainedFileCountLimit: 7表示最多保留 7 个文件,自动清理旧文件,防止磁盘被日志塞满。

3.2 在 ASP.NET Core 里集成 DI 与 ILogger

Web 项目里推荐用UseSerilog扩展方法,把 Serilog 挂到通用主机上。这样你既可以在代码里用ILogger<T>,也能让 ASP.NET Core 框架自身的日志(比如 MVC 的请求日志)走 Serilog 输出。

先安装包:

<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />

然后加装你需要的 Sink:

<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Seq" Version="8.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.jsonSerilog节点下的配置;ReadFrom.Services允许 Serilog 从依赖注入容器里解析一些自定义服务和 Enricher,属于高级用法,但建议一开始就保留。

然后在 Controller 里直接注入ILogger<T>

[ApiController] [Route("api/orders")] public class OrdersController : ControllerBase { private readonly ILogger<OrdersController> _logger; public OrdersController(ILogger<OrdersController> logger) { _logger = logger; } [HttpPost] public IActionResult Create(OrderRequest request) { _logger.LogInformation("Creating order for user {UserId}", request.UserId); // 业务处理 return Ok(); } }

注意这里用的是 .NET 自带的ILogger<T>,不是 Serilog 的ILogger。因为有UseSerilog,DI 里的ILogger<T>底层输出已经被接管到 Serilog,所以业务代码里的日志同样会享受结构化输出、Enricher、Sink 等能力。

3.3 用 appsettings.json 管配置,别把配置写死在代码里

把日志配置写死在代码里,对于一个小工具没问题,但对真正要上线的服务并不友好——你没法在不重新发布的情况下调整日志级别、切换日志平台、处理敏感信息等。Serilog 官方提供了一个配置包Serilog.Settings.Configuration,可以直接从appsettings.json读取配置。

安装包:

<PackageReference Include="Serilog.Settings.Configuration" Version="9.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对应你代码里的参数名,比如pathrollingInterval,大小写要和你配置的 Sink API 参数匹配,这点经常有人踩坑。

3.4 关键参数怎么选:RollingInterval、RetainedFileCountLimit、Shared、Buffered

文件 Sink 的配置不只是路径和一个滚动间隔那么简单,这里有几个参数很关键。

RollingInterval

表示日志文件按什么周期滚动。可选值有InfiniteYearMonthDayHourMinute。我推荐生产环境至少用Day,如果日志量特别大,可以按Hour滚动。按天滚动的好处是一天一个文件,排查问题时可以直接去翻某一天的文件,清理也方便。

RetainedFileCountLimit

保留多少个历史日志文件。默认是 31,也就是大约一个月的日志。如果磁盘空间充足,可以设大一点;如果服务日志非常频繁,建议根据磁盘容量估算一下。公式很简单:

单文件大小 x 保留文件数 <= 磁盘可用空间

比如一天产生 2GB 日志,保留 7 天就是 14GB,你要确保磁盘有这个余量。

Shared

这个参数很多人没注意。在多进程同时写同一个日志文件时,如果不开shared: true,可能会遇到文件被占用、写入失败的问题。尤其是在 IIS 宿主环境或者多个 worker 进程的场景下,建议打开:

.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day, shared: true)

不过开启shared会有少量性能开销,单进程场景不需要开。

Buffered

buffered: true时,Serilog 会把日志先写入缓冲区,满了再刷到磁盘。这个参数能提升吞吐,但缺点是如果进程崩溃或断电,缓冲区里的日志会丢失。我自己的建议是:默认不要开 buffered。日志追求的是可靠,不是极限性能。真到并发高到必须开缓冲的时候,你应该优先考虑异步 sink 而不是文件缓冲。

另外提一句,如果你是容器环境,建议文件日志输出到 stdout 而不是文件,由容器引擎统一收集,这样避免容器内文件轮转和宿主机日志收集之间产生冲突。Serilog 的 Console Sink 写入 stdout,是最省心的选择。

4. 真正上线后才会踩到的坑(含自查清单)

这一节是全文的干货核心。我把自己在多个项目里真实踩过的坑,以及常见的排查经验整理出来,希望对你有用。

4.1 性能陷阱:MessageTemplate 里别做字符串插值

Serilog 的MessageTemplate是它的灵魂,但很多人没意识到,写日志时如果用了字符串插值,就等于绕过了 MessageTemplate 的结构化优势

反例:

Log.Information($"User {userId} created order {orderId}");

这样写,Serilog 收到的消息已经是一个完整字符串,userIdorderId不会被拆成独立字段,后续没法按字段检索,也失去了模板缓存的好处。

正例:

Log.Information("User {UserId} created order {OrderId}", userId, orderId);

这样写有几个好处:

  • 模板可缓存。相同模板只解析一次,性能更高。
  • 字段可检索UserIdOrderId是独立属性,日志平台可以直接索引。
  • 聚合分析方便。按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.With<SensitiveDataEnricher>()

另外,不要只依赖 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 默认是批量写入,但网络抖动时会有堆积。你需要关注BatchPostingLimitPeriod参数,以及失败后的重试机制。如果 ES 不可用,日志会积压在内存里,极端情况下可能造成 OOM。

我的建议是,如果团队没有专门的 ES 运维能力,优先用 Seq 或者云厂商的日志服务,省心很多。ES 的玩法很多,但坑也很多,需要投入人力才能维护好。

4.4 几个常见异常和排查速查表

我在实际使用中遇到过这些问题,整理成表格方便你自查:

现象可能原因解决方法
日志完全没有输出没配置MinimumLevel或 Sink检查 LoggerConfiguration 是否至少有WriteTo和一个有效级别
文件没有生成路径不存在或权限不足检查运行账号是否有写权限,路径目录是否存在
控制台中文乱码控制台代码页不是 UTF-8在 Program.cs 开头加Console.OutputEncoding = Encoding.UTF8;或在部署环境设置DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
日志文件不按天滚动文件名没加日期占位符路径必须包含-.log这样的日期占位符,如app-.log
Seq 收不到日志网络不通、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 这套方案值得你花一晚上试一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询