跳到主要内容

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 定义了六个级别,严重程度从低到高:

级别写法典型用途
VerboseLog.Verbose(...)最细粒度的跟踪信息
DebugLog.Debug(...)开发调试信息、内部状态
InformationLog.Information(...)正常且有价值的业务或系统事件
WarningLog.Warning(...)可恢复问题、异常趋势、降级行为
ErrorLog.Error(...)当前操作失败,应用仍可运行
FatalLog.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=1001CustomerId=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.SeqSeq 日志服务器团队集中查询结构化日志
Serilog.Sinks.ElasticsearchElasticsearch接入 ELK 体系
Serilog.Sinks.MSSqlServerSQL 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 节读取,代码中的 MinimumLevelWriteToEnrich 都有对应的 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

RequestMethodRequestPathStatusCodeElapsed 均为独立字段。配合上面把 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();
}

这个模板解决了三个实际问题:

  1. 启动失败也有日志:配置文件写错、端口被占用等发生在正式日志器就绪之前的异常,由引导日志器输出到控制台;
  2. DI 服务可参与配置ReadFrom.Services(services) 允许从容器解析 Enricher 等组件;
  3. 日志不丢CloseAndFlush 确保进程退出前缓冲全部落盘。

Log.Logger 静态入口只出现在 Program.cs 的启动/关闭代码中;业务类一律注入 ILogger<T>,既保留类别过滤能力,也不与具体日志框架耦合。

参考资料