关于 serilog
面向 .NET 开发的 Serilog 使用指南,介绍日志配置与实践模式,帮助开发者构建结构化日志、记录启动和请求事件,并规避常见问题。
主要功能
- 01两阶段初始化日志记录器
- 02使用结构化消息模板
- 03通过 appsettings.json 配置
- 04请求日志中间件
- 05LogContext 作用域属性
- 06导出事件到 OTLP
适用场景
- 01捕获应用启动期间的错误
- 02按命名属性查询日志
- 03汇总 ASP.NET Core 请求
- 04将事件导出到 OTLP 后端
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill serilog安装后在 Claude Code 中输入/serilog确认已加载。
详细介绍
Serilog
核心原则
- 两阶段初始化 — 为启动阶段创建引导日志记录器,然后在 DI 就绪后将其替换为完整的日志记录器。这样可以捕获否则会丢失的启动错误。
- 优先使用
AddSerilog(),而非UseSerilog()— 使用builder.Services.AddSerilog()(现代 API),而不是builder.Host.UseSerilog()。它通过ReadFrom.Services(services)集成 DI 服务。 - 使用消息模板,而非字符串插值 —
{PropertyName}语法会创建可供查询的结构化数据。字符串插值($"...")会破坏结构,并且即使日志级别已禁用也会分配内存。 - 通过 appsettings.json 配置 — 将日志级别、接收器和覆盖项保存在配置中,以便按环境更改,而无需重新部署。
模式
两阶段引导设置
using Serilog;
// Stage 1: Bootstrap logger — captures startup errors before DI
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
Log.Information("Starting application");
var builder = WebApplication.CreateBuilder(args);
// Stage 2: Full logger with DI and configuration
builder.Services.AddSerilog((services, lc) => lc
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "MyApp.Api"));
var app = builder.Build();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
diagnosticContext.Set("UserAgent",
httpContext.Request.Headers.UserAgent.ToString());
};
});
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
await Log.CloseAndFlushAsync();
}
appsettings.json 配置
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/app-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 30,
"fileSizeLimitBytes": 104857600
}
},
{
"Name": "Seq",
"Args": { "serverUrl": "http://localhost:5341" }
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithEnvironmentName"],
"Destructure": [
{ "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } },
{ "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 1024 } },
{ "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } }
]
}
}
覆盖部分使用与 SourceContext 匹配的命名空间前缀。更具体的前缀优先级更高。
请求日志中间件
将 ASP.NET Core 为每个请求生成的多条日志事件替换为一条摘要事件。
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms";
options.GetLevel = (httpContext, elapsed, ex) => ex is not null
? LogEventLevel.Error
: httpContext.Response.StatusCode >= 500
? LogEventLevel.Error
: LogEventLevel.Information;
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("UserId",
httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous");
};
});
结构化日志记录与解构
// Named properties — creates queryable structured data
logger.LogInformation("Order {OrderId} placed by {CustomerId} for {Total:C}",
orderId, customerId, total);
// @ operator preserves object structure as properties
logger.LogInformation("Processing {@SensorInput}", sensorInput);
// Output: Processing {"Latitude": 25, "Longitude": 134}
// $ operator forces ToString()
logger.LogInformation("Received {$Data}", new[] { 1, 2, 3 });
// Output: Received "System.Int32[]"
使用 LogContext 设置作用域属性
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("TenantId", tenantId))
{
logger.LogInformation("Processing order {OrderId}", orderId);
// CorrelationId and TenantId attached to ALL log events in this scope
}
需要在日志记录器配置中添加 .Enrich.FromLogContext()。
OpenTelemetry 接收器(OTLP 导出)
无需 OpenTelemetry SDK,即可将 Serilog 事件直接导出到任意 OTLP 后端:
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = "http://localhost:4317";
options.Protocol = OtlpProtocol.Grpc;
options.ResourceAttributes = new Dictionary<string, object>
{
["service.name"] = "MyApp.Api",
["deployment.environment"] = "production"
};
})
使用 Serilog.Expressions 进行筛选
需要 Serilog.Expressions 包。
// Exclude health check noise
.Filter.ByExcluding("RequestPath like '/health%'")
// Route errors to a separate file
.WriteTo.Conditional("@l = 'Error'",
wt => wt.File("logs/errors-.log", rollingInterval: RollingInterval.Day))
使用 [LoggerMessage] 源生成器处理热点路径
内置于 Microsoft.Extensions.Logging.Abstractions 中——编译时生成,禁用相应级别时零内存分配。
public static partial class OrderLogs
{
[LoggerMessage(Level = LogLevel.Information,
Message = "Order {OrderId} created for {CustomerId}")]
public static partial void OrderCreated(this ILogger logger, Guid orderId, Guid customerId);
}
// Usage
logger.OrderCreated(order.Id, order.CustomerId);
反模式
不要使用字符串插值
// BAD — breaks structured logging, allocates even when level is disabled
logger.LogInformation($"Order {orderId} created for {customerId}");
// GOOD — message template with named parameters
logger.LogInformation("Order {OrderId} created for {CustomerId}", orderId, customerId);
不要跳过 CloseAndFlush
// BAD — async sinks (Seq, OTLP, Elasticsearch) lose buffered events
app.Run();
// GOOD — wrap in try/finally
try { app.Run(); }
catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); }
finally { await Log.CloseAndFlushAsync(); }
不要记录敏感数据
// BAD — passwords and tokens in logs
logger.LogInformation("Login: {Email} with password {Password}", email, password);
// GOOD — never log secrets, passwords, tokens, or PII
logger.LogInformation("Login: {Email}", email);
不要不加限制地解构
// BAD — large object graphs cause memory issues and massive log entries
logger.LogInformation("Request: {@Request}", httpContext.Request);
// GOOD — configure destructuring limits
.Destructure.ToMaximumDepth(4)
.Destructure.ToMaximumStringLength(1024)
.Destructure.ToMaximumCollectionCount(10)
// BETTER — destructure to specific properties
.Destructure.ByTransforming<HttpRequest>(r => new { r.Method, r.Path })
不要使用已弃用的 Elasticsearch 接收器
// BAD — the Serilog.Sinks.Elasticsearch PACKAGE is deprecated
// <PackageReference Include="Serilog.Sinks.Elasticsearch" />
.WriteTo.Elasticsearch("http://localhost:9200")
// GOOD — same method name, but from the official Elastic.Serilog.Sinks
// package, which writes ECS-formatted documents to data streams
// <PackageReference Include="Elastic.Serilog.Sinks" />
.WriteTo.Elasticsearch([new Uri("https://elastic.example.com:9200")], opts =>
opts.DataStream = new DataStreamName("logs", "myapp"))
决策指南
| 场景 | 建议 |
|---|---|
| 应用程序日志记录 | 使用 AddSerilog() 和 appsettings.json 配置 Serilog |
| 日志存储(开发环境) | Seq(免费单用户版)或 Aspire Dashboard |
| 日志存储(生产环境) | Seq、Elasticsearch(Elastic 接收器)或 OTLP 后端 |
| 请求日志记录 | UseSerilogRequestLogging()(替代每个请求产生的冗余日志) |
| 作用域属性 | 在中间件中使用 LogContext.PushProperty() |
| 日志筛选 | 使用 Serilog.Expressions 进行基于表达式的筛选 |
| 高性能路径 | [LoggerMessage] 源生成器 |
| 审计跟踪 | AuditTo(同步执行,异常会向上传播) |
| 按环境设置日志级别 | 在 appsettings 中按命名空间使用 MinimumLevel.Override |
| OpenTelemetry 集成 | Serilog.Sinks.OpenTelemetry(无 SDK 依赖) |
评论
支持 Markdown