干后端这些年,我对日志的理解经历了一次彻底“翻篇”,转折点就是在一套真实项目里认真用起了 Serilog。以前排查问题靠记事本打开几十MB文本再Ctrl+F,因为多行异常日志还经常把上下文冲散;后来切到结构化日志,按一个业务字段就能把整条调用链从入口一路拉到数据库层,那种舒畅感是传统文本日志给不了的。这篇内容不打算只讲理论,我想把 Serilog 的认知模型、在 .NET 工程里的接法,以及 WriteTo.File 这类日常落地细节一次性讲透。适合刚接触 Serilog 的 .NET 后端开发,也适合正在维护老项目、准备把日志体系升级成结构化方案的团队参考。
1. 结构化日志到底把“日志”变成了什么
1.1 字符串拼接那套,为什么会害了你
最早写日志,很多团队的习惯是 string.Format 或者 $"用户{userId}下单成功",然后整行字符串丢给 NLog、Log4Net 写到文本文件。早期这确实够用,毕竟系统小、请求量低,出问题翻日志也就几屏的事。可当流量上来、微服务拆分之后,这套做法开始暴露三个非常现实的问题。
第一个问题是没法按字段查。你只能依赖“关键词匹配”,但如果用户ID、订单号被拼接成了长字符串,你想查某个订单的全部日志,就得用订单号去全文搜索,还要忍受异常堆栈撑爆一行后出现的断行误匹配。第二个问题是上下文容易丢。一次请求经过网关、业务服务、数据库访问层,每层都往文件里追加文本,线程一交错,看起来就是一片乱码,想还原调用关系困难。第三个问题是日志缺少“类型”。你很难一眼分清某一行到底是错误、警告,还是普通业务信息,程序里虽然打了 Level,可在文本文件里它只是字符串前缀。
如果只是个人小项目,忍忍也就过去了。但任何一个需要多人维护、需要线上快速定位的系统,日志就不是“能记下来就行”这么简单。它应该是一等公民,是系统运行状态的结构化记录,而不仅仅是给人看的纯文本流水。
1.2 LogEvent才是Serilog眼里的日志
Serilog 的核心模型其实不是“日志字符串”,而是 LogEvent。你可以把一次日志记录想象成一个带有类型的数据包,里面包括时间戳、日志级别、消息模板、异常对象,以及一组键值对属性。
比如下面这行:
csharp复制Log.Information("用户 {UserId} 登录成功,来源IP {ClientIp}", userId, clientIp);
Serilog 不会把它简单地拼成 用户 10001 登录成功,来源IP 192.168.1.1 这样一个大字符串,而是记录成类似下面的结构:
Timestamp: 2025-04-12T10:24:31.235ZLevel: InformationMessageTemplate: 用户 {UserId} 登录成功,来源IPProperties.UserId= 10001Properties.ClientIp= 192.168.1.1
看到区别了吗?UserId、ClientIp 成了独立的结构化属性。日志到了 Seq、Elasticsearch、日志服务里,就能按属性过滤、聚合、做监控告警。就算只写本地 JSON 文件,也可以用 jq 之类的工具直接筛出某个 UserId 的全部日志。
这正是结构化日志的价值:它把“看日志”变成了“查数据”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先弄清Serilog的四个元件,再动手配置
2.1 四个角色:Logger、Sink、Enricher、Filter
Serilog 的架构非常清晰,你只需要理解四个角色:
Logger 是入口,你在代码里调用 Log.Information 或 ILogger<T>.LogInformation 时,就是在创建 LogEvent。LoggerConfiguration 负责组装这个 Logger,告诉它日志写到哪儿、怎么增强、是否需要过滤。
Sink 是输出端,负责把 LogEvent 写到某个目的地。控制台、文件、Seq、Elasticsearch、云日志服务都有对应的 Sink。一个 Logger 可以同时接多个 Sink,也就是一份日志既写文件又发到日志中心。
Enricher 是属性增强器。它可以在 LogEvent 进入 Sink 之前,给属性集合追加一些字段,比如机器名、进程ID、线程ID、请求路径。你不需要在每个业务类里手工塞这些信息,启动时统一挂上即可。
Filter 是过滤器。和日志级别的不同之处在于,它可以按任意属性决定哪些事件放行,比如只保留某个服务的错误日志,或者过滤掉健康检查接口的噪音。
理解这四个角色之后,再去看复杂配置就不会晕了。
2.2 用最小配置跑通第一条结构化日志
先来一个最简模型,感受一下:
bash复制dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
csharp复制using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.CreateLogger();
Log.Information("Hello, {Name}!", "Serilog");
Log.CloseAndFlush();
这段代码做了三件事:创建一个 Logger、把 Information 级别以上事件输出到控制台、打完日志后清理并关闭。其中 {Name} 就是消息模板占位符,控制台格式化时看起来是普通的字符串,但内部已经保留了属性结构。
如果你希望控制台输出变成追查友好的 JSON 结构,可以加一个参数:
csharp复制.WriteTo.Console(new RenderedCompactJsonFormatter())
这样控制台每行输出就会是紧凑 JSON。很多容器环境就靠这种方式把日志通过标准输出交给采集器,再集中到日志平台。
2.3 选Sink不是越多越好
Sink 的选择听起来简单,实际项目里却常常翻车。我见过一个团队一口气接了 Console、File、数据库、日志服务四个 Sink,结果业务日志量一大,数据库直接被打满,应用响应也跟着变慢。日志链路必须考虑自身的成本。
在 .NET 项目里,常见的组合是:本地开发用 Console,服务器环境要么写文件由采集器收集,要么直接 Console 输出标准输出交给容器运行时。核心审计日志单独走独立的 Sink,比如专门的日志文件或队列,避免和普通业务日志混在一起。WriteTo.File 写入本地文件是最直观的方式,本章后面会展开聊;日志量较大且团队有查询诉求时,再考虑 Seq 或 Elasticsearch 这类集中日志服务。
3. 在真实.NET工程里接入Serilog
3.1 包安装、日志自动收编
真实工程不会在 Main 里到处手动写 Log.Information,而是会把自己的日志体系嵌入到 .NET 的通用日志抽象中。.NET 生态里的 ILogger<T> 本来就是为这种切换设计的,接入 Serilog 后,框架日志、EF Core 日志、第三方库日志都能统一走 Serilog 管道。
新建 ASP.NET Core 项目时,推荐直接安装:
bash复制dotnet add package Serilog.AspNetCore
这个包会带上一组常用配置和 ASP.NET Core 集成扩展。然后在 Program.cs 里做三件事:
csharp复制using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
Log.Information("应用启动中");
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, loggerConfig) =>
{
loggerConfig.ReadFrom.Configuration(ctx.Configuration);
});
var app = builder.Build();
app.UseSerilogRequestLogging();
app.MapGet("/", () => "Hello World");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "应用启动失败,进程退出");
}
finally
{
Log.CloseAndFlush();
}
这里的 CreateBootstrapLogger 是专门给“启动阶段”用的。因为在 builder.Host.UseSerilog 执行之前,host 的配置还没加载完,如果启动期间就抛了异常,你得有个能输出的 Logger 把异常记下来。这也是很多人忽略的一个细节,结果应用启动失败时,配置在 appsettings.json 里的日志规则根本来不及生效,现场一团黑。
UseSerilog 会替换掉默认的 ILoggerFactory,此后你在 Controller、Service 里通过构造函数注入的 ILogger<T>,内部其实都走 Serilog。对业务代码来说,几乎是无侵入式接入。
3.2 给框架日志“刹刹车”
接完 Serilog 后,很多人第一次运行会吓一跳:控制台和日志文件里冒出海量 Microsoft.AspNetCore、Microsoft.EntityFrameworkCore 的日志,全是框架内部细节,有用的业务日志反而被淹没了。
这是正常的,.NET 默认的日志管道会把框架内部事件也吐出来。解决方式是在配置里做级别覆盖,appsettings.json 里的 Serilog 配置段可以这样写:
json复制{
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": "30",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}"
}
}
]
}
}
注意几个容易被忽略的点:Microsoft 覆盖为 Warning 后,会过滤掉绝大多数框架内部的 Information 日志;但 Microsoft.Hosting.Lifetime 保留了 Information,因为应用启动/停止这类生命周期日志还是值得留的。System 按需要覆盖,如果某个库的内部日志还吵,可以单独把对应命名空间压下去。
3.3 用结构化模板记录业务上下文
接入后真正关键的,是业务代码里怎么写日志。你要养成一个和以前完全不同的习惯:永远用“模板 + 参数”的方式记录,而不是字符串插值。
csharp复制// 推荐:参数独立,后续可按属性检索
_logger.LogInformation(
"用户 {UserId} 尝试支付订单 {OrderId},支付渠道 {Channel}",
userId, orderId, channel);
// 不推荐:日志变纯文本,结构化属性全部丢失
_logger.LogInformation($"用户 {userId} 尝试支付订单 {orderId},支付渠道 {channel}");
第二种写法执行时,消息模板其实就是拼好的长字符串,Serilog 拿不到 UserId 所对应的属性。虽然文本看起来和第一种一样,但它们的可查询性天差地别。第一种写法到了日志平台,你能直接筛 UserId = 123,第二种只能全文搜索。
如果想把整个对象结构化展开,可以用解构符 @:
csharp复制_logger.LogInformation("支付结果: {@PaymentResult}", paymentResult);
否则 Serilog 默认会对复杂对象调用 ToString(),输出出来可能只是一行类型名。这是新手经常会踩的坑:日志里明明传了对象,最后却看不到对象内容。
4. WriteTo.File到底怎么写:一个文件名引发的血案
4.1 文件名格式的规范和示例
很多朋友搜到 Serilog 后,会卡在 WriteTo.File 的文件名格式上,因为网上代码千奇百怪,什么写法都有。这里直接给结论:Serilog 文件滚动命名靠的是路径里的连字符(-)占位符,不是花括号占位符。
当你这样配置时:
csharp复制.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
Serilog 会把文件名中扩展名前的连字符替换成当前日期,实际生成的文件名是:
text复制logs/app-20250412.log
logs/app-20250413.log
很多人的困惑来自这句话:名字里写的是“app-.log”,怎么生成的却变成了“app-20250412.log”?因为 RollingInterval.Day 决定了 Serilog 在 - 的位置填上“当天日期”格式。不同的 rollingInterval 会生成不同粒度的文件名:
| rollingInterval | 日期粒度 | 文件名示例 |
|---|---|---|
Infinite |
不按时间滚动 | app.log |
Year |
按年 | app-2025.log |
Month |
按月 | app-202504.log |
Day |
按天 | app-20250412.log |
Hour |
按小时 | app-2025041214.log |
所以如果你的代码里写的是 logs/app.log,同时又设置了 rollingInterval: RollingInterval.Day,那 Serilog 照样还是每天切一个文件吗?答案是不切,因为文件名里没有连字符占位符,Serilog 会一直往同一个 app.log 里写,文件会无限增长。这是文件日志最经典的误配置。
那就有人会问了:日期格式能自定义吗?比如想生成 app-2025-04-12.log。目前的官方 File Sink 没有暴露日期格式参数,文件名中的日期由 rollingInterval 决定,格式是紧凑的 yyyyMMdd,不是 yyyy-MM-dd。如果你执着于用中间带横杠的日期做文件名,就绕开了 Serilog 默认滚动机制,我并不推荐。因为日志平台的采集规则通常按目录或文件名前缀做正则,日期里多出来的横杠反而容易把匹配规则搞复杂。日志内容里已经有完整时间戳,文件名的颗粒度够用就行。
4.2 滚动、保留、大小、共享的完整配置
文件日志的生产级配置,一般要同时考虑时间滚动、大小滚动、保留数量、路径和模板。下面是一份我常用在普通 Web 服务上的完整配置:
csharp复制Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.File(
path: "logs/app-.log",
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true,
fileSizeLimitBytes: 200 * 1024 * 1024,
retainedFileCountLimit: 30,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}",
shared: false)
.CreateLogger();
逐个参数说清楚:
path 是日志文件路径,可带目录。Serilog 通常会在首次写入时创建缺失目录,但如果目录权限不对也会失败,文中后面会提到排查方法。
rollingInterval 是时间切分维度。每天一切适合大多数系统,如果你的日志量一天能到几 GB,可以用 Hour,把文件切小块,方便采集和清理。
rollOnFileSizeLimit 和 fileSizeLimitBytes 配合使用,意思是即使还没到午夜,单文件超过指定字节数也会触发滚动。200 * 1024 * 1024 即 200MB。如果 fileSizeLimitBytes 不设置,Serilog 默认限制在 1GB 左右。当同时设置按大小滚动但 rollingInterval 是 Day 时,当天如果多滚几次,Serilog 会生成类似 app-20250412_001.log、app-20250412_002.log 这样的文件。
retainedFileCountLimit 是保留文件数量。我这里设 30,配合 Day 滚动就是大约保存最近一个月。注意这是文件总数上限,不是“天数”上限,因为同一天可能因为大小滚动产生多个文件。数值过大容易把磁盘堆满,过小又留不下足够排查周期的数据,一般结合你的磁盘空间和日志保留要求来定。
outputTemplate 是文本输出格式。每次日志事件会按这个模板渲染成一行文本。{Message:lj} 里的 l 表示消息中的换行符要转义,避免多行日志把文件切碎;{Exception} 会自动追加完整异常堆栈。如果你希望文件里也存 JSON 结构,可以把 .WriteTo.File 的参数改成:
csharp复制.WriteTo.File(
path: "logs/app-.log",
rollingInterval: RollingInterval.Day,
shared: false)
上面这个写法默认不是 JSON,想要 JSON 需要另外加 formatter,比如 new CompactJsonFormatter() 或 new RenderedCompactJsonFormatter()。JSON 文件更利于采集器解析,但可读性差,很难直接用记事本看。如果你生产环境是“本地文件 + 采集器”的模式,建议直接输出 JSON。
shared 表示是否允许多个进程共享同一个文件。如果只有单个应用进程写日志,保持 false 就行。如果是多个进程实例写同一路径,把 shared 设成 true 后,Serilog 会以共享读写的方式打开文件,但它只能保证多个进程不至于把文件句柄互相挤掉,并不保证日志顺序和内容不交错。多实例部署时更推荐的做法是每个实例写到不同前缀的文件,比如 app-{实例ID}-.log,再由采集器统一收走。
4.3 别忽略 flush 与缓冲区问题
File Sink 底层用 StreamWriter 做缓冲,不是每条日志都立刻 fsync 到磁盘。对绝大多数场景这是好事,因为日志 IO 如果同步刷盘,极端情况下会成为性能瓶颈。但你得知道一个代价:如果进程被强杀或机器断电,最后几百字节到几 KB 日志可能会丢。对 MySQL Binlog、支付流水那种强一致审计不太合适,对普通应用的运行追踪问题不大。
如果你希望折中,可以让 Serilog 定时把缓冲刷到磁盘:
csharp复制.WriteTo.File(
path: "logs/app-.log",
rollingInterval: RollingInterval.Day,
flushToDiskInterval: TimeSpan.FromSeconds(1))
意思是每秒把积累的日志刷一次盘,崩溃时最多丢不到一秒的数据。这个参数对应用性能影响很小,生产里我一般会开。如果你的日志量极大,或者主线程容易被慢磁盘拖慢,可以给 File Sink 包一层异步:
csharp复制.WriteTo.Async(a => a.File("logs/app-.log", rollingInterval: RollingInterval.Day))
注意 WriteTo.Async 扩展方法来自 Serilog.Sinks.Async 包,需要单独安装。
bash复制dotnet add package Serilog.Sinks.Async
异步 Sink 最大好处是把日志写入放到后台线程,不阻塞业务线程,代价是进程异常退出时可能丢失一小段排队中的日志。核心账务类需求别这么干,日志链路也得按可靠性分级设计。
5. 文件日志使用中的高频坑与排查手段
5.1 文件不生成、不滚动、写不进,从头查一遍
这是我被问过很多次的问题“日志文件怎么不生成”。排查路径其实很固定,按顺序查。
先看 path 是否有目录分隔符。logs/app-.log 的 logs 是相对路径,程序当前工作目录如果在 Docker 容器里可能不是你想象的位置。容器启动时工作目录通常是 /app 或镜像里指定的目录,所以在宿主机上找不到 logs 很常见。建议把路径调整成明确的绝对路径,或者干脆让容器把日志输出到 Console,由容器运行时接管。
再看目录权限。Windows 服务、IIS 应用池、Linux 下以 www-data 运行的进程,如果对目标目录没有写权限,Serilog 可能静默失败或启动时抛异常。先用同身份手动在目标目录创建文件,能排除权限问题。
接着看日志级别。如果 MinimumLevel 是 Warning,你调用的 _logger.LogInformation 是不会产生事件的。Override 可能把命名空间级别的级别提得很高,也会导致控制台/文件里什么都没有。可以先临时把全局级别调到 Verbose 测试,确认通了再调回去。
至于不滚动,原因九成是文件名里没写连字符占位符。logs/app.log 永远是单文件,logs/app-.log 才会按 rollingInterval 滚动。我把这个问题放在第 4 节用一整节篇幅去讲,就是因为它极隐蔽且高发。
5.2 日志重复、乱码、多实例写入的数据撕裂
有一个现象是日志打了两份:一份是默认的控制台文本,一份是你自己配置的文件格式。这通常是因为程序里既没有清掉默认日志提供程序,又额外配置了 Serilog。实际上 UseSerilog 会替换默认 LoggerFactory,一般不会重复;但如果你的代码里同时手动创建了 new LoggerConfiguration() 的和通过 DI 获得的 Logger,那两套管道就会各写一份,排查时先查是否存在多处 Logger 入口。
中文乱码最常出现在 Windows 上。文件本身的编码一般是 UTF-8,问题通常出在读取端。用 PowerShell 的 Get-Content 看文件时如果用了系统默认 ANSI 编码,中文就变成乱码。建议用 VS Code 或专门工具打开,并确认文件编码为 UTF-8。如果确实需要 GBK 输出,File Sink 也允许指定编码:
csharp复制.WriteTo.File(
path: "logs/app-.log",
encoding: System.Text.Encoding.GetEncoding("GB2312"))
但除非有上游系统强制要求,否则我劝你统一 UTF-8,省掉后续所有乱码烦恼。
多进程写同一个文件是更隐蔽的坑。Kestrel 单实例时没问题,但如果同一台机器上部署了多个进程实例,并且都指向同一个日志文件,即便 shared: true,也可能会出现日志行被切断、属性串到别的请求里。我处理过一个现场:两台实例同时写一个共享网络盘,日志里同一行混着两个不同 RequestId 的内容,排查问题的时候直接误导了方向。这类场景别硬靠 Serilog 解决,正确做法是让每个实例写各自文件,或者通过网络发送到集中日志服务。Serilog 擅长的是应用内写入,跨进程、跨机器的日志聚合必须有专门的采集链路上。
6. 从“日志能写出来”到“日志能当资产”
6.1 集中式的出口要做对
文件日志不是终点。等系统拆成多个服务后,每个服务各写各的文件,排查一个跨服务请求时,你还是得一台台机器跳上去翻日志,这又回到了没有结构化日志的老路。
这里要形成一条链路:应用进程生成结构化日志,输出到文件或标准输出,然后由采集器(比如 Promtail、Filebeat、Fluent Bit 这类组件)收集,汇入 Elasticsearch、ClickHouse 或者云厂商日志服务,再通过 Kibana 这类平台统一查询。关键点是:无论你用哪一套平台,Serilog 这端要做的是把属性保留完整、输出格式规范,给下游创造便利。
以“落本地文件”为例,如果采集器支持解析 JSON,建议 File Sink 直接输出压缩 JSON 格式;如果采集器只能一行行读文本,那 outputTemplate 就必须保持一行一条日志,消息中的换行由 {Message:lj} 转移到成转义序列。很多采集器把多行文本合并成一条很麻烦,所以不要图日志文件“人类可读”而放弃单行结构。
容器环境里做法简单很多:Console Sink 把结构化日志写到标准输出,容器平台自己会收。你只要确保输出不会因为跨行破坏单条日志结构即可,{Message:lj} 在控制台模板里同样重要。
6.2 性能优化、去敏和团队规范的平衡
日志加得越多,系统越慢?这个说法一半对一半错。日志性能损耗主要来自三块:字符串渲染、IO 写入、序列化开销。Serilog 本身对消息模板有缓存,同一种模板只解析一次,比 string.Format 拼字符串要高效。真正拖慢应用的往往是文件 Sink 同步写入和大对象序列化,这两块分别可以用异步包装和避免过度解构来优化。
关于解构,还是要克制。{@Object} 确实能让你在日志平台看到对象全貌,但如果一个对象里有嵌套多层的大集合,序列化成本很高,日志平台存储也会飙涨。常见做法是只记录业务的必要字段,或者定义专门的日志 DTO。一个订单对象几百个字段,全打出来绝大多数时候没人会看,花钱存的都是垃圾。
日志安全这件事很多人会忽略。Serilog 在记录 HTTP 请求时,默认不会把 Header、Query、Body 自动全量打出来,这其实是好事。但你自己的业务日志里有可能会把手机号、身份证、Token、支付接口响应原样传给 LogInformation,一旦进了采集链路,等于把敏感数据永久留在日志系统里。比较稳妥的方式是:能不打就不打;非要保留痕迹,把手机号、卡号做脱敏处理再记录。有人说日志平台内部才可见,问题是日志平台通常不止一个人能访问,而且第三方服务一旦被拖库,日志里的 PII 就是顺带泄露的资产。这不是技术问题,是安全底线问题。
另一个建议是给业务日志的“级别”立规矩。不要把什么都打成 Information,更不要为了排查方便临时把日志全都升到 Verbose。我给团队的习惯是:Information 记录关键业务节点,Debug 记录可复现的调试上下文,Warning 记录预期内的异常和重试,Error 记录导致当前操作失败的异常,Fatal 只留给整个应用无法继续运行的灾难场景。级别混乱的日志系统,查问题的时候等于每个文件都在“狼来了”,没人愿意信。
有一个我在生产里实际用过的动态调级手段也顺便提一下:如果你使用 LoggingLevelSwitch,可以做到运行时不重启应用就调整全局日志级别。
csharp复制var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Information);
builder.Host.UseSerilog((ctx, loggerConfig) =>
{
loggerConfig
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day);
});
// 通过某个内部接口或控制台工具修改
levelSwitch.MinimumLevel = LogEventLevel.Debug;
线上偶发问题最难复现时,把它临时调到 Debug 跑几分钟,拿到现场日志后再调回 Information,成本极低但效果极好。不过切记别把这种“后门接口”暴露在公网,内部权限校验是前提。
我个人的体会是:日志是软件工程里听起来最不起眼、但做扎实后回报最明显的基础设施之一。Serilog 只是把“结构化”的入口摆到了你面前,真要让日志变成可查询、可告警、可追溯的资产,还取决于你代码里的记录习惯、输出链路的设计和运维侧的采集配合。这篇文章如果只能留一句话,那就是:从今天起,用模板和结构化属性写日志,哪怕你还没接日志平台,等接的那一刻你会回来感谢当初这个决定的。
