Serilog 日志庫簡單實踐(一):文件系統 Sinks(.net8)
〇、前言
前文已經介紹過什么是 Serilog,以及其核心特點,詳見:http://www.rzrgm.cn/hnzhengfy/p/19167414/Serilog_basic。
從本文開始,后續將對各種類型的 Sinks 進行簡單的實踐。
本文將以文件系統相關的 Sinks 為主進行介紹,針對多個相關的動態庫,進行了簡介以及示例項目實現,供參考。
一、文件系統 Sinks 用法
1.1 Serilog.Sinks.File:最基礎的日志組件
Serilog.Sinks.File 是 Serilog 社區維護的核心文件日志接收器,用于將日志事件寫入到一個或多個文本文件中。
它支持文本和 JSON 格式輸出,保留結構化數據,是 Serilog 生態中最基礎的文件日志組件。也支持按時間(如每天、每周)或大小滾動日志文件,可配置多進程共享日志文件,支持寫入緩沖以提高性能。
如下是簡單實踐,可以簡單分為四步:
1)創建 .NET 8 Web API 項目,并安裝 NuGet 包:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
// dotnet sdd package Serilog.Enrichers.Environment // 有需求要添加機器名時,需要添加此包
// dotnet sdd package Swashbuckle.AspNetCore // 若需要使用 Swagger 時,需添加此包
// 注意:使用 Serilog.AspNetCore 而不是基礎的 Serilog,
// 因為它為 ASP.NET Core 提供了更好的集成(如自動記錄請求、依賴注入支持等)
2)添加 appsettings.json 配置(非必要)。
雖然當前示例項目中,主要在代碼中配置 Serilog,但也可以將部分設置放在 appsettings.json 中。
例如:
{
"Serilog": { // 根節點
"MinimumLevel": { // 定義了日志記錄的最低級別,只有等于或高于此級別的日志才會被記錄下來
"Default": "Debug", // 全局默認的日志最低級別是 Debug
"Override": { // 為特定命名空間或類庫重寫默認的日志級別
"Microsoft": "Warning", // 所有來自 Microsoft 命名空間(例如框架內部代碼)的日志,只記錄 Warning 及以上級別(即 Warning, Error, Fatal)
"Microsoft.AspNetCore": "Warning" // Microsoft.AspNetCore 相關組件(如 MVC、HTTP 請求管道等)也只記錄 Warning 及以上級別
}
}
},
"AllowedHosts": "*" // 用于控制應用響應哪些 HTTP 主機頭(Host header)的請求,"*" 表示允許所有主機訪問
// 在開發環境下通常設為 "*",但在生產環境中建議明確指定受信任的主機以增強安全性
}
當全局默認的日志最低級別是 Debug 時,應用程序中所有代碼(除非特別覆蓋)產生的 Debug、Information、Warning、Error、Fatal 級別的日志都會被記錄。
日志級別從低到高通常是:Verbose < Debug < Information < Warning < Error < Fatal。
ASP.NET Core 框架本身會產生大量關于Microsoft、Microsoft.AspNetCore 兩個命名空間的 Information 和 Debug 級別的日志,如果不加限制,在生產環境中會非常冗長且影響性能。通過將其級別提高到 Warning,可以減少噪音日志,只關注真正重要的信息。
3)修改 Program.cs(核心配置)
using Microsoft.AspNetCore.Builder;
using Serilog;
using Serilog.Formatting.Json;
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration) // 讀取 appsettings.json 中的 Serilog 配置
.Enrich.FromLogContext() // 啟用上下文(如 BeginScope)
.Enrich.WithMachineName() // 添加機器名 // 需要包:Serilog.Enrichers.Environment
.Enrich.WithEnvironmentUserName() // 添加用戶名
.WriteTo.Console(new JsonFormatter()) // 控制臺也輸出 JSON(可選,便于開發)
.WriteTo.File(
path: "logs/api-.log",
// formatter: new JsonFormatter(), // 關鍵:使用 JSON 格式保留結構化數據
formatter: new JsonFormatter(renderMessage: true), // renderMessage 加上此選項,日志中才會輸出"RenderedMessage"
rollingInterval: RollingInterval.Day, // 按天滾動
fileSizeLimitBytes: 10_000_000, // 單文件最大 10MB
retainedFileCountLimit: 31, // 最多保留 31 天
rollOnFileSizeLimit: true, // 達到大小限制時也滾動
shared: true // 允許多個進程寫入(IIS/容器場景需要)
)
.CreateLogger();
// 告訴 ASP.NET Core 使用 Serilog 作為日志提供者
builder.Host.UseSerilog();
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // 要使用 Swagger 需要添加包:Swashbuckle.AspNetCore
var app = builder.Build();
// 配置 HTTP 請求管道
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// 在應用啟動時記錄一條結構化日志
app.Lifetime.ApplicationStarted.Register(() =>
{
Log.Information("Application started. Environment: {Environment}, ContentRoot: {ContentRoot}",
app.Environment.EnvironmentName,
app.Environment.ContentRootPath);
});
// 在應用停止時記錄
app.Lifetime.ApplicationStopping.Register(() =>
{
Log.Information("Application is stopping.");
});
app.Run();
4)修改 WeatherForecastController.cs
using Microsoft.AspNetCore.Mvc;
namespace Test.WebAPI._8._0.Serilog.WriteToFile.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// 記錄結構化日志
_logger.LogInformation("Fetching weather forecasts for user {UserId} from IP {ClientIp}",
HttpContext.User.Identity?.Name ?? "Anonymous",
HttpContext.Connection.RemoteIpAddress?.ToString());
try
{
var rng = new Random();
var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToArray();
// 記錄復雜對象(使用 @ 解構)
_logger.LogDebug("Generated {ForecastCount} forecasts: {@Forecasts}",
forecasts.Length, forecasts);
return forecasts;
}
catch (Exception ex)
{
// 記錄異常和上下文
_logger.LogError(ex, "An error occurred while generating weather forecasts for user {UserId}",
HttpContext.User.Identity?.Name ?? "Anonymous");
throw;
}
}
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}
輸出結果:
注:實際輸出為默認的 json 字符串,如下是格式化后便于查看。
{
"Timestamp": "2025-10-23T23:30:44.9346095+08:00",
"Level": "Information",
"MessageTemplate": "Fetching weather forecasts for user {UserId} from IP {ClientIp}",
"RenderedMessage": "Fetching weather forecasts for user \"Anonymous\" from IP \"::1\"", // formatter: new JsonFormatter(renderMessage: true),配置效果
"TraceId": "8168e507fe989ade0dd4887075e3535e",
"SpanId": "9c78435a71c11ee6",
"Properties": {
"UserId": "Anonymous",
"ClientIp": "::1",
"SourceContext": "Test.WebAPI._8._0.Serilog.WriteToFile.Controllers.WeatherForecastController",
"ActionId": "15cb902a-b7c7-4b58-98e4-745dffec67e9",
"ActionName": "Test.WebAPI._8._0.Serilog.WriteToFile.Controllers.WeatherForecastController.Get (Test.WebAPI.8.0.Serilog.WriteToFile)",
"RequestId": "0HNGI94H06F88:00000001",
"RequestPath": "/weatherforecast",
"ConnectionId": "0HNGI94H06F88",
"MachineName": "WIN-J",
"EnvironmentUserName": "WIN-J\\Administrator"
}
}
1.2 Serilog.Sinks.Async:異步操作來避免主程阻塞
Serilog.Sinks.Async 不是一個獨立的 Sink,而是一個異步包裝器,用于將任何其他同步的 Sink(如 Serilog.Sinks.File)包裝在異步操作中,減少日志記錄對主線程的阻塞(特別是 Web 請求線程)。
在高并發場景下,如果日志直接寫入文件、數據庫或網絡服務,這些 I/O 操作是同步阻塞的。如果在每個請求中都記錄日志,會拖慢響應速度,甚至影響吞吐量。
Serilog.Sinks.Async 將日志記錄工作委托給后臺線程,減少 I/O 瓶頸對應用性能的影響,特別適用于文件和數據庫等受 I/O 瓶頸影響的接收器。
- 工作原理
當程序調用 Log.Information() 時,日志事件被推入一個內部的異步隊列(默認大小是 1000 條)之后,立馬返回。一個后臺線程(或線程池任務)從隊列中取出日志,再交給被包裝的 Sink(如 File Sink)處理。主線程無需等待寫入完成,立即返回。
另外需要注意,極端情況下可能丟失最后幾條日志(程序崩潰時),需要合理配置隊列大小和溢出策略。
- 示例項目
大概思路:分別生成兩個.net8.0 WebAPI 項目,一個使用同步,一個使用異步來實現將同樣的內容,以 json 字符串的格式寫入到文本文件中。啟動服務后,通過壓測工具(本文使用:wrk)觀察性能差異。
下邊是實際操作的簡要步驟:
1.2.1 創建兩個 .net 8.0 WebAPI 示例項目,然后添加幾個動態庫
同步項目名稱:Test.WebAPI.8.0.Serilog.WriteToFile。異步實現項目:Test.WebAPI.Net8.Serilog.FileAsync。
需要引用的動態庫:
// 兩個項目都需要添加下邊三個包:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Formatting.Compact
// 異步實現另外添加 Async 專用包:
dotnet add package Serilog.Sinks.Async
1.2.2 編輯 Program.cs
同步項目實現:
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
var builder = WebApplication.CreateBuilder(args);
// 【配置 Serilog(同步)】
builder.Host.UseSerilog((context, config) =>
{
config.WriteTo.File(
path: "logs-sync/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
// formatProvider: CultureInfo.InvariantCulture,
// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
formatter: new RenderedCompactJsonFormatter(), // 配置使用json格式字符串輸出
restrictedToMinimumLevel: LogEventLevel.Information
);
});
var app = builder.Build();
app.MapGet("/api/logSync", () =>
{
Log.Information("Fetching weather forecasts for user {UserId} from IP {ClientIp}",
"Anonymous",
"This is IP");
return "Log recorded (synchronous)";
});
app.Run();
異步項目實現:
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
var builder = WebApplication.CreateBuilder(args);
// 【配置 Serilog(異步)】
builder.Host.UseSerilog((context, config) =>
{
config.WriteTo.Async(a => a.File(
path: "logs-async/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
//formatProvider: CultureInfo.InvariantCulture,
//outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
formatter: new RenderedCompactJsonFormatter(), // 配置使用json格式字符串輸出
restrictedToMinimumLevel: LogEventLevel.Information
));
});
var app = builder.Build();
app.MapGet("/api/logAsync", () =>
{
Log.Information("Fetching weather forecasts for user {UserId} from IP {ClientIp}",
"Anonymous",
"This is IP");
return "Log recorded (asynchronous)";
});
app.Run();
配置完成,同時啟動兩個項目,兩個服務地址例如:
// 服務地址:
http://localhost:5043/api/logSync
http://localhost:5001/api/logAsync
注:日志文件夾會在項目啟動時自動創建,無需手動配置或新增。
1.2.3 測試與解析
直接在 Windows 上通過 Docker 和 wrk 來進行的是兩種方式的性能差異。
// 安裝和配置 Docker Desktop 步驟【略。。。】
// 博主使用的鏡像源地址:"https://docker.xuanyuan.me",也可以使用阿里和騰訊的,哪個效率高用哪個
// 拉取 wrk 包
C:\Users\Administrator>docker pull williamyeh/wrk
Using default tag: latest
latest: Pulling from williamyeh/wrk
4fe2ade4980c: Pull complete
c4d7e348633d: Pull complete
3e403d3ebdda: Pull complete
bdb672ee55d9: Pull complete
2bfb714176a4: Pull complete
Digest: sha256:78adc0d9d51a99e6759e702a08d03eaece81c890ffcc9790ef9e5b199d54f091
Status: Downloaded newer image for williamyeh/wrk:latest
docker.io/williamyeh/wrk:latest
// 異步記錄日志測試
// 注意:host.docker.internal 是 Docker 宿主機的特殊 DNS 名稱,說明被測服務運行在宿主機上,而 wrk 在容器內發起請求
C:\Users\Administrator>docker run -it --rm williamyeh/wrk -t12 -c100 -d10s http://host.docker.internal:5001/api/logAsync
Running 10s test @ http://host.docker.internal:5001/api/logAsync
12 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 15.59ms 5.85ms 103.93ms 93.31%
Req/Sec 524.79 106.91 660.00 78.36%
63040 requests in 10.01s, 10.82MB read
Requests/sec: 6296.32
Transfer/sec: 1.08MB
// 同步記錄日志測試
C:\Users\Administrator>docker run -it --rm williamyeh/wrk -t12 -c100 -d10s http://host.docker.internal:5043/api/logSync
Running 10s test @ http://host.docker.internal:5043/api/logSync
12 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 19.52ms 6.70ms 90.80ms 79.40%
Req/Sec 413.05 43.31 656.00 85.56%
49576 requests in 10.05s, 8.46MB read
Requests/sec: 4930.52
Transfer/sec: 862.05KB
C:\Users\Administrator>
// 關于 wrk -t12 -c100 -d10s
// 同時開啟 12 個線程,每個線程與目標服務創建 100 個 TCP/HTTP keep-alive 連接,持續發起請求,持續 10 秒鐘
// 相當于,在同一時間,有 1200 個連接在持續請求(一個請求完成,接著繼續發出下一個請求)目標服務
下邊簡單分析下測試結果。
| 指標 | 異步 (/logAsync) |
同步 (/logSync) |
對比 |
|---|---|---|---|
| 總請求數 | 63,040 | 49,576 | +27.1% ↑ |
| 吞吐量 (Requests/sec) | 6,296.32 | 4,930.52 | +27.7% 提升 |
| 傳輸速率 (Transfer/sec) | 1.08 MB | 862.05 KB | +25.3% ↑ |
| 平均延遲 (Latency Avg) | 15.59ms | 19.52ms | 快 20% |
| 最大延遲 (Max Latency) | 103.93ms | 90.80ms | 略高(可能因隊列堆積) |
| 延遲標準差 (Stdev) | 5.85ms | 6.70ms | 更穩定 |
| 每秒請求數波動 (Req/Sec Stdev) | ±106.91 | ±43.31 | 異步波動更大,但均值更高 |
其中,異步操作的最大延遲稍高。可能原因:異步任務積壓在線程池或消息隊列中,在高峰期出現短暫排隊,導致個別請求的“真實完成時間”變長(但從 API 返回角度看仍是快的)。
異步雖然吞吐高,但單位時間內請求處理數量波動更大,可能是由于線程調度、異步任務調度引入了不確定性。
總結一下,就是異步顯著優于同步(+27% QPS,-20% 延遲)。
1.3 Serilog.Sinks.RollingFile:已過時,無需單獨使用
此動態庫主要功能是,配置日志文件如何按規則滾動,但現在已經在 Serilog.Sink.File 中進行了實現(詳見本文1.1),因此無需再使用此動態庫。
這個變化是 Serilog 在 2.0 版本之后(特別是向 3.0 和 5.0 進化過程中)的一個重要的演變。
但是,RollingFile 的功能并沒有消失,而是被整合并增強到了 Serilog.Sinks.File 包中。現在推薦的做法是:只安裝 Serilog.Sinks.File 這一個包,它就能滿足所有文件輸出需求,包括強大的滾動功能。
Serilog.Sinks.File 的 File() 方法提供了以下關鍵參數來實現滾動:
rollingInterval:這是最主要的滾動策略參數。它是一個 RollingInterval 枚舉,可選值包括:
??RollingInterval.Year:按年滾動。
??RollingInterval.Month:按月滾動。
??RollingInterval.Day:最常用,按天滾動。
??RollingInterval.Hour:按小時滾動。
??RollingInterval.Minute:按分鐘滾動(適用于極高日志量的場景)。
??RollingInterval.Infinite:不滾動,所有日志寫入單個文件。(注意:Infinite 在 Serilog 5.0 中以被移除,如需配置不滾動,可以通過略過此項配置來實現)。
rollOnFileSizeLimit:一個布爾值。如果設置為 true,并且設置了 fileSizeLimitBytes,那么當日志文件達到指定大小時,也會觸發滾動,即使當前時間間隔(如一天)還未結束。這實現了“按大小或時間”任一條件滿足即滾動的邏輯。
fileSizeLimitBytes:設置單個日志文件的最大字節數。達到此大小后,如果 rollOnFileSizeLimit 為 true,則會創建新文件。
文件名中的 {Date} 占位符:當使用 rollingInterval 時,通常在文件路徑中包含 {Date}。Serilog 會自動用當前日期(格式為 yyyyMMdd)替換它,從而生成如 log-20251026.txt 的文件名。
1.4 Serilog.Sinks.Map:根據日志屬性的值將日志時間路由到不同的 Sink
Map 允許根據日志屬性的值將日志事件路由到不同的 Sink。例如,可以根據 UserId 將日志寫入不同的文件,非常適合需要按用戶或租戶隔離日志的場景。
因此,如果需要根據不同上下文(如用戶ID、請求ID、租戶ID等)將日志分發到不同的目標文件或處理邏輯,Map 是非常有用的;否則,一般不需要。
需要注意的地方:
??性能開銷:Map 內部使用字典緩存子 logger,頻繁變化的鍵可能導致內存增長。
??過度設計風險:大多數場景下,統一結構化日志 + 集中式日志分析工具(如 Seq、ELK)更合適。
??Json 寫入本身不需要 Map:你完全可以用 WriteTo.File(..., formatter: new JsonFormatter()) 直接輸出 JSON 日志。
典型應用場景就是,按 UserId 將審計日志寫入不同日志文件。
1.4.1 創建測試項目并添加必要的包
// 添加下邊三個包:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sink.Map
1.4.2 修改 Program.cs
using Serilog;
using Serilog.Context;
using Serilog.Formatting.Compact;
using Serilog.Formatting.Json;
var builder = WebApplication.CreateBuilder(args);
// 配置 Serilog
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Information()
// 主日志:所有日志匯總到一個 JSON 文件
.WriteTo.File(
new JsonFormatter(),
"logs/app-all-.json",
rollingInterval: RollingInterval.Day
)
// 特殊需求:按 UserId 分別記錄審計日志到獨立 JSON 文件
.WriteTo.Map("UserId", "unknown", (id, wt) =>
{
wt.File(
path: $"logs/audit/user_{id}-.json",
formatter: new RenderedCompactJsonFormatter(), // 輸出為 JSON 格式
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 10_000_000,
rollOnFileSizeLimit: true
);
})
.CreateLogger();
builder.Host.UseSerilog(); // 使用 Serilog 替代默認日志
var app = builder.Build();
// 模擬 API:記錄帶 UserId 的審計日志
app.MapGet("/api/user/{id}/action", (int id, ILogger<Program> logger) =>
{
using (LogContext.PushProperty("UserId", id)) // 設置上下文屬性
{
Log.Information("User performed an action {id}", id);
Log.Information("User viewed profile {TestParameters}", "測試字符串");
}
// 也可以直接記錄
Log.ForContext("UserId", id).Information("Another action logged.");
return Results.Ok(new { Message = "Action logged", UserId = id });
});
app.Run();
1.4.3 測試結果驗證
// 如下是兩個測試地址,分別是用戶 1001 和 1002
http://localhost:5001/api/user/1001/action
http://localhost:5001/api/user/1002/action
注意,實際記錄的 json 格式日志是沒有格式化的,下面展示下格式化后的 json,僅為了方便查看。
如下是 \logs\audit\user_1001-20251031.json 文件內容:
{
"@t": "2025-10-31T09:20:18.5714393Z",
"@m": "User performed an action 1001",
"@i": "0c7b7f7a",
"@tr": "b93dc8db0938aac572cf3d92d437deaf",
"@sp": "baa1c0bc7eb6880f",
"id": 1001,
"UserId": 1001,
"RequestId": "0HNGOBQMLDQHE:00000001",
"RequestPath": "/api/user/1001/action",
"ConnectionId": "0HNGOBQMLDQHE"
}
{
"@t": "2025-10-31T09:20:18.5783697Z",
"@m": "User viewed profile \"測試字符串\"",
"@i": "acc2ba35",
"@tr": "b93dc8db0938aac572cf3d92d437deaf",
"@sp": "baa1c0bc7eb6880f",
"TestParameters": "測試字符串",
"UserId": 1001,
"RequestId": "0HNGOBQMLDQHE:00000001",
"RequestPath": "/api/user/1001/action",
"ConnectionId": "0HNGOBQMLDQHE"
}
{
"@t": "2025-10-31T09:20:18.5794363Z",
"@m": "Another action logged.",
"@i": "58648cce",
"@tr": "b93dc8db0938aac572cf3d92d437deaf",
"@sp": "baa1c0bc7eb6880f",
"UserId": 1001,
"RequestId": "0HNGOBQMLDQHE:00000001",
"RequestPath": "/api/user/1001/action",
"ConnectionId": "0HNGOBQMLDQHE"
}
另外兩個日志文件內容就略過了:
\logs\app-all-20251031.json
\logs\audit\user_1002-20251031.json
輸出日志內容為 json 格式,然后就可以用 ELK / Seq / Loki 等工具做結構化解析和查詢,這才是現代日志實踐。
以上,就是對文件系統 Sink 的全部實踐記錄了,后續還有其他類型的 Sink 持續挖掘中。
本文來自博客園,作者:橙子家,歡迎微信掃碼關注博主【橙子家czzj】,有任何疑問歡迎溝通,共同成長!

浙公網安備 33010602011771號