那些年我們用過的元件-結構化紀錄檔元件 Serilog

2023-01-05 12:01:10

什麼是結構化紀錄檔

我們記錄紀錄檔慣常使用 log4j2NLog 等紀錄檔元件,這些元件提供了輸出到多種終端的能力,但是大部分時候我們選擇將紀錄檔輸出到作業系統的檔案系統中,為什麼呢?至少有一部分原因是記錄的每條紀錄檔為字串格式,且按時間由遠往進順序記錄,開啟檔案可以直接人肉檢索;如果這些紀錄檔記錄到其它終端比如資料庫中,由於是字串格式,無法依靠資料庫的機制提高檢索效率,反而紀錄檔的頻繁寫入和資料量的持續增大,對資料庫造成很大壓力,還需要花時間調優資料庫結構。

但 22 世紀都快到了,還在用古老的人肉檢索實在說不過去,於是出現了流行一時的 EFKELK框架,它們是幾個元件的集合。大致流程如下:

  1. 首先是紀錄檔採集元件比如 filebeats,定時從設定好的路徑中採集增量紀錄檔;
  2. 上傳到訊息佇列比如 kafka,緩解紀錄檔過多時的傳輸壓力;
  3. 然後送達紀錄檔處理元件比如 logstash, logstash 使用 filter 對紀錄檔進行拆分、對映、過濾等,抽取關鍵內容並形成符合目標資料庫特性的格式。注意此處出來的就是結構化紀錄檔;
  4. 將結構化紀錄檔儲存到特定的資料庫比如 elasticsearch 中;
  5. 通過使用者介面如 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 紀錄檔記錄