概述
本文档旨在说明如何配置 Serilog,以实现在运行时动态路由日志,并提供企业生产环境中关键参数的详细配置说明。
-
核心场景: 你拥有一个静态工具类(
LogUtil),你希望在调用它时传递一个"标签"(如功能名称featureName),Serilog 根据这个标签将该条日志写入到特定的文件中。 -
核心机制:
- 代码 (C#): 使用
Serilog.Context.LogContext将featureName动态"附加"到日志条目上。 - 配置 (JSON): 使用
Serilog.Filters.Expressions读取这个附加的属性,并配置Logger规则将日志路由到对应的FileSink。
- 代码 (C#): 使用
-
文档内容: 本文将详细分解
FileSink(用于文件定制、大小限制、保留策略)和ExpressionsFilter(用于路由)的配置参数。
1. 基础核心:ASP.NET Core 集成
-
功能说明:
让 Serilog 接管 .NET 默认的 ILogger,并使其能够从 appsettings.json 文件中读取所有配置。
-
所需模块包 (NuGet):
Serilog.AspNetCore(此包会自动包含Serilog核心库和Serilog.Settings.Configuration配置库)
-
配置实现 (C# - Program.cs):
这是启动 Serilog 的标准"模板代码",用于引导和注册 Serilog。
C#using Serilog; // 1. 在构建 WebApplicationBuilder 之前,配置一个临时的静态 Logger // 这能确保在 DI 容器构建失败或应用启动早期阶段的日志能被捕获 Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) // 尝试从配置中读取 .Enrich.FromLogContext() // (关键) 确保 LogContext 在引导时也可用 .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); // 2. (关键) 将 Serilog 注册到 .NET 主机 // 这将替换掉所有 .NET 默认的 LoggerProvider builder.Host.UseSerilog((context, services, configuration) => configuration .ReadFrom.Configuration(context.Configuration) // 读取 appsettings.json .ReadFrom.Services(services) // 从 DI 读取 .Enrich.FromLogContext()); // (关键) 确保 LogContext 能被 DI Logger 识别 // ... (添加你的 Services) ... var app = builder.Build(); // 3. (可选但推荐) 自动记录所有 HTTP 请求 // 详细配置参见本文档第 6 节 app.UseSerilogRequestLogging(); // ... (配置你的中间件) ... Log.Information("应用程序启动中..."); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "应用程序启动失败"); } finally { // 4. 确保在应用退出时,将缓冲区中的日志刷入文件 Log.CloseAndFlush(); }
2. 核心功能:动态上下文 (LogContext)
-
功能说明:
在代码中创建一个静态工具类 LogUtil,它能接收一个 featureName 字符串,并将其作为属性附加到**当前线程(或异步上下文)**的后续日志中。
-
所需模块包 (NuGet):
Serilog(已由Serilog.AspNetCore自动引入)
-
配置实现 (C# -
LogUtil.cs):C#using Serilog; using Serilog.Context; // 1. 必须引入 LogContext 命名空间 public static class LogUtil { // 你可以根据需要添加更多参数,如 LogEventLevel public static void Write(string featureName, string message, params object[] propertyValues) { // 2. (关键) PushProperty 会将 "FeatureName" 属性添加到 // 当前 LogContext 中。 // using 块确保了该属性只在块执行期间有效, // 并在退出时自动移除,防止"污染"后续日志。 using (LogContext.PushProperty("FeatureName", featureName)) { // 3. 使用全局静态 Logger 写入日志 (使用结构化日志) // 这条日志现在会自动携带 { FeatureName: "..." } 属性 Log.Information(message, propertyValues); } } } -
配置实现 (C# - Controller 调用示例):
C#[ApiController] public class OrdersController : ControllerBase { [HttpGet] public IActionResult GetOrder(int id) { // ... 业务逻辑 ... // 运行时动态指定 "Orders" 标签,并使用结构化日志 LogUtil.Write("Orders", "已完成对订单 {OrderId} 的查询。", id); return Ok(); } }
3. 核心功能:表达式过滤器 (Serilog.Filters.Expressions)
-
功能说明: 提供了强大的
SQL-Like语法来过滤日志,这是实现动态路由的核心。 -
所需模块包 (NuGet):
Serilog.Filters.Expressions -
Args (参数) 详细解析:
在 Filter 配置中,expression (表达式) 是关键。
JSON"Filter": [ { "Name": "ByIncludingOnly", // 或 ByExcluding (排除) "Args": { // 示例:只包含 FeatureName = 'Orders' 并且级别为 Warning 以上的日志 "expression": "FeatureName = 'Orders' and Level >= LogLevel.Warning" } } ] -
常用表达式函数和语法:
| 示例表达式 | 说明 |
|---|---|
FeatureName = 'Orders' |
(你的方案) 属性等于 'Orders' (注意:字符串用单引号)。 |
FeatureName is not null |
(你的方案) FeatureName 属性存在。 |
Level >= LogLevel.Warning |
过滤级别。可以是 Verbose, Debug, Information, Warning, Error, Fatal。 |
StartsWith(SourceContext, 'MyProject.Features') |
(方案 A) SourceContext 以...开头。 |
Contains(Message, 'timeout') |
日志消息体 (Message) 包含 "timeout" 字符串。 |
... or ... / ... and ... |
复杂的布尔逻辑。 |
StatusCode = 200 |
(用于 HTTP 请求日志) 过滤 HTTP 状态码。 |
Elapsed > 1000 |
(用于 HTTP 请求日志) 过滤耗时超过 1000ms 的请求。 |
4. 核心功能:文件日志 (Serilog.Sinks.File) - 详细参数
这是日志配置的核心,用于自定义文件名、大小限制和保留策略。
-
所需模块包 (NuGet):
Serilog.Sinks.File -
配置实现 (
appsettings.json):下面是一个"全功能"的文件日志配置示例,我们将在下面详细分解它的
Args。JSON{ "Serilog": { "WriteTo": [ { "Name": "File", "Args": { "path": "logs/app_logs/my-log-.log", "rollingInterval": "Day", "fileSizeLimitBytes": 104857600, "retainedFileCountLimit": 30, "shared": true, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}", "buffered": true, "flushToDiskInterval": "00:00:05", "restrictedToMinimumLevel": "Debug" } } ] } }
Args (参数) 详细解析:
a. 自定义文件名 (path) 和滚动 (rollingInterval)
-
path(路径和文件名模板):-
示例:
"logs/app_logs/my-log-.log" -
说明:
path决定了文件名和滚动方式。路径中最后一个破折号 (-) 是一个占位符。 -
rollingInterval会决定这个占位符被什么内容替换:"rollingInterval": "Day"(默认) ->my-log-20251116.log"rollingInterval": "Hour"->my-log-2025111619.log(19点)
-
无占位符: 如果路径是
"logs/my-log.log"(没有-),它将永远写入这一个文件,直到达到大小限制。
-
b. 文件大小限制 (fileSizeLimitBytes)
-
fileSizeLimitBytes(文件大小限制 - 字节):-
示例:
104857600(这是 100 MB) -
说明: 当一个日志文件(例如 my-log-20251116.log)达到这个大小时,Serilog 会自动创建一个新文件,并
在文件名后加上 _001, _002...
-
默认值:
null(无限制)。在生产环境中,强烈建议设置此值!
-
c. 文件保留策略 (retainedFileCountLimit)
-
retainedFileCountLimit(保留文件数限制):-
示例:
30 -
说明: Serilog 会自动清理旧的 日志文件,只保留最近的
N个文件。 -
重要: 这个"文件"是基于滚动策略的。
- 如果
rollingInterval是 "Day",30就意味着保留最近 30 天的日志。 - 如果
rollingInterval是 "Hour",30就意味着保留最近 30 小时的日志。
- 如果
-
默认值:
null(永久保留)。在生产环境中,强烈建议设置此值!
-
d. 性能与并发 (shared, buffered, flushToDiskInterval)
-
shared(共享访问):- 示例:
true - 说明: (解决你的"文件被占用"需求) 设置为
true时,Serilog 会在每次写入后释放文件锁,允许其他进程(和你)读取该文件。
- 示例:
-
buffered(缓冲区写入):- 示例:
true(默认值) - 说明: 为了性能,Serilog 会先将日志写入内存缓冲区,而不是每次都访问磁盘。
- 示例:
-
flushToDiskInterval(缓冲区刷盘间隔):- 示例:
"00:00:05"(5 秒) - 说明: 当
buffered为true时,此设置决定缓冲区多久被强制写入磁盘。5 秒是一个合理的折中,可在应用崩溃时最多只丢失 5 秒的日志。
- 示例:
e. 日志格式 (outputTemplate)
-
outputTemplate(输出模板):-
示例:
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}" -
说明: 控制日志在 .txt 文件中的外观。
-
常用标记:
{Timestamp:yyyy-MM-dd ...}: 时间戳(可自定义格式)。{Level:u3}: 日志级别(u3表示大写的3个字母,如INF,WRN,ERR)。{SourceContext}: 日志来源(通常是类名)。{FeatureName}: 你用LogContext添加的自定义属性。{Message:lj}: 你记录的消息体(lj表示换行和 JSON 字符串化)。{NewLine}: 换行符。{Exception}: 异常堆栈信息。
-
5. (可选) 远程日志 (Serilog.Sinks.Seq) - 生产环境
-
功能说明: 将日志发送到 Seq 服务器,用于集中查看和分析。
-
所需模块包 (NuGet):
Serilog.Sinks.Seq -
Args(参数) 详细解析:JSON{ "Name": "Seq", "Args": { "serverUrl": "http://my-seq-server:5341", "apiKey": "YOUR_API_KEY_IF_NEEDED", "restrictedToMinimumLevel": "Information", "bufferBaseFilename": "logs/seq_buffer/buffer" } }-
serverUrl: (必需) 你的 Seq 服务器地址。 -
restrictedToMinimumLevel: (推荐)- 说明: 仅针对此 Sink 的级别过滤。
- 示例:
Information。这意味着Debug级别的日志不会被发送 到 Seq(节省网络流量和存储),但如果你的本地文件 Sink 设置了Debug,它仍然会被写入本地文件。
-
bufferBaseFilename: (生产环境关键特性)- 说明: 持久化缓冲区 。如果你的应用无法连接到 Seq 服务器(网络闪断),Serilog 会将日志缓存到本地磁盘 (路径为
logs/seq_buffer/)。一旦连接恢复,它会自动将缓存的日志重新发送到 Seq,确保日志的高可用性。
- 说明: 持久化缓冲区 。如果你的应用无法连接到 Seq 服务器(网络闪断),Serilog 会将日志缓存到本地磁盘 (路径为
-
6. (可选) HTTP 请求日志 (UseSerilogRequestLogging) - C# 配置
-
功能说明: 自动记录所有进入 ASP.NET Core 的 HTTP 请求。
-
配置实现 (C# - Program.cs):
app.UseSerilogRequestLogging() 方法接受一个配置委托 (delegate),允许你进行精细控制。
C#// 在 Program.cs 中 app.Build() 之后 app.UseSerilogRequestLogging(options => { // 自定义日志级别 options.GetLevel = (httpContext, elapsed, ex) => { // 如果是 404 (Not Found),降级为 Information,避免噪音 if (httpContext.Response.StatusCode == 404) { return Serilog.Events.LogEventLevel.Information; } // 其他 4xx 错误 (如 401, 403) 仍为 Warning if (httpContext.Response.StatusCode >= 400 && httpContext.Response.StatusCode < 500) { return Serilog.Events.LogEventLevel.Warning; } // 5xx 错误或异常为 Error if (ex != null || httpContext.Response.StatusCode >= 500) { return Serilog.Events.LogEventLevel.Error; } // 默认情况 (如 2xx, 3xx) 为 Information return Serilog.Events.LogEventLevel.Information; }; // 为所有请求日志添加自定义属性 options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { // 从 HttpContext 中获取并添加 UserID var userId = httpContext.User.Identity?.Name; if (userId != null) { diagnosticContext.Set("UserId", userId); } // 添加一个相关 ID,用于追踪 diagnosticContext.Set("CorrelationId", httpContext.TraceIdentifier); }; });
7. 综合配置:完整的 appsettings.json 示例
这是一个将动态路由 与生产级参数相结合的完整示例:
JSON
{
"Serilog": {
"Using": [
"Serilog.Sinks.Console",
"Serilog.Sinks.File",
"Serilog.Filters.Expressions",
"Serilog.Sinks.Seq"
],
"MinimumLevel": { // 全局最小级别 (应用启动时)
"Default": "Debug",
"Override": {
"Microsoft": "Warning", // 过滤掉 .NET Core 的冗余日志
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"Enrich": [
"FromLogContext", // 必须,用于读取 FeatureName
"WithMachineName",
"WithThreadId"
],
"WriteTo": [
// --- (可选) DEV 环境输出到控制台 ---
{
"Name": "Console",
"Args": {
"restrictedToMinimumLevel": "Information"
}
},
// --- 路由规则 1: "Orders" 功能 ---
{
"Name": "Logger",
"Args": {
"configureLogger": {
"Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "FeatureName = 'Orders'" } } ],
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/features/orders-.log",
"rollingInterval": "Day",
"shared": true,
"fileSizeLimitBytes": 10485760, // 10MB
"retainedFileCountLimit": 7, // 保留 7 天
"outputTemplate": "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
},
// --- 路由规则 2: "Users" 功能 ---
{
"Name": "Logger",
"Args": {
"configureLogger": {
"Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "FeatureName = 'Users'" } } ],
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/features/users-.log",
"rollingInterval": "Day",
"shared": true,
"fileSizeLimitBytes": 10485760, // 10MB
"retainedFileCountLimit": 7 // 保留 7 天
}
}
]
}
}
},
// --- 路由规则 3: 兜底日志 (所有其他日志) ---
{
"Name": "Logger",
"Args": {
"configureLogger": {
"Filter": [ { "Name": "ByExcluding", "Args": { "expression": "FeatureName is not null" } } ],
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/application-all-.log",
"rollingInterval": "Day",
"shared": true,
"fileSizeLimitBytes": 104857600, // 100MB (兜底日志可以大一些)
"retainedFileCountLimit": 14, // 保留 14 天
"restrictedToMinimumLevel": "Information" // 兜底文件不记录 Debug
}
}
]
}
}
},
// --- 路由规则 4: PRD 环境发送到 Seq ---
{
"Name": "Seq",
"Args": {
"serverUrl": "http://my-seq-server:5341",
"restrictedToMinimumLevel": "Information", // 不发送 Debug 日志
"bufferBaseFilename": "logs/seq_buffer/buffer" // 启用高可用性缓存
}
}
]
}
}
8. 避坑建议、最佳实践与官方文档
⚠️ 避坑建议 (Potholes)
-
忘记 Enrich.FromLogContext():
这是最常见的错误。如果你在 appsettings.json 或 Program.cs 中忘记了 .Enrich.FromLogContext(),FeatureName 属性将永远为 null,导致过滤器 FeatureName = 'Orders' 匹配失败,所有日志都将进入"兜底文件"。
-
LogContext 的异步/线程问题:
LogContext.PushProperty 是基于 AsyncLocal 实现的。这意味着它在 await 之间是安全的。但你必须使用 using 块来确保属性被正确移除。否则,该属性可能会"泄露"到当前请求处理完毕后的其他日志中(例如,在线程池线程被重用时)。
-
"魔法字符串" (Magic Strings) 依赖:
你的代码 LogUtil.Write("Orders", ...) 和配置 "expression": "FeatureName = 'Orders'" 之间是硬编码的字符串依赖。如果有人在代码中改成了 "Order",路由就会失效。
✨ 最佳实践 (Best Practices)
-
用常量管理标签:
为了解决"魔法字符串"问题,在项目中创建一个静态常量类。
C#public static class LogFeatures { public const string Orders = "Orders"; public const string Users = "Users"; } // 调用时: LogUtil.Write(LogFeatures.Orders, "..."); // 配置中 ("appsettings.json" 无法使用常量,但至少代码侧是受控的) // "expression": "FeatureName = 'Orders'" -
混合使用 ILogger:
不要用 LogUtil 替换所有的日志记录。ILogger 仍然是最佳实践,因为它能自动附加 SourceContext(类名)。
- 建议: 在你的
Controller和Service内部优先使用ILogger<T>(通过 DI 注入)来进行常规的 Debug 和 Info 级别的日志记录。这些日志会自动进入"兜底文件" (all-.txt)。 - 仅在 你需要触发特定路由规则 时(例如 "Controller 执行完毕后"),才调用
LogUtil.Write(LogFeatures.Orders, ...)。
- 建议: 在你的
-
使用结构化日志:
始终使用结构化日志模板,而不是字符串拼接($)。
C#// 不好: Log.Information($"已完成对订单 {id} 的查询。"); // 推荐: Log.Information("已完成对订单 {OrderId} 的查询。", id);- 为什么? 推荐的方式会生成
{ "OrderId": 123 },而$字符串只会生成一个扁平的message,你无法在 Seq 等工具中按OrderId进行搜索。
- 为什么? 推荐的方式会生成
📚 官方文档推荐
- Serilog (官网): serilog.net/
- JSON 配置 (Configuration): github.com/serilog/ser... (查看
README中的所有配置语法) - 日志上下文 (LogContext): github.com/serilog/ser...
- 表达式过滤器 (Expressions Filter): github.com/serilog/ser... (查看
README了解所有可用函数,如Contains,StartsWith等) - 文件 Sink (File Sink): github.com/serilog/ser... (查看所有文件参数,如
retainedFileCountLimit等)