Serilog
适用范围:Serilog 4.x / Serilog.AspNetCore 8.x,.NET 8 及以上版本。
1. Serilog 是什么
Serilog 是 .NET 生态中最常用的日志框架之一。与传统"把日志拼成一行文本"的做法不同,Serilog 从设计之初就是结构化日志:消息模板中的占位符会作为独立字段完整保留,日志既可以渲染成人类可读的文本,也可以输出成 JSON 交给日志平台做精确查询和聚合。
它的核心组成非常简单:
LoggerConfiguration(配置)
├── MinimumLevel 最低输出级别
├── WriteTo 输出目标(Sink)
└── Enrich 附加字段(Enricher)
↓ CreateLogger()
Logger(日志器)
Serilog 可以在任何 .NET 应用中独立使用——控制台程序、Windows 服务、类库测试——并不依赖 ASP.NET Core。
2. 安装与第一条日志
在一个控制台项目中安装核心包和控制台 Sink:
dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
最小示例:
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
Log.Information("Hello, Serilog!");
Log.CloseAndFlush();
三个要点:
LoggerConfiguration负责描述"输出到哪里、输出什么级别",CreateLogger()生成日志器;Log.Logger是全局静态入口,赋值后就可以在任何地方调用Log.Information(...)等方法;- 程序退出前调用
Log.CloseAndFlush()——部分 Sink 有写入缓冲,不刷新可能丢失最后几条日志。
3. 日志级别
Serilog 定义了六个级别,严重程度从低到高:
| 级别 | 写法 | 典型用途 |
|---|---|---|
| Verbose | Log.Verbose(...) | 最细粒度的跟踪信息 |
| Debug | Log.Debug(...) | 开发调试信息、内部状态 |
| Information | Log.Information(...) | 正常且有价值的业务或系统事件 |
| Warning | Log.Warning(...) | 可恢复问题、异常趋势、降级行为 |
| Error | Log.Error(...) | 当前操作失败,应用仍可运行 |
| Fatal | Log.Fatal(...) | 导致应用终止的严重故障 |
通过 MinimumLevel 控制最低输出级别:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
未设置时默认为 Information。注意与 .NET 内置日志的级别名称差异:内置的 Trace 对应 Serilog 的 Verbose,内置的 Critical 对应 Fatal,其余同名。
按命名空间覆盖级别
MinimumLevel.Override 是按类别(Category)配置级别。类别通常就是写日志的类的完整名称(命名空间 + 类名),Override 按名称前缀匹配,因此可以为不同命名空间分别设置不同的日志级别:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", Serilog.Events.LogEventLevel.Error)
.MinimumLevel.Override("MyCompany.OrderApi", Serilog.Events.LogEventLevel.Debug)
.WriteTo.Console()
.CreateLogger();
上面的配置意味着:
Microsoft开头的框架日志只输出Warning及以上,压低框架噪音;- EF Core 的 SQL 命令日志(类别
Microsoft.EntityFrameworkCore.Database.Command)进一步收紧到Error; - 自己项目
MyCompany.OrderApi命名空间下的日志放开到Debug,便于定点排查; - 其他未匹配的类别使用全局的
Information。
多条 Override 规则中,前缀匹配越长的越具体,优先生效。典型用法就是"框架安静、自己模块详细":排查某个模块时,只放开它所在命名空间的级别,其余不受影响。
4. 消息模板与结构化日志
这是 Serilog 最核心的特性。变量放进 {Placeholder} 占 位符,而不是拼接字符串:
var orderId = 1001L;
var customerId = 42L;
// 推荐:占位符成为独立字段
Log.Information(
"Order {OrderId} created for customer {CustomerId}",
orderId,
customerId);
// 不推荐:字符串插值丢失字段语义
Log.Information($"Order {orderId} created for customer {customerId}");
第一种写法中,OrderId=1001、CustomerId=42 会作为字段随日志一起保存。输出到 JSON 时形如:
{
"@t": "2026-09-04T08:30:00.000Z",
"@mt": "Order {OrderId} created for customer {CustomerId}",
"OrderId": 1001,
"CustomerId": 42
}
日志平台可以直接执行"查出 OrderId = 1001 的全部日志"这类精确查询,这是纯文本日志做不到的。
记录复杂对象时,在占位符前加 @ 表示按结构序列化:
var position = new { X = 12.5, Y = 8.2, Theta = 90 };
Log.Information("Robot arrived at {@Position}", position);
// 输出: Robot arrived at {"X": 12.5, "Y": 8.2, "Theta": 90}
不加 @ 时对象只会被 ToString()。记录异常则使用专门的重载,把异常对象作为第一个参数:
catch (Exception exception)
{
Log.Error(exception, "Failed to submit order {OrderId}", orderId);
throw;
}
这样异常类型、消息和堆栈会被完整保留,而不是混进消息文本。
5. Sink:日志输出到哪里
Sink 是 Serilog 的输出目标,每种 Sink 是一个独立的 NuGet 包,可以同时配置多个——一条日志会被送到所有满足级别要求的 Sink。
| Sink 包 | 输出目标 | 典型场景 |
|---|---|---|
Serilog.Sinks.Console | 控制台 | 本地开发、容器日志采集 |
Serilog.Sinks.File | 文件(支持滚动) | 服务器落盘、无日志平台的部署 |
Serilog.Sinks.Seq | Seq 日志服务器 | 团队集中查询结构化日志 |
Serilog.Sinks.Elasticsearch | Elasticsearch | 接入 ELK 体系 |
Serilog.Sinks.MSSqlServer | SQL Server | 日志入库 |
滚动文件
文件 Sink 是引入 Serilog 最常见的理由——.NET 内置日志没有滚动文件能力:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File(
"logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 100 * 1024 * 1024,
rollOnFileSizeLimit: true)
.CreateLogger();
rollingInterval: RollingInterval.Day:按天滚动,生成app-20260904.log;retainedFileCountLimit: 30:只保留最近 30 个文件,旧文件自动删除;fileSizeLimitBytes+rollOnFileSizeLimit:单个文件超过限制时切分新文件。
每个 Sink 可以有独立级别
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File(
"logs/error-.log",
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning,
rollingInterval: RollingInterval.Day)
.CreateLogger();
控制台输出全部 Debug 及以上日志,错误文件只收 Warning 及以上——排查时看控制台,巡检时只翻错误文件。
6. Enricher 与 LogContext:给日志附加字段
Enricher 自动给每条日志加上字段,省去在每个调用点重复传参。最常用的是 FromLogContext,它配合 LogContext.PushProperty 给一段代码范围内的所有日志统一打标:
using Serilog.Context;
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate:
"[{Level:u3}] {TaskId} {Message:lj}{NewLine}")
.CreateLogger();
using (LogContext.PushProperty("TaskId", "T20260904001"))
{
Log.Information("Task started");
DoStepOne(); // 内部写的日志同样带 TaskId
DoStepTwo();
Log.Information("Task finished");
}
using 范围内(包括同一异步调用链上的深层方法)写出的每条日志都带有 TaskId 字段。任务号、请求号、租户号这类贯穿整个调用链的标识,用这种方式比在每条日志里手写占位符可靠得多。
机器名、线程号等现成 Enricher 需要额外安装包,例如 Serilog.Enrichers.Environment 提供 Enrich.WithMachineName(),Serilog.Enrichers.Thread 提供 Enrich.WithThreadId()。
7. 与 ASP.NET Core 集成
前面章节里 Serilog 都是独立使用的。在 ASP.NET Core 中,正确的姿势不是在业务代码里调用 Log.Information,而是把 Serilog 注册为日志提供程序:业务代码继续注入 ILogger<T>(写法见 ASP.NET Core 日志介绍),Serilog 只接管输出端。
安装集成包(它已带上 Console、File Sink 和 JSON 配置支持):
dotnet add package Serilog.AspNetCore
7.1 注册为日志提供程序
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSerilog(configuration => configuration
.ReadFrom.Configuration(builder.Configuration));
AddSerilog 会移除默认提供程序。之后所有通过 ILogger<T> 写出的日志——包括 ASP.NET Core 框架自身的——都由 Serilog 输出;ILogger<T> 的泛型类别会成为 Serilog 的 SourceContext 字段,MinimumLevel.Override 就按它匹配。
7.2 用 appsettings.json 配置
ReadFrom.Configuration 从配置的 Serilog 节读取,代码中的 MinimumLevel、WriteTo、Enrich 都有对应的 JSON 写法:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 30
}
}
],
"Enrich": [ "FromLogContext" ]
}
}
注意:接入 Serilog 后,内置的 Logging 配置节不再生效,级别过滤应统一写在 Serilog:MinimumLevel 下。按环境覆盖的方式不变——在 appsettings.Development.json 中把 Default 改成 Debug 即可。
7.3 请求日志中间件
框架默认为一次 HTTP 请求写出多条 Information 日志。Serilog 的请求日志中间件把它们压缩成一条结构化摘要:
var app = builder.Build();
app.UseSerilogRequestLogging();
输出形如:
[INF] HTTP GET /api/wms/stocks responded 200 in 12.3410 ms
RequestMethod、RequestPath、StatusCode、Elapsed 均为独立字段。配合上面把 Microsoft.AspNetCore 覆盖为 Warning,就得到"框架噪音安静、每个请求一条摘要"的干净输出。
7.4 推荐的完整 Program.cs
生产项目推荐"两阶段初始化":先建引导日志器捕获启动早期的错误,宿主构建完成后再切换到正式配置,退出时刷新缓冲:
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
Log.Information("Starting web application");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSerilog((services, configuration) => configuration
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext());
var app = builder.Build();
app.UseSerilogRequestLogging();
app.MapControllers();
app.Run();
}
catch (Exception exception)
{
Log.Fatal(exception, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
这个模板解决了三个实际问题:
- 启动失败也有日志:配置文件写错、端口被占用等发生在正式日志器就绪之前的异常,由引导日志器输出到控制台;
- DI 服务可参与配置:
ReadFrom.Services(services)允许从容器解析 Enricher 等组件; - 日志不丢:
CloseAndFlush确保进程退出前缓冲全部落盘。
Log.Logger 静态入口只出现在 Program.cs 的启动/关闭代码中;业务类一律注入 ILogger<T>,既保留类别过滤能力,也不与具体日志框架耦合。