ASP.NET Core 沉思录 - 结构化日志
在 《ASP.NET Core 沉思錄 - Logging 的兩種介入方法》中我們介紹了 ASP.NET Core 中日志的基本設(shè)計(jì)結(jié)構(gòu)。這一次我們來觀察日志記錄的格式,并進(jìn)一步考慮如何在應(yīng)用程序中根據(jù)不同的需求選擇不同的日志記錄形式。
太長(zhǎng)不讀:直接飛到文章最后 :-D
Microsoft.Extension.Logging 體系下的日志格式
為了便于閱讀,我們?nèi)匀粚?Microsoft.Extension.Logging 的基本設(shè)計(jì)結(jié)構(gòu)放在這里:
奇怪的不合理之處
注:所謂奇怪就是這種不合理是只是從某一種特定視角看的。
對(duì)于記錄日志而言,雖然一些具體的日志記錄目標(biāo)和記錄的格式會(huì)有一些聯(lián)系,但是日志記錄的目標(biāo)和日志記錄的格式應(yīng)該是兩件事情。貌似 Microsoft.Extension.Logging 在此處進(jìn)行了一些抽象。首先,日志具體的記錄地點(diǎn)和記錄格式全部由具體的 ILoggerProvider 創(chuàng)建的 ILogger 來完成。而對(duì)于日志的格式化方法,則使用 ILogger.Log 方法中的委托來完成。該委托中包含了一個(gè) formatter 委托參數(shù)。該委托接收需要記錄的對(duì)象,關(guān)聯(lián)的異常實(shí)例并返回日志字符串。我們可以在其中定義自己的格式化邏輯。總結(jié)起來感覺是:
特定的 ILoggerProvider 創(chuàng)建將日志記錄到特定種類的目的地的日志記錄器。例如,ConsoleLogger。
指定 ILogger.Log 方法中的 formatter 參數(shù)對(duì)日志對(duì)象進(jìn)行格式化。
ILogger.Log 方法除了 formatter 之外還包含如下的參數(shù):
logLevel:日志的級(jí)別。
eventId:當(dāng)前事件的標(biāo)識(shí)。
state:日志對(duì)象。
exception:關(guān)聯(lián)的異常對(duì)象。
而 formatter 參數(shù)將使用其中的 state 參數(shù)和 exception 參數(shù)對(duì)日志進(jìn)行格式化。這樣通過替換 formatter 的邏輯就可以更改日志的形式了。例如,使用如下的邏輯就可以將 state 格式化為 JSON 形式:
//?Capture?output?so?that?we?can?assert?its?contentvar?writer?=?new?StringWriter();
Console.SetOut(writer);
//?Normal?initialization?logic
var?serviceCollection?=?new?ServiceCollection();
serviceCollection.AddLogging(
????config?=>?config.SetMinimumLevel(LogLevel.Debug).AddConsole());
ServiceProvider?provider?=?serviceCollection.BuildServiceProvider();
var?loggerFactory?=?provider.GetService<ILoggerFactory>();
var?logger?=?loggerFactory.CreateLogger("category");
//?Write?log?message
logger.Log(
????LogLevel.Information,
????1,
????new?{message?=?"Hello?{name}",?name?=?"World"},
????null,
????(state,?exception)?=>?JsonConvert.SerializeObject(state));
//?This?is?very?important.?The?console?logger?using?a?async?processor?to?consume
//?the?queued?log?message.
Thread.Sleep(1000);
Assert.Equal("...(omitted)...",?writer.ToString())
如果您嘗試了上述范例程序就會(huì)感到這個(gè)設(shè)計(jì)好像有問題,而如果聯(lián)系整個(gè) Extension.Logging 體系則感覺問題就更大了:
formatter只是解決了日志 message 部分的格式化問題,而無法影響其他信息的格式化,例如 eventId、logLevel、exception 等。
我們根本不會(huì)用到具體的 ILoggerProvider 而是會(huì)使用 ILoggerFactory 提供的 Logger 門面。這個(gè) Logger 是一個(gè)組合 Logger,也就是它會(huì)將 ILogger<>.Log 調(diào)用分發(fā)出來。但是我們很少使用 ILogger<>.Log 方法,而會(huì)使用擴(kuò)展方法使用 template message 進(jìn)行日志記錄,這意味著所有的子 ILogger 實(shí)現(xiàn)都會(huì)接到同樣的 formatter。自此,不同的目標(biāo)采用不同的消息格式的理想破滅了。
總結(jié)一下就是,日志的記錄目標(biāo)和日志的格式混合了起來。職責(zé)區(qū)分不清。message 的格式化職責(zé)交給了門面擴(kuò)展方法;而另一部分格式化職責(zé)交給了具體的 ILoggerProvider。
從另一種視角看的合理之處
我們換一個(gè)視角可能就會(huì)得到不一樣的體驗(yàn)。首先我們更改分析問題的策略。從端到端的角度來思考問題。當(dāng)我們記錄日志的時(shí)候希望有哪幾類信息呢?日志作為追蹤一個(gè)事件的依據(jù),應(yīng)當(dāng)能夠清晰的說明這個(gè)事件。那么小學(xué)語文老師就告訴過我們,記錄一件事情需要有:
時(shí)間
地點(diǎn)
人物
起因
經(jīng)過
結(jié)果
如果我們將這些信息歸一歸類,我們就可以得到這些信息:
物理世界的環(huán)境參數(shù):時(shí)間
判斷事件嚴(yán)重程度的依據(jù):結(jié)果
事件過程的上下文參數(shù):地點(diǎn)(例如 URI 或代表某種操作的入口)、人物(例如誰進(jìn)行的操作)、起因(例如方法調(diào)用參數(shù))、經(jīng)過(例如調(diào)用的那個(gè)方法)
而要記錄這些信息,則可以對(duì)應(yīng)到程序中的以下幾種形式的數(shù)據(jù):
物理世界的環(huán)境參數(shù):例如 DateTime、DateTimeOffset
判斷事件嚴(yán)重程度的依據(jù):例如 LogLevel
事件過程上下文的參數(shù):例如事件的類別 CategoryName;一個(gè)包含各種各樣上下文參數(shù)的 object[] 對(duì)象;以及對(duì)人類友好,能夠?qū)⑦@些參數(shù)串起來的消息模板。
至此,你能夠看到這些參數(shù)正是 Microsoft.Extension.Logging 中門面擴(kuò)展方法中需要你來提供的參數(shù)。它本來也沒有希望你調(diào)用 ILogger.Log 方法。而是希望你調(diào)用擴(kuò)展方法用最舒服的方式達(dá)成日志記錄的目的。
而作為 ILoggerProvider 開發(fā)者,你并不一定必須得接受 formatter 生成的格式化后的日志消息。你可以選擇處理每一個(gè)傳入?yún)?shù)。具體的請(qǐng)參見 FormattedLogValues 類型的源代碼。
階段性總結(jié)
日志記錄和記敘文一樣,只要滿足了六要素就可以說清楚一件事情。而記錄這六要素的形式正式 Extension.Logging 提供給我們的擴(kuò)展方法的參數(shù)形式。
分析問題從端到端分析是一種非??孔V的分析方法??梢员苊庾邚澛?。
ILogger 的擴(kuò)展方法負(fù)責(zé)生成日志消息,ILoggerProvider 和負(fù)責(zé)記錄工作的 ILogger 實(shí)現(xiàn)負(fù)責(zé)格式化日志消息并將日志記錄到特定的目標(biāo)上去。
利用 SeriLog 實(shí)現(xiàn)靈活的日志記錄形式
通過上述分析我們應(yīng)該能夠看到這種設(shè)計(jì)的合理性。但是不爭(zhēng)的事實(shí)是 ILoggerProvider 一系包攬兩種職能,并沒有進(jìn)一步抽象,有沒有人來對(duì)日志記錄的目標(biāo)和日志記錄的整體格式進(jìn)行抽象呢?有!那就是被千萬人喜愛的 SeriLog。它在 ILoggerProvider 一級(jí)抽象了 ITextFormatter 解決了這個(gè)問題:
我不會(huì)在這里介紹 SeriLog 的具體使用方法。網(wǎng)上教程一大堆大家去搜搜好了。我建議直接去官網(wǎng)。
例如,我可以將日志記錄到 Console 中,默認(rèn)情況下,這種日志的格式是給人看的:
//?normal?initialization?logicvar?serviceCollection?=?new?ServiceCollection();
serviceCollection.AddLogging(b?=>
{
????Logger?seriLogger?=?new?LoggerConfiguration()
????????.WriteTo.Console()
????????.MinimumLevel.Debug()
????????.CreateLogger();
????b.AddSerilog(seriLogger);
});
ServiceProvider?provider?=?serviceCollection.BuildServiceProvider();
//?create?logger
var?loggerFactory?=?provider.GetService<ILoggerFactory>();
var?logger?=?loggerFactory.CreateLogger("category");
//?write?log
logger.LogInformation("Hello?{name}",?"world");
此時(shí)屏幕上會(huì)輸出高亮版的,適于閱讀的日志,類似這樣:
[18:41:27 INF] Hello world
但是如果希望使用其他的格式,則可以通過 ITextFormatter 快速的轉(zhuǎn)換格式:
var?serviceCollection?=?new?ServiceCollection();serviceCollection.AddLogging(b?=>
{
????Logger?seriLogger?=?new?LoggerConfiguration()
????????//?PLEASE?NOTE?that?we?use?JsonFormatter?as?input?paramter
????????.WriteTo.Console(new?JsonFormatter())
????????.MinimumLevel.Debug()
????????.CreateLogger();
????b.AddSerilog(seriLogger);
});
ServiceProvider?provider?=?serviceCollection.BuildServiceProvider();
var?loggerFactory?=?provider.GetService<ILoggerFactory>();
var?logger?=?loggerFactory.CreateLogger("category");
logger.LogInformation("Hello?{name}",?"world");
這樣就會(huì)得到以下的日志:
{????"Timestamp":"2019-03-24T19:38:54.4833240+08:00",
????"Level":"Information",
????"MessageTemplate":"Hello?{name}",
????"Properties":{"name":"world","SourceContext":"category"}
}
如果還需要 formatter 格式化之后的完整消息,可以在創(chuàng)建 JsonFormatter 時(shí)指定 new JsonFormatter(renderMessage: true) 這樣就會(huì)得到包含完整可讀消息的結(jié)果:
{????"Timestamp":"2019-03-24T19:44:51.0430260+08:00",
????"Level":"Information",
????"MessageTemplate":"Hello?{name}",
????"RenderedMessage":"Hello?\"world\"",
????"Properties":{"name":"world","SourceContext":"category"}
}
這樣,在實(shí)際的操作中。我們可以直接使用 Microsoft.Extension.Logging 默認(rèn)體系對(duì) ILoggerProvider 進(jìn)行擴(kuò)展達(dá)到對(duì)記錄目標(biāo)和記錄格式的控制;也可以將其與 SeriLog 集成。通過 Sink 和 ITextFormatter 組合的方式分別對(duì)記錄目標(biāo)和記錄格式進(jìn)行控制。
總結(jié)
一圖勝千言:
圖-1 默認(rèn) Microsoft.Extension.Logging 類型及信息傳遞路徑
圖-2 集成 SeriLog 后類型及信息傳遞路徑
如果您覺得本文對(duì)您有幫助,也歡迎分享給其他的人。我們一起進(jìn)步。歡迎關(guān)注我的博客(https://clrdaily.com)和微信公眾號(hào):
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 沉思录 - 结构化日志的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《从零开始学ASP.NET CORE M
- 下一篇: 一份.NET 容器化的调查小结