結構化日志組件 Serilog
什么是結構化日志
我們記錄日志慣常使用 log4j2、NLog 等日志組件,這些組件提供了輸出到多種終端的能力,但是大部分時候我們選擇將日志輸出到操作系統的文件系統中,為什么呢?至少有一部分原因是記錄的每條日志為字符串格式,且按時間由遠往進順序記錄,打開文件可以直接人肉檢索;如果這些日志記錄到其它終端比如數據庫中,由于是字符串格式,無法依靠數據庫的機制提高檢索效率,反而日志的頻繁寫入和數據量的持續增大,對數據庫造成很大壓力,還需要花時間調優數據庫結構。
但 22 世紀都快到了,還在用古老的人肉檢索實在說不過去,于是出現了流行一時的 EFK、ELK框架,它們是幾個組件的集合。大致流程如下:
- 首先是日志采集組件比如
filebeats,定時從配置好的路徑中采集增量日志; - 上傳到消息隊列比如
kafka,緩解日志過多時的傳輸壓力; - 然后送達日志處理組件比如
logstash, logstash 使用filter對日志進行拆分、映射、過濾等,抽取關鍵內容并形成符合目標數據庫特性的格式。注意此處出來的就是結構化日志; - 將結構化日志存儲到特定的數據庫比如
elasticsearch中; - 通過用戶界面如
Kibana進行日志檢索。
上述流程在不同場景下有一些變種,不再贅述。 它們的主要目的就是使得傳統的文件日志可以被計算機高效檢索。
那么有沒有一種可能,跳過文件存儲,直接將日志按特定格式寫入到目標存儲容器,可能是 elasticsearch,也可能是 mysql,甚至是文件系統。同樣代碼,輸出不同的格式到不同的終端,同時滿足 human-friendly and machine-readable。
在 .NET 世界中, 本文的主角 Serilog 就可以幫我們省去那些彎彎繞繞,依靠它,記錄與查詢日志顯得簡單而純粹。
Serilog
以官方例子說明:
var position = new { Latitude = 25, Longitude = 134 };
var elapsedMs = 34;
log.Information("Processed {@Position} in {Elapsed} ms", position, elapsedMs);
按字面意思,最終會輸出:
09:14:22 [INF] Processed {"Latitude": 25, "Longitude": 134} in 34 ms.
當 Serilog 將日志直接輸出到文件系統或命令行時,結果是這樣沒錯,其它日志組件也能做到(廢話)。
當輸出到 MongoDB 時,結果就不一樣了:
{ "Position": { "Latitude": 25, "Longitude": 134 }, "Elapsed": 34 }
Sink
Serilog 將輸出目標稱之為 sink,不同的 sink 可以有各自的格式要求。其實原理很簡單,輸出到特定 sink 時,日志對象會先格式化處理(注意不是先生成字符串再格式化)。Serilog.Formatting.Compact 就是格式化為 json 的類庫,輸出到 elasticsearch 還需要 Serilog.Formatting.Elasticsearch。不過除非自定義 sink,這些我們都不用關心,使用時只要引入需要的 sink 類庫即可。
使用
下面介紹在 .NET6 中使用 Serilog。
先引入 Serilog 類庫和需要的 Sink 庫比如這里的 Serilog.Sinks.File:
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
以通用宿主程序為例:
IHost host = Host.CreateDefaultBuilder(args).Build();
// 配置并創建 logger 實例
var log = new LoggerConfiguration()
.MinimumLevel.Warning()
.WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, fileSizeLimitBytes: 10485760, rollOnFileSizeLimit: true, retainedFileCountLimit: 100, buffered: true)
.CreateLogger();
log.Information("Hello, Serilog!"); // 直接使用(可以創建多個實例使用)
Log.Logger = log; // Serilog 并沒有實例狀態需要線程間維護,所以為了方便我們可以使用單例模式,將實例賦給全局靜態屬性
Log.Information("The global logger has been configured"); // 項目內任意其它地方均可使用
await host.RunAsync().ContinueWith(_=> Log.CloseAndFlush()); // app 退出時釋放 logger 占用資源
如果想以 .NET 內置的方式調用 Serilog,對于通用宿主程序,須引入 Serilog.Extensions.Hosting,其扮演適配器的角色,將 Serilog 自己的接口 Serilog.ILogger 轉換為 Microsoft.Extensions.Logging.ILogger 使用。如果是 web 項目的話,引入的是 Serilog.AspNetCore;.NET Core 1.0, 1.1 等版本需要引入的是 Serilog.Extensions.Logging。
更改后的版本如下:
IHost host = Host
.CreateDefaultBuilder(args)
.UseSerilog() // 新增該行
.Build();
// ... 其余代碼同上
另外,上述代碼是直接硬編碼配置 logger,更好的方式是通過 appsettings.json 配置 logger。首先引入 Serilog.Settings.Configuration,然后在 appsettings.json 中移除默認的 Logging 配置節,替換為 Serilog 配置節如下:
{
"Serilog": {
"Using": [ "Serilog.Sinks.File" ],
"MinimumLevel": "Warning",
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "Logs/log.txt",
"rollingInterval": "Day",
"fileSizeLimitBytes": 10485760,
"rollOnFileSizeLimit": true,
"retainedFileCountLimit": 100,
"buffered": true
}
}
]
}
}
代碼更改如下:
IHost host = Host
.CreateDefaultBuilder(args)
.UseSerilog((ctx, config) => config
.ReadFrom.Configuration(ctx.Configuration))
.Build();
//以下注釋
//var log = new LoggerConfiguration()
// .MinimumLevel.Warning()
// .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, fileSizeLimitBytes: 10485760, rollOnFileSizeLimit: true, retainedFileCountLimit: 100, shared: true, buffered: true)
// .CreateLogger();
//Log.Logger = log;
await host.RunAsync(); //注釋.ContinueWith(_ => Log.CloseAndFlush());
采用這種方式,Log.Logger 會隱式賦值,并在系統退出時自動釋放資源。
參考資料
Docker+EFK 快速搭建日志收集系統
Message Templates
.NET Worker Service 添加 Serilog 日志記錄

浙公網安備 33010602011771號