什麼是結構化日誌
我們記錄日誌慣常使用 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 日誌記錄