日志组件 - Serilog
为什么选择 Serilog?
Serilog 优势
Serilog 是 .NET 中最流行的结构化日志库,支持丰富的输出目标(Sink)和强大的结构化日志功能。
📊 结构化日志 - 支持结构化数据,便于查询和分析
🔌 丰富的 Sink - 支持文件、数据库、Elasticsearch等多种输出
⚡ 高性能 - 异步写入,不阻塞应用程序
快速开始
1. 安装 NuGet 包
# 核心包
dotnet add package Serilog
dotnet add package Serilog.AspNetCore
# 常用 Sinks
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
# 扩展
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
2. 基础配置
- Program.cs 配置
- appsettings.json 配置
Program.cs (.NET 6+)
using Serilog;
// 创建日志配置
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
try
{
Log.Information("应用程序启动中...");
var builder = WebApplication.CreateBuilder(args);
// 使用 Serilog
builder.Host.UseSerilog();
builder.Services.AddControllers();
var app = builder.Build();
app.UseSerilogRequestLogging(); // 记录 HTTP 请求
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "应用程序启动失败");
}
finally
{
Log.CloseAndFlush();
}
appsettings.json
{
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {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}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
}
}
Program.cs
var builder = WebApplication.CreateBuilder(args);
// 从配置文件读取
builder.Host.UseSerilog((context, configuration) =>
{
configuration.ReadFrom.Configuration(context.Configuration);
});
日志级别
级别 | 使用场景 | 示例 |
---|---|---|
Verbose | 详细的调试信息 | 跟踪程序执行流程 |
Debug | 调试信息 | 变量值、状态信息 |
Information | 常规信息 | 应用启动、关键操作完成 |
Warning | 警告信息 | 可恢复的错误、性能问题 |
Error | 错误信息 | 异常、失败的操作 |
Fatal | 致命错误 | 导致应用崩溃的错误 |
Log.Verbose("详细信息:{Details}", details);
Log.Debug("调试信息:变量值 = {Value}", value);
Log.Information("用户 {UserId} 登录成功", userId);
Log.Warning("磁盘空间不足:{FreeSpace}GB", freeSpace);
Log.Error(ex, "处理订单 {OrderId} 时发生错误", orderId);
Log.Fatal(ex, "应用程序崩溃");
结构化日志
结构化日志的威力
使用 {}
占位符而非字符串插值,Serilog 会自动捕获结构化数据。
- ✅ 推荐
- ❌ 不推荐
// 结构化日志:使用占位符
var user = new { Id = 123, Name = "张三", Email = "zhangsan@example.com" };
Log.Information("用户 {UserId} {UserName} 登录成功", user.Id, user.Name);
// 使用 @ 解构对象
Log.Information("创建订单:{@Order}", order);
// 使用 $ 转换为字符串
Log.Information("处理文件:{FilePath}", filePath);
输出(JSON格式):
{
"Timestamp": "2025-01-15T10:30:00",
"Level": "Information",
"MessageTemplate": "用户 {UserId} {UserName} 登录成功",
"Properties": {
"UserId": 123,
"UserName": "张三"
}
}
// 字符串插值:丢失结构化信息
var user = new { Id = 123, Name = "张三" };
Log.Information($"用户 {user.Id} {user.Name} 登录成功");
// 输出只是普通字符串,无法按字段查询
// "用户 123 张三 登录成功"
问题
使用字符串插值会丢失结构化信息,导致无法高效查询和分析日志。
常用 Sinks
1. 文件 Sink
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
path: "logs/app-.log",
rollingInterval: RollingInterval.Day, // 按天滚动
retainedFileCountLimit: 30, // 保留30天
fileSizeLimitBytes: 10_000_000, // 10MB
rollOnFileSizeLimit: true, // 超过大小时滚动
shared: true, // 多进程共享
flushToDiskInterval: TimeSpan.FromSeconds(1) // 刷新间隔
)
.CreateLogger();
2. Seq Sink (推荐用于开发和生产)
// 安装: dotnet add package Serilog.Sinks.Seq
Log.Logger = new LoggerConfiguration()
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
为什么用 Seq?
- 🔍 强大的查询和过滤功能
- 📊 实时日志查看和图表
- 🎯 支持结构化日志
- 🆓 免费版足够小团队使用
3. Elasticsearch Sink
// 安装: dotnet add package Serilog.Sinks.Elasticsearch
Log.Logger = new LoggerConfiguration()
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
{
AutoRegisterTemplate = true,
IndexFormat = "applogs-{0:yyyy.MM.dd}"
})
.CreateLogger();
4. 数据库 Sink
// 安装: dotnet add package Serilog.Sinks.MSSqlServer
Log.Logger = new LoggerConfiguration()
.WriteTo.MSSqlServer(
connectionString: "Server=.;Database=Logs;...",
sinkOptions: new MSSqlServerSinkOptions
{
TableName = "Logs",
AutoCreateSqlTable = true
})
.CreateLogger();
日志增强 (Enrichers)
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext() // 从上下文添加属性
.Enrich.WithMachineName() // 机器名
.Enrich.WithThreadId() // 线程ID
.Enrich.WithEnvironmentName() // 环境名(开发/生产)
.Enrich.WithProperty("Application", "MyApp") // 自定义属性
.CreateLogger();
// 使用 LogContext 添加上下文属性
using (LogContext.PushProperty("UserId", userId))
{
Log.Information("执行操作"); // 自动包含 UserId
}
在代码中使用
1. 控制器中使用
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ILogger<UsersController> _logger;
public UsersController(ILogger<UsersController> logger)
{
_logger = logger;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
_logger.LogInformation("获取用户信息:{UserId}", id);
try
{
var user = await _userService.GetByIdAsync(id);
if (user == null)
{
_logger.LogWarning("用户不存在:{UserId}", id);
return NotFound();
}
_logger.LogInformation("成功获取用户:{@User}", user);
return Ok(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户失败:{UserId}", id);
return StatusCode(500, "内部服务器错误");
}
}
}
2. 服务中使用
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
using (_logger.BeginScope("OrderId:{OrderId}", Guid.NewGuid()))
{
_logger.LogInformation("开始创建订单:{@Request}", request);
try
{
// 创建订单逻辑
var order = await CreateAsync(request);
_logger.LogInformation("订单创建成功:{OrderId}, 金额:{Amount}",
order.Id, order.TotalAmount);
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "创建订单失败");
throw;
}
}
}
}
最佳实践
日志最佳实践
1. 使用结构化日志
// ✅ 好
Log.Information("用户 {UserId} 下单 {OrderId},金额 {Amount}", userId, orderId, amount);
// ❌ 差
Log.Information($"用户 {userId} 下单 {orderId},金额 {amount}");
2. 合理选择日志级别
// Information: 重要业务事件
Log.Information("订单 {OrderId} 创建成功", orderId);
// Warning: 可恢复的异常情况
Log.Warning("缓存未命中,使用数据库查询:{CacheKey}", cacheKey);
// Error: 需要关注的错误
Log.Error(ex, "支付失败:{OrderId}", orderId);
3. 记录关键业务指标
_logger.LogInformation("订单处理耗时:{ElapsedMs}ms, 订单ID:{OrderId}",
elapsed.TotalMilliseconds, orderId);
4. 避免记录敏感信息
// ❌ 不要记录密码、信用卡号等敏感信息
Log.Information("用户登录:{Username}, 密码:{Password}", username, password);
// ✅ 只记录必要信息
Log.Information("用户登录:{Username}", username);
5. 使用作用域关联日志
using (_logger.BeginScope(new Dictionary<string, object>
{
["UserId"] = userId,
["CorrelationId"] = correlationId
}))
{
// 此作用域内的所有日志自动包含 UserId 和 CorrelationId
_logger.LogInformation("执行操作A");
_logger.LogInformation("执行操作B");
}
性能优化
1. 异步日志
// 使用 Async Sink 避免阻塞
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File("logs/app-.log"))
.CreateLogger();
2. 条件日志
// 避免不必要的字符串操作
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("复杂计算结果:{Result}", ExpensiveOperation());
}
3. 批量写入
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
"logs/app-.log",
buffered: true, // 启用缓冲
flushToDiskInterval: TimeSpan.FromSeconds(2) // 定期刷新
)
.CreateLogger();
HTTP 请求日志
var app = builder.Build();
// 记录 HTTP 请求
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
// 自定义属性
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress);
};
});
总结
关键要点
- Serilog 提供强大的结构化日志功能
- 使用
{}
占位符保持日志结构化 - 选择合适的 Sink(文件、Seq、Elasticsearch)
- 合理使用日志级别
- 避免记录敏感信息
- 使用异步日志提高性能