一、前言
對內建日誌系統的整體實現進行了介紹之後,可以通過使用內建記錄器來實現日誌的輸出路徑。而在實際專案開發中,使用第三方日誌框架(如: Log4Net、NLog、Loggr、Serilog、Sentry 等)來記錄也是非常多的。首先一般基礎的內建日誌記錄器在第三方日誌框架中都有實現,然後第三方日誌框架在功能上更加強大和豐富,能滿足我們更多的專案分析和診斷的需求。
所以在這一篇中,我們將介紹第三方日誌記錄提供程式——Serilog
二、回顧
系統內建日誌系列:
1. 基於.NetCore3.1系列 —— 日誌記錄之日誌配置揭祕
2. 基於.NetCore3.1系列 —— 日誌記錄之日誌核心要素揭祕
3. 基於.NetCore3.1系列 —— 日誌記錄之自定義日誌元件
從之前學習的內建日誌系統中,我們根據日誌配置的方式瞭解到了通過配置的方式,可以有效的輸出日誌記錄,方便我們查詢發現問題。
而在進一步對內部執行的主要核心機制進行深入探究後發現了內建日誌記錄的幾個核心要素,在日誌工廠記錄器(ILoggerFactory
)中實現將日誌記錄提供器(ILoggerProvider
)物件都可以整合到Logger
物件組合中,這樣的話,我們就可以通過基於ILoggerProvider
自定義日誌記錄程式整合到Logger
中,再建立寫日誌定義Ilogger
,自定義日誌記錄器實現日誌的輸出方式,這樣實現自定義日誌記錄工具。
在最後我們通過自定義的方式簡單的實現了自定義日誌元件,在這個基礎上,我們可以根據具體的需求進行完善修改。當然了,我們也可以借用第三方日誌框架元件程式進行使用。
三、說明
我們都知道日誌記錄在專案開發中或者生產環境中,都起到舉足輕重的作用。因此,我們都會採用在專案加入第三方框架日誌或自行封裝日誌記錄來記錄日誌。
所以在這一篇中,我們會採用在專案中使用Serilog,目的不僅僅在於希望在使用者使用之前發現程式碼中的BUG和錯誤,更多的是方便我們可以快速的查詢生產環境的日誌問題,深入的瞭解系統執行的表現。
從Serilog的官方介紹中,我們可以發現 其框架是.net中的診斷日誌庫,可以在所有的.net平臺上執行。支援結構化日誌記錄,對複雜、分散式、非同步應用程式的支援非常出色。
Serilog是基於日誌事件(log events),而不是日誌訊息(log message)。可以將日誌事件格式化為控制檯的可讀文字或者將事件化為JSON格式。應用程式中的日誌語句會建立LogEvent
物件,而連線到管道的接收器(sinks)會知道如何記錄它們。(接收器 包括各種終端、控制檯、文字、SqlServer、ElasticSearch等等可用的列表)
結構化與非結構化之間的問題:
對於日誌的處理,在大部分情況下,會權衡是否對開發者的友好型以及對程式解析的方便性。在很多情況下,開發者可能只是想記錄一段日誌而已,所以可以會考慮簡單的加上一行程式碼來以達到記錄日誌的目的,如(
log.debug("Disk quota {0} exceeded by user {1}", quota, user);
)當然了,日誌的執行結構可能被存於文字檔案或者資料庫中。這樣的日誌從開發者的角度來說,清晰易懂,十分友好。但是如果後續要使用程式取查詢海量的的上述例子在某段時間內的特定使用者,則很難高效率地完成這一要求,因為需要對每個日誌進行字串解析。因此,我們就需要尋求更快更方便的方式來查詢記錄。
非結構的日誌
對自由格式文字的解析往往依賴於正規表示式,並且依賴於不變的文字。這會使解析自由格式的文字變得非常脆弱(即解析與程式碼中的確切文字緊密耦合)。
還考慮搜尋/查詢的情況,例如:
SELECT text FROM logs WHERE text LIKE "Disk quota";
LIKE
條件需要與每個text
行值進行比較;再次,這在計算上是相對浪費的,尤其是在使用萬用字元時:SELECT text FROM logs WHERE text LIKE "Disk %";
結構化的日誌
使用結構化日誌記錄,與磁碟錯誤相關的日誌訊息在JSON中可能如下所示:
{ "level": "DEBUG", "user": "username", "error_type": "disk", "text": "Disk quota ... exceeded by user ..." }
這種結構的欄位可以很容易地對映到例如 SQL表列名,這意味著查詢可以更具體/更細粒度:
SELECT user, text FROM logs WHERE error_type = "disk";
您可以在希望經常搜尋/查詢其值的列上放置索引,只要您不對
LIKE
這些列值使用子句即可。您可以將日誌訊息細分為特定類別的內容越多,查詢的物件就越有針對性。例如,除了error_type
上面示例中的欄位/列之外,您甚至可以設定為be"error_category": "disk", "error_type": "quota"
或諸如此類。結構越多,你的日誌訊息,通過解析/檢索系統(如
fluentd
,elasticsearch
,kibana
),可以利用該結構,並以更快的速度和更低的CPU /記憶體執行任務。總之這不僅與速度和效率有關,更重要的是使用結構化日誌記錄和“結構化查詢”時,能以特定格式捕獲以及呈現結構化日誌,同時提供對開發者與程式友好的解析支援。可以更方便地以其為條件進行篩選,搜尋結果的相關性將更高。如果沒有這種搜尋,那麼在不同上下文中出現的任何單詞都會給您帶來大量無關的點選。
四、開始
為了更好的理解認識Serilog,我們這簡單的建立一個新的專案來認識一下Serilog的使用。這裡我們就簡單的使用Console
和Debug
的方式來實現,後續有機會我們可以實現更多方式的接收器寫入日誌。
4.1 Serilog使用
4.1.1 安裝依賴包
Serilog.AspNetCore : 基於AspNetCore框架整合的Serilog日誌記錄程式包,包含了Serilog基本庫和控制檯日誌的實現。
當然了,你也可以直接安裝Serilog 基本庫,然後根據需要安裝對應的擴充包。
說明:
- Serilog.Extensions.Logging 包含了注入了Serilog的擴充方法。
- Serilog.Sinks.Async 實現了日誌非同步收集。
- Serilog.Sinks.Console 實現了控制檯輸出日誌。
- Serilog.Sinks.Debug 實現了除錯臺輸出日誌。
- Serilog.Sinks.File 實現了檔案輸出日誌。
4.1.2 配置Serilog
在應用程式中Program.cs
檔案中,配置Serilog記錄,確保正確記錄任何配置日誌問題。
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting web host");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
然後,新增UseSerilog()
到CreateHostBuilder()
中的通用主機中。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) //從appsettings.json中讀取配置。
.UseSerilog() // <-- Add this line
.ConfigureLogging((hostingContext, logging) =>
{
logging.ClearProviders(); //去掉預設新增的日誌提供程式
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
最後,通過刪除預設記錄器的其餘配置進行清理,從appsettings.json
檔案中刪除Logging
對應的配置部分。可以再使用根據Serilog
的配置規則進行相應配置替換它。
"Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning" } } }
4.1.3 提示
當在IIS下執行時候,要在Visual Studio輸出視窗中檢視Serilog輸出日誌的時候,需要將輸出方式選擇為 Web 伺服器方式,輸出視窗檢視日誌,或者使用WriteTo.Debug()
替換記錄器配置中的WriteTo.Console()
。
4.2 輸出格式
4.2.1 文字格式
作為文字,它的格式如下:
[21:45:15 INF] HTTP GET / responded 200 in 227.3253 ms
測試在控制檯中輸出如下:
上述事件格式中,可以看出由以下幾個格式組成:
- 事件發生時的時間戳[timestamp]
- 描述何時應該捕獲事件的級別[level]
- 記錄事件的訊息[message]內容]
- 描述事件的命名屬性[properties]
- 還可能有一個Exception物件
4.2.2 JSON格式
作為JSON格式,它的格式如下:
{
"@t": "2020-08-27T13:59:44.6410761Z",
"@mt": "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms",
"@r": ["224.5185"],
"RequestMethod": "GET",
"RequestPath": "/",
"StatusCode": 200,
"Elapsed": 224.5185,
"RequestId": "0HLNPVG1HI42T:00000001",
"CorrelationId": null,
"ConnectionId": "0HLNPVG1HI42T"
}
在寫入日誌檔案中,根據Serilog的多種接收器的中(Console()、Debug()、File())等支援使用JSON寫入日誌記錄,通過引用緊湊的JSON格式化類庫[Serilog.Formatting.Compact]接收所有JSON格式的輸出。
要編寫以換行符分隔的JSON,請將CompactJsonFormatter
或RenderedCompactJsonFormatter
傳遞到接收器配置方法,如下:
.WriteTo.Console(new RenderedCompactJsonFormatter())
或
.WriteTo.Console(new CompactJsonFormatter())
執行這個程式將產生使用Serilog的緊湊格式JSON,並在對應的輸出路徑中生成換行符分隔的JSON流。
4.3 示例
4.3.1 安裝依賴包
安裝 Serilog.AspNetCore
NuGet 包 ;
4.3.2 配置檔案
在appsettings.json
配置檔案新增 Serilog
配置,WriteTo
指定輸出目標位置,它是一個陣列型別,所以可以指定多個目標位置,這裡暫時只指定輸出到控制檯:
{
"Serilog": {
"MinimumLevel": {
"Default": "Debug"
}
}
}
4.3.3 設定配置資訊
讀取配置檔案資訊,設定配置資訊
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEnvironmentVariables()
.Build();
在main方法中,
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.Enrich.FromLogContext()
.WriteTo.Debug() //輸出路徑
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}") //模板
.CreateLogger();
try
{
Log.Information("Starting web host");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
在Program.cs
新增 UseSerilog
()
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog(); //新增
4.3.4 設定請求管道
在 Startup.cs 的 中的Configure
請求管道中新增 UseSerilogRequestLogging
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
重要的是UseSerilogRequestLogging()
呼叫應出現在諸如MVC之類的處理程式之前。 中介軟體不會對管道中出現在它之前的元件進行時間或日誌記錄。通過將UseSerilogRequestLogging()
放在它們之後,可以將其用於從日誌中排除雜亂的處理程式,例如UseStaticFiles()。)
為了減少每個HTTP請求需要構造,傳輸和儲存的日誌事件的數量。 在同一事件上具有許多屬性還可以使請求詳細資訊和其他資料的關聯更加容易。
預設情況下,以下請求資訊將作為屬性新增:
請求方法
請求路徑
狀態碼
響應時間
您可以使用
UseSerilogRequestLogging()
上的選項回撥來修改用於請求完成事件的訊息模板,新增其他屬性或更改事件級別:app.UseSerilogRequestLogging(options => { // 自定義訊息模板 options.MessageTemplate = "Handled {RequestPath}"; // 發出除錯級別的事件,而不是預設事件 options.GetLevel = (httpContext, elapsed, ex) => LogEventLevel.Debug; //將其他屬性附加到請求完成事件 options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("RequestScheme", httpContext.Request.Scheme); }; });
4.3.5 輸出效果
由於日誌總是輸出一堆,我們不能快速的查詢定位問題,其實 Serilog
輸出的日誌是非常簡潔的,只有 HTTP GET ...
這一條,其他都是 AspNetCore 系統本身輸出的,所以我們可以對輸出的日誌進行簡化操作。
4.3.6 輸出簡化
為了使日誌輸出更簡潔,我們可以設定不輸出 AspNetCore Info 日誌,只需在 Serilog
配置節點中設定 AspNetCore 日誌輸出級別為 Warning
:
{
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
}
}
}
五、總結
- 本篇主要是對Serilog的說明,認識到是一個基於日誌事件的而非日誌訊息的結構化日誌類庫。
- 簡單的涉及對基礎知識的認識以及使用,通過構建一個新的專案來實現Serilog的日誌記錄以及怎麼使用這個框架。
- 在後續中如何結合這個日誌類庫引入專案中使用,以及對日誌怎麼儲存和查詢進行說明(會考慮 ELK儲存採集分析 )。
- 如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
- 參考資料: 官方簡介 、Serilog文件、serilog-aspnetcore
- 本文原始碼下載地址