[譯]Serilog Tutorial
在過去的幾年中,結構化日誌已經大受歡迎。而Serilog是 .NET 中最著名的結構化日誌類庫 ,我們提供了這份的精簡指南來幫助你快速了解並運用它。
0. 內容
- 設定目標
- 認識Serilog
- 事件和級別
- 觸發和收集結構化數據
- 為過濾和關聯添加事件標記
- 大海撈針 [Finding needles in the haystack]
- 下一步是什麽?
- 獲得幫助
1. 設定目標
你可能之前已經在項目中使用了Serilog,或者你有一個新的項目希望使用Serilog,又或者你只是對結構化日誌記錄感興趣: 那就非常好! 你來對地方了。
然而,更進一步來說,你的目標可能是:
- 希望在用戶之前發現代碼中的BUG和錯誤
- 為了更快的找到生產環境中的問題
- 深入的了解系統運行表現
Serilog使用json格式來記錄應用程序中的事件,方便我們可以快速的查詢日誌,關鍵是可以方便地過濾和查詢日誌,而不用編寫正則表達式。
在本教程中,我們將介紹最關鍵的幾個部分,幫助我們可以在生成環境中提供令人驚嘆的診斷能力。[註:原文這句挺拗口]
2. 認識 Serilog
那就讓我們開始吧!為了更好的理解,你可以先創建一個新的.Net console 項目,可以是netcore或者傳統的NETFramework版本。
Serilog 通過NuGet分發,項目包括一個Seirlog核心項目Seirlog和很多接收器sinks(超過100個),這些接收是通過插件的方式來實現將日誌寫入到各種終端,文件,郵件,數據庫或日誌服務器
我們將通過使用Serilog和Serilog.Sinks.Console這兩個組件開始,在稍後討論其他選項:
dotnet add package Serilog
dotnet add package Serilog.Sinks.Console
這是世界上最簡單的Serilog配置:
using Serilog; class Program { public static void Main(string[] args) { using (var log = new LoggerConfiguration() .WriteTo.Console() .CreateLogger()) { log.Information("Hello, Serilog!"); log.Warning("Goodbye, Serilog."); } } }
讓我們稍微分解一下:
LoggerConfiguration
類提供一個流式接口用於構建一個日誌記錄管道WriteTo.Console()
將控制臺接收器添加到上述管道中CreateLogger()
組裝並返回一個實現ILogger
接口的Logger
對象- 上述Logger對象同樣實現了IDisposable,所以我們可以在
using
中調用它 - 最後
log.Information()
和log.Warning()
觸發記錄日誌
這個日誌記錄管道是一個可釋放的[disposable],這可能會讓你有點意外,但是請記住,記錄器通常是要寫入文件,數據庫等等: 很多sinks 必須被完全地關閉掉。盡管這樣,也僅僅在應用程序退出前,根logger才需要被釋放。而在應用程序中使用logger是不需要關心這些細節的。
你運行了這個程序嘛?這是你看到的效果吧?
Apart from just passing it around everywhere, there are two possibilities. [ 除了在各地傳遞外,還有兩種可能性。] If you‘re using an IoC container, you might have components receive an ILogger through dependency injection. [ 如果您使用的是IoC容器,則可能會讓組件通過依賴註入來接收ILogger。] Packages like AutofacSerilogIntegration can help with that. [ 像AutofacSerilogIntegration這樣的軟件包可以提供幫助。]
現在最直接的問題是:我們在應用程序的其他類裏面如何獲得這個log
對象,除了到處傳遞之外,還有兩個辦法。
- 如果你使用IoC容器,你可以組件註入一個ILogger對象來接收,像
AutofacSerilogIntegration
的包括幫助你實現這種方式。 - 或者,您可以將Logger對象存儲在眾所周知的位置; Serilog 已經內容內置了一個靜態的Log對象,就像這樣:
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
Log.Information("Hello again, Serilog!");
Log.CloseAndFlush();
Log
類提供所有與ILogger
接口相同的方法,這裏我們顯示調用Log.CloseAndFlush()
來關閉它,而不是使用using
代碼塊
你可以使用依賴註入的方式,也可以是靜態屬性的方式 - 這取決你的個人喜好問題。為了簡單起見,我們在本教程中使用了靜態Log的方式。
也許,你不是在編寫一個控制臺應用程序。我們將使用Console應用作為廣為人知的示例,但是你一旦完成了本教程,您應該查看目標平臺的文檔(例如,ASP.NET Core)。
3. Event and Level [時間和級別]
和一些老的日誌類庫相比(如log4net),在使用Serilog時,你需要做的就是最大改變就是思考日誌事件[log events],而不是日誌消息[log message],一條事件[event]由以下幾個內容組成:
- 事件發生時的時間戳[timestamp]
- 描述何時應該捕獲事件的級別[level]
- 記錄事件的消息[message]內容]
- 描述事件的命名屬性[properties]
- 還可能有一個Exception對象
您可以將日誌事件格式化為控制臺的可讀文本,正如我們在第一個示例中看到的那樣:
11:33:01 [INF] Hello, Serilog!
或者,您可以將相同的事件格式化為JSON並將其發送到遠程日誌服務器:
json {"@t":"2017-11-20T11:33:01.22138","@m":"Hello, Serilog!"}
在背後,應用程序中的日誌語句會創建LogEvent
對象,而連接到管道的接收器[sinks]會知道如何記錄它們。
### Logging levels
Serilog速度很快,但始終構建和記錄詳細的日誌事件會浪費CPU,磁盤和網絡資源。為了管理這個,Serilog事件被賦予了多種級別:Debug
, Information
, Warning
和 Error
等。對應的有一個Log.*()
方法來對應各個級別。
在開發過程中,可能會打開調試級別的事件:
```csharp
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() // <- Set the minimum level
.WriteTo.Console()
.CreateLogger();
// In a tight loop...
Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
```
在生成環境中,通常關閉調試的日誌,並將最小的日誌級別設置成Information
,以便只記錄重要的事件,在這篇文檔中可以獲得有關Serilog Logger Lever的更多信息
Tip: Serilog has special handling for Exception objects; methods like Log.Error() take the exception as the first parameter, e.g. [ 提示:Serilog對Exception對象有特殊的處理;] Log.Error(ex, "Task {TaskName} was canceled", task). [ Log.Error(例如,“任務{任務名稱}被取消”,任務)。] Don‘t include exceptions in the log message itself. [ 不要在日誌消息本身中包含異常。]
提示:Serilog對Exception對象有特殊的處理; 像方法Log.Error()
將 exception 作為第一個參數,例如Log.Error(ex, "Task {TaskName} was canceled", task)
,不要將異常的包括在message消息中
4. 觸發和收集結構化數據
讓我們回到最後一個代碼片段:
var itemNumber = 10;
var itemCount = 999;
Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
您是否註意到日誌消息中的{ItemNumber}
這樣的占位符? 這不是一個C#的內插字符串[Interpolated string C# 6.0的新特性],Log.*()
方法接收一個消息模板,另外一種.NET格式化字符串,除了支持傳統的{0}
的方式,還支持{Name}
的方式。
這看起來有點奇怪,除非您意識到通過這樣做,Serilog可以將這些消息的一部分作為類的屬性與人性化的文本一起捕獲:
{
"@t": "2017-11-20T11:33:01.22138",
"@l": "Debug",
"@m": "Processing item 10 of 999",
"ItemNumber": 10,
"ItemCount": 999
}
我們為什麽要這樣做?因為通過這種有趣的字段插入方式,並作為屬性記錄到事件日誌中,我們可以立即使用優雅的簡單的過濾器來查找事件,就像ItemNumber > 900
,而無需通過正則表達式從消息中提取了。
進一步,我們可以使用 @
結構捕獲運算符 來獲取不僅僅是平坦的屬性值,而是完整的對象:
var user = new { Name = "Nick", Id = "nblumhardt" };
Log.Information("Logged on user {@User}", user);
在這裏,user
對象被捕獲,並生成的JSON中,以便我們可以使用查詢來查找事件,如:User.Id = ‘nblumhardt‘
{
"@t": "2017-11-20T11:33:01.22138",
"@m": "Logged on user {\"Name\": \"Nick\", \"Id\": \"nblumhardt\"}",
"User": {"Name": "Nick", "Id": "nblumhardt"}
}
生產環境的監控和調試已經非常困難和耗時,而且經常是壓力山大的任務:而通過將相關的數據放在手邊,Serilog除去了運維操作相關活動的最大難題之一。
Tip: 從Visual Studio Marketplace安裝令人驚嘆的Serilog Analyzer,可以幫助你檢查你的消息模板類型( 註:這個插件還能幫你通過配置代碼生成appsetting.json的內容,但是只支持生成一級配置:( )
這實際上有多大的差異取決於你如何收集Serilog的事件。一般來說,日誌事件進入文本文件並用grep
進行搜索。Serilog也可以記錄文本文件,但不能在記事本中執行ItemNumber> 900
,因此您需要評估更強大的工具來執行此操作。
寫 JSON 格式的日誌文件
如果您的需求很簡單,您可以將JSON寫入日誌文件,並使用支持JSON的工具直接查詢文件。] [ Serilog的文件接收器[sink]和緊湊的JSON格式化類庫[compact JSON formatter]使第一部分變得簡單。 讓我們重新建一個控制臺應用程序,並安裝下列軟件包:
dotnet add package Serilog
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Formatting.Compact
在Main
函數中插入:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(new CompactJsonFormatter(), "log.clef")
.CreateLogger();
var itemCount = 99;
for (var itemNumber = 0; itemNumber < itemCount; ++itemNumber)
Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
Log.CloseAndFlush();
運行這個程序將產生使用Serilog的緊湊格式[compact],在文件log.clef中生成以換行符分隔的JSON
流,如果沒有使用CompactJsonFormatter
,則會創建一個簡單餓扁平日誌文件。
如果你在文本編輯器中打開文件,你會看到JSON事件,就像我們上面使用的例子。
CLEF-tool 是查詢CLEF格式的日誌文件的方便的命令行應用程序:(註:貌似只支持windows)
註意第二行的過濾器ItemNumber> 95
:毫不費力地在大型日誌流中定位事件,就是結構化日誌記錄的好處吧。
將日誌寫入日誌服務器
將日誌事件從多個應用程序發送到中央服務器或日誌聚合服務非常方便,而不是試圖在生產環境中多個日誌進行渾水摸魚[shuffle log files]
日誌服務器通常通過HTTP/S或UDP在網絡上接收事件,並為開發人員和操作員工程師提供用戶界面,以便在出現問題時搜索和分析日誌流。
Serilog接收器[sinks]支持大量的日誌服務器,其中許多具有結構化數據支持。
註:這段是廣告就不翻譯了,讀者可以根據實際需求選擇自己的日誌服務器。
5. 為過濾和關聯標記事件
我們剛剛看到消息模板如何實現我們傳統上認為可以有效搜索和分析的日誌“消息”。
結構化日誌記錄的另一方面是通過某種因果關系或空間關聯來識別相關事件集合。事件觸發:[Events raised: ]
- 在處理單個HTTP請求期間
- 從特定的機器,應用程序,服務或組件
- 關於單個客戶,訂單或交易
- 起因於事件的因果鏈
Serilog通過enrichment來處理所有這些情況(以及其他情況)。Enrichment只是為事件添加附加屬性,而不是源自消息模板的屬性
添加[Enriching]特定的屬性
最簡單的enrichment方法將固定屬性值添加到源自日誌記錄管道的所有事件,可以通過Enrich.WithProperty()
方法快速實現
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Application", "Demo")
.WriteTo.Seq("http://localhost:5341") //Seq 日誌服務器
.CreateLogger();
在LogEvents
上,通過enrichment添加的屬性看起來與源自消息模板的屬性相同
json { "@t": "2017-11-20T11:33:01.22138", "@l": "Debug", "@m": "Processing item 10 of 999", "ItemNumber": 10, "ItemCount": 999, "Application": "Demo" }
豐富特殊的屬性
通過創建和使用上下文記錄器,可以將屬性添加到一個或幾個相關事件中,而不是增加具有相同值的所有事件
var orderLog = Log.ForContext("OrderId", order.Id);
orderLog.Information("Looking up product codes");
// ...
orderLog.Information("Product lookup took {Elapsed} ms", elapsed.TotalMilliseconds);
在這裏,通過orderLog
發出的兩個事件都會附加一個OrderId
屬性。
Enrichmen
是附加的:如果應用程序屬性設置在管道級別,則上面的第二個事件將攜帶Elapsed
(來自消息),OrderId
(來自上下文記錄器)和Application
(來自記錄管道)。
豐富消息源類型信息
記錄器特定的enrichment一個特例是關於如何使用創建它們的類標記事件
在名為HomeController
的類中,使用以下命令創建類型特定的記錄器:
private readonly ILogger _log = Log.ForContext<HomeController>();
通過_log
發出的事件將攜帶一個值為MyApp.Controllers.HomeController
的SourceContext
屬性。
充分利用上下文
為了豐富工作單元中所有事件[為所有事件添加特定屬性],Serilog
提供了LogContext
,首先需要使用Enrich.FromLogContext()
在LoggerConfiguration
級別啟用:
Log.Logger = new LoggerConfiguraition()
.Enrich.FromLogContext()
// ...
LogContext可以被認為是一堆(key,value)
鍵值對;
using (LogContext.PushProperty("MessageId", message.Id))
{
Log.Debug("Dispatching message of type {MessageType}", message.GetType());
await handler.HandleAsync(message);
}
關於LogContext有趣的是沒有什麽需要對象需要傳遞。在示例代碼中,HandleAsync()
以及由它調用的任何其他方法的實現可以直接使用Log
和ILogger
- MessageId屬性將自動T並添加LogEvent
中。
Tip: LogContext
是一個堆棧;推送到堆棧上的屬性必須通過釋放從PushProperty()返回的對象, -- 上述通過手動使用using
塊的方式
已經存在的Enricher
所有enrichment API都是使用Enricher
的實現Serilog的ILogEventEnricher
接口的對象來實現的。
NuGet中為線程細節,機器信息和用戶詳細信息等內容提供了一些有趣的預先構建的Enricher實現。
Serilog.Enrichers.Thread 通過 Enrich.WithThreadId() 來添加線程ID相關的擴展:
Log.Logger = new LoggerConfiguration()
.Enrich.WithThreadId()
// ...
這將為事件附加一個ThreadId
屬性,以便交錯事件可以再次分開。
我們將在下一節中看到一個簡單的例子,說明如何編寫和插入自己的應用程序專用的Enricher
程序。
6. 大海撈針 Finding needles in the haystack
如果我們已經知道如何使用Serilog調用消息模板和enrichment結構化日誌的兩個支柱,那麽第三個支柱就是隱式事件類型的概念。
結構化日誌適合有效處理大量日誌數據。關於大型日誌流的一個有趣的觀察是,真實產生的事件比編寫日誌語句代碼塊時要多的多(註:這算什麽發現)
Log.Debug("Processing item {ItemNumber} of {ItemCount}", itemNumber, itemCount);
這意味著,盡管生成了許多獨特的消息字符串,如"Processing item 31 of 4159"
,但由此日誌記錄語句生成的每個事件共享相同的消息模板,即"Processing item {ItemNumber} of {ItemCount}"
Serilog及其許多sinks 利用這一事實從根本上改進了查詢和過濾日誌事件的體驗。如果消息模板與事件一起保存,則下面的過濾器可以立即從嘈雜的日誌記錄語句中排除成千上萬的事件,從而更容易看到否則會被淹沒的有趣事件:
@MessageTemplate <> 'Processing item {ItemNumber} of {ItemCount}'
反轉也適用 - 放大事件類型可以從單個日誌記錄語句中查找所有事件
如何利用此功能取決於您存儲和搜索日誌的位置。接下來我們會看看細節。
Tip:字符串鏈接,C#內插字符串,以及其他技術手段來預格式化傳遞給Serilog的消息內容,會取消此功能。詳細請看 How (not) to parameterize Serilog events
隱式事件類型
存儲,然後過濾羅嗦的消息模板並不總是理想的。 相反,通常從消息模板創建一個數字哈希值,並將其與事件一起存儲:
事件類型 enrichment
日誌文件和本地不支持消息模板的日誌服務器仍然可以通過在Serilog管道中明確地enricher
事件來接收事件類型。
為此,自定義enricher
程序將EventType
屬性附加到每個事件
// Install-Package Murmurhash
class EventTypeEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var murmur = MurmurHash.Create32();
var bytes = Encoding.UTF8.GetBytes(logEvent.MessageTemplate.Text);
var hash = murmur.ComputeHash(bytes);
var numericHash = BitConverter.ToUInt32(hash, 0);
var eventType = propertyFactory.CreateProperty("EventType", numericHash);
logEvent.AddPropertyIfAbsent(eventType);
}
}
當插入管道時,這會使{EventType}
屬性可用於sinks
Log.Logger = new LoggerConfiguration()
.Enrich.With<EventTypeEnricher>()
.WriteTo.Console(outputTemplate:
"{Timestamp:HH:mm:ss} [{EventType:x8} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
WriteTo.Console()
的參數是一個Serilog輸出模板,描述了如何將日誌事件的屬性格式化為文本。 大多數基於文本的sinks(包括Serilog.Sinks.File
)都可以接受輸出模板來指導格式化。
[ 輸出如下所示:]
7. 下一步做什麽
Serilog是一個強大的庫,擁有廣泛的插件和工具生態系統。我們只涉及絕對的基礎知識 - 取決於您希望如何使用Serilog以及您使用的框架和平臺,還有很多可以發現的地方。
這裏有一些文章和擴展,供你參考:
- Debugging and diagnostics - if you‘re having trouble getting Serilog or a sink to work, check out this page on the Serilog wiki
appsettings.json
configuration - in this article we‘ve only shown the C# configuration API; Serilog.Settings.Configuration adds support for logger configuration in .NET Core JSON settings- XML
<appSettings>
configuration - Serilog.Settings.AppSettings adds support for reading logger configuration from .NET Framework configuration files - ASP.NET Core integration - the Serilog.AspNetCore package seamlessly integrates Serilog into ASP.NET Core 2.0 apps to take advantage of the structured log events emitted by the framework
- ASP.NET integration - check out SerilogWeb.Classic for a quick-and-painless way to record unhandled exceptions and request timings from ASP.NET projects
- Smart logging middleware for ASP.NET Core - improve the quality of request logging in ASP.NET Core with the middleware from this article
- Timed operations - SerilogTimings is a small wrapper for Serilog that makes it easy to log operation timings
- Autofac-Serilog integration - use AutofacSerilogIntegration to inject Serilog ILoggers through Autofac with type information automatically added
- Code analysis for Serilog - mentioned earlier, Serilog Analyzer checks message template syntax in Visual Studio as-you-type, and detects many potential Serilog usage issues
- Dynamic filtering - Serilog.Filters.Expressions makes it possible to filter events using a simple domain-specific language
- Async wrapper - the latency of logging to files or the console can be reduced further using the Serilog.Sinks.Async package
- Sink READMEs - most sinks, including Serilog.Sinks.File, Serilog.Sinks.Console, Serilog.Sinks.Seq and others, have good README documents in their GitHub repositories with detailed instructions for using the sink
- Structured Logging Concepts in .NET series - this blog series on structured logging has more detail on much of what we‘ve covered in this tutorial
- F# support - if your application is written in F#, Destructurama.FSharp will let you log F# types seamlessly through Serilog
- JSON.NET support - Destructurama.JsonNet extends Serilog to allow JSON.NET dynamic objects to be logged as structured data
- Exception enricher - Serilog.Exceptions collects additional exception-type-specific information and attaches it to log events
- Async stack trace unmangling - Serilog.Enrichers.Demystify plugs in Ben Adams‘ Demystifier to improve the readability of exception stack traces
8. 獲得幫助
Serilog有三大優秀的社區支持渠道:
- Stack Overflow - 如果您有Seri??log使用問題,Stack Overflow上的Serilog標簽是一個很好的開始; 它被積極監控,並且您將通過留下一個容易找到的答案來幫助其他人解決同一個問題
- Gitter Chat - 如果您的問題不符合Stack Overflow格式,或者您只是想完善檢查方法,那麽Gitter聊天室是與Serilog社區中的其他人聯系的快捷方式
- GitHub Issues - 最後,如果你發現了一個bug或者想要對Serilog進行改進,GitHub就是這個地方; Serilog organization 包括了serilog所有的核心庫和問題跟蹤。
Happy logging!
原文地址http://blog.getseq.net/serilog-tutorial/
[譯]Serilog Tutorial