当前位置:网站首页>Serilog原始碼解析——使用方法

Serilog原始碼解析——使用方法

2020-11-06 20:10:10 itread01

在上兩篇文章([連結1](https://www.cnblogs.com/iskcal/p/implementation-of-the-log-demo-1.html)和[連結2](https://www.cnblogs.com/iskcal/p/implementation-of-the-log-demo-2.html))中,我們通過一個簡易 demo 瞭解到了一個簡單的日誌記錄類庫所需要的功能,即一條日誌有哪些資料,以及如何通過一次記錄的方式將同一條日誌訊息記錄到多個日誌媒介中。在本文中,針對 Serilog,我們從以下幾個方面來了解 Serilog 核心功能需求和用法,併為下一篇正式開始探究原始碼準備相關工作。([系列目錄](https://www.cnblogs.com/iskcal/p/introduction-to-the-source-code-of-serilog.html#目錄)) ## Serilog 核心功能 目前,在 Asp.net core 中,對於日誌記錄庫,除了微軟官方準備的 Microsoft.Extensions.Logging 外,Serilog 也算是一個最常用的日誌記錄的類庫。作為一類最常用的日誌記錄庫,Serilog 具有良好的擴充套件性,其組織所維護的60多個專案均是為 Serilog 提供額外功能的擴充套件類庫。為更好地瞭解原始碼具體在做什麼,我們需要對 Serilog 有一個最基本的功能瞭解。 ### 1. Serilog 將日誌資訊記錄到哪裡? Serilog 將日誌記錄媒介稱之為 Sink,在 Serilog 組織維護的類庫中,有各種各樣的 Sink,比如說寫入控制檯的 ConsoleSink 以及寫入檔案的 FileSink 等。這些 Sink 的包名通常命名為 Serilog.Sinks.XXX。 那麼 Serilog 如何向多個 Sinks 中寫入日誌呢?下面給出一個向控制檯和檔案寫入日誌的例子。在此之前,我們需要向所在專案新增三個包: - Serilog - Serilog.Sinks.Console - Serilog.Sinks.File 新增完三個包之後,通過以下呼叫方式即可將日誌寫入到控制檯和檔案中。 ```csharp var log = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File("./log.txt") .CreateLogger(); log.Information("Hello world."); log.Error("Hello world, again."); ``` 如果大家對之前的 Demo 瞭解的話,那麼會覺得這段程式碼非常的熟悉。`LoggerConfiguration`類似於先前的`LogBuilder`,通過`CreateLogger`函式來建立對應的日誌記錄器,呼叫`Console`和`File`函式類似於先前的`AddConsole`以及`AddFile`方法,其輸入引數的個數和形式都完全一樣。但不同的是這些函式在`WriteTo`物件上呼叫而不是`LoggerConfiguration`上做呼叫,這一點和先前的 Demo 不一樣,不過這一點並不影響我們的理解。再往後,日誌的記錄是通過`Information`和`Error`函式來呼叫,而LogDemo 中採用`LogInformation`和`LogError`函式記錄日誌。 ![Serilog輸出到控制檯上的日誌資訊](https://img2020.cnblogs.com/blog/1077681/202009/1077681-20200901222348617-1132924867.jpg) 上圖顯示的是 Serilog 向控制檯中記錄的日誌資訊,可以看到,其最終記錄的日誌資訊和 LogDemo 差不多,均是日誌時間+等級+訊息。 ### 2. Serilog 向 Sink 中具體寫入了什麼? 在之前的 LogDemo 中,我們一直認為日誌訊息本質上就是一字串。將日誌記錄下來就是將相關資訊組合成字串並寫入到對應 Sink 中,這是非結構化日誌記錄庫常用的做法。然而,這種使用方式有兩個問題: **2.1 日誌訊息不能附帶資料。日誌訊息附帶資料有非常多的好處,比如說,如果類庫具有自動解析資料的能力,那麼我們只需要給出資料以及帶有插入位置的訊息字串模板,就可以由類庫自行構造對應的日誌。在 Serilog 中,這種帶有資料的日誌訊息被稱為日誌事件,它包含了待解析的字串模板以及需要渲染的資料。** 下面就取官網的例子做說明吧。可以看出,`position`是一個匿名類物件,它包含經度和緯度兩個值。在使用日誌記錄的時候,向函式中傳入三個引數,第一個是字串模板,後兩個是資料。其格式遵循[Message Template](https://messagetemplates.org/)定義,字串模板的寫法非常像C#中的$開頭的字串,字串採用`{}`來標記資料的位置,採用`:`分割變數名和資料輸出格式,二者的區別幾乎只有開頭有沒有$。另外,在字串模板中的變數名部分,還可以使用@和$來決定資料的渲染方法(即如何將資料內容寫入到字串中)。@採用的是解構方法,將內部內容取出直到基本型別後寫入,$則是直接呼叫資料的`ToString`方法渲染。在下例中,`position`採用解構的方式渲染,而整數`elapseMs`利用`000`格式化字串控制其渲染方法。 ```csharp var position = new { Latitude = 25, Longitude = 134 }; var elapsedMs = 34; var log = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); log.Information("Processed {@Position} in {Elapsed:000} ms.", position, elapsedMs); // 輸出: [20:54:34 INF] Processed {"Latitude": 25, "Longitude": 134} in 034 ms. ``` **2.2 不同的人有不同的日誌記錄方式。舉個例子,日誌所包含的時間、等級和訊息不同的人希望採用不同的格式輸入。可以發現,在之前 Demo 中,通過`LogData`類給定的`Tostring()`方法轉化不利於不同人的定製化需求。** 對於這個需求,如果大家對前一個問題充分了解的話,可以發現,日誌事件、日誌等級以及日誌訊息都可以算是日誌事件中的資料,我們可以通過設定輸出模板(output template)達到。實際上,在 Serilog 中,大部分 Sink 都提供了一個預設的輸出模板,通過提供自定義的輸出模板可以達到日誌資訊定製化的目的。 ```csharp var log = new LoggerConfiguration() .WriteTo.Console(outputTemplate: "({Timestamp:HH:mm:ss}/{Level}) {Message:lj}{NewLine}{Exception}") .CreateLogger(); log.Information("Hello world."); // 輸出: (21:22:14/Information) Hello world. ``` 這裡通過設定`outputTemplate`輸入引數來控制日誌的輸出格式。輸出模板的改變會導致日誌的輸出內容發生變化,但可以看到其資料內容是一樣的。 **2.3 在完成前兩個需求後,通過結合這二者,我們可以提供一個新的功能。即向日志中新增其他的自定義資料並將其渲染到日誌中。** 這個功能非常的方便,比如說,我們在日誌記錄的時候還需要記錄當前上下文的使用者名稱稱。一種簡單的辦法是將使用者名稱稱放在訊息字串中,但這樣處理的方法會在每次記錄一條日誌都需要手動填寫相關資料和模板。更好的一種操作是,將使用者名稱稱這個資料項放在日誌事件中,就像日誌時間和等級一樣,在合適的位置自動記錄而不是每次呼叫。 ```csharp var log = new LoggerConfiguration() .Enrich.WithProperty("User", "Dave") .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{User}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); log.Information("Hello world."); // 輸出: [21:44:04 INF] [Dave] Hello world. ``` 在 Serilog 中,這種向日志事件中新增資料的行為叫 Enrichment,對應資料物件是 Enricher。Enrichment 是 Serilog 在資料方面一個具有強大擴充套件性的功能,通過向 Serilog 日誌記錄時塞入新資料,並在日誌模板中進行使用,可以大大降低了呼叫時程式碼的重複性,且減少了遺漏的可能。比如說,有的人希望每次日誌記錄都記錄當前執行的執行緒資訊、程序資訊以及環境變數資料等,通過新增相應的 Enricher 可以達到無需過多關心這些值而直接記錄。甚至,像這些較為常用的 Enrichers,官方組織早已給出了相應的擴充套件包: - Serilog.Enrichers.Thread:附帶當前處理的程序資訊 - Serilog.Enrichers.Process:附帶當前處理的執行緒資訊 - Serilog.Enrichers.Environment:附帶當前的環境資訊 ### 3. Serilog 該不該記錄這條日誌 對於一條日誌記錄,很多時候,我們並不是要求每條都記錄下來,往往是需要丟棄一些日誌。這看起來似乎挺反直覺的,資料是重要的,不應該隱式地丟棄某些資料,但是,在實際應用中這樣的需求確實是合理的,有時候我們僅希望記錄最為重要的日誌而不是全部的日誌資訊。 日誌的過濾有兩種形式,一種是在將日誌記錄到各個 Sink 前需要過濾一遍,這種通常是全域性過濾。另一種則是每個 Sink 物件有其自己的過濾方式,通常是區域性過濾。這裡通過兩個例子說明。 #### 全域性過濾 全域性過濾應用場景是日誌記錄器會記錄海量的日誌,通常大部分是等級非常低的日誌,這類日誌往往在開發時候有用,執行期間不應該再輸出出來。這種情況下,我們需要設定最小日誌輸出等級為Information即可。其使用方式如下: ```csharp var log = new LoggerConfiguration() .WriteTo.Console() .MinimumLevel.Information() .CreateLogger(); log.Debug("Test here."); // 沒有任何輸出 ``` 另外,全域性的過濾條件也可以很複雜,甚至我們可以將之前的 Enricher 結合在一起,比如說,在原先帶有使用者名稱的 Enricher 中,我們希望只記錄用戶名為 Lily 的日誌,其他使用者名稱都不記錄。這裡`ForContext`也是一種新增 Enricher 的方法,和之前不同的是,它將 Enricher 新增到子`Logger`中,即所新增的資料只有`log1`和`log2`有,`log`中並沒有。 ```csharp var log = new LoggerConfiguration() .Filter.ByIncludingOnly(Matching.WithProperty("User", "Lily")) // 新增過濾條件 .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{User}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); var log1 = log.ForContext("User", "Lily"); log1.Information("Log successed."); // 輸出 var log2 = log.ForContext("User", "Dave"); log2.Information("Log failed."); // 不輸出 ``` #### 區域性過濾 同樣地,Serilog 允許我們將日誌的過濾條件從全域性設定縮小到對某個 Sink 的過濾,即只有指定的 Sink 具有過濾日誌的功能。 ```csharp var log = new LoggerConfiguration() .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug) .WriteTo.File("log.txt") .CreateLogger(); log.Information("Hello world."); // 在Console中沒有記錄,在File中被記錄 ``` ### 4. Serilog中的日誌記錄器的配置 從上述幾個問題中可以看到,所有的日誌記錄器都是通過`LogConfiguration`物件的相關屬性執行配置後通過`CreateLogger`方法而生成的。因為在之前都已經或多或少提到,這裡就不細談了。值得一提的是 Serilog 不光提供了程式碼文字的設定方法(即通過呼叫某個函式執行設定),還提供了一套通過配置字串的設定方法,這種方法較為動態,且不需要寫固定的程式碼流程而只需要提供相關配置檔案,具體處理流程在後續再提。 ## Serilog 原始碼準備 好了,終於開始接觸 Serilog 的原始碼了。這部分主要準備好原始碼,以便為了後續的學習。 ### 準備原始碼 Serilog 的地址為 https://github.com/Serilog/Serilog 。我們開啟 Windows terminal,通過下面命令將其下載下來。 ```powershell git clone https://github.com/Serilog/Serilog ``` 下載完成後我們就可以看到Serilog資料夾。然後命令列進入該資料夾。 ```powershell cd ./Serilog ``` Serilog原始碼的預設分支在dev上,這個分支主要是開發的版本,主要用於開發新功能以及修復bug,其變動通常會比較大,不利於學習。通常我們採用發行的最新版本去看,這裡採用2.9.0版本,Serilog已經為其打上標籤,我們只要切換過去就可以了。 > 截止到文章發出為止,Serilog已經出了2.10.0版本,不過因為當時還未更新,我看的是2.9.0版本,兩個版本差距不大。 ```powershell git chekckout v2.9.0 ``` 之後,需要還原包,編譯程式碼,Serilog 的開發人員編寫了一個檔案幫助我們編譯這個專案,在 windows 下執行 build.ps1 檔案,在 Linux 下執行 build.sh 檔案即可。 ```powershell ./build.ps1 ``` > 可能有部分人會遇到無法執行的情況,可能需要修改 powershell 指令碼的執行許可權。 build.ps1 檔案不光會還原專案所需的包,還會重新執行一遍測試程式碼,檢測會不會有錯。考慮Serilog是多平臺(.net framwork 以及 .net core),在驗證測試時,如果沒有相應的執行框架,也會報錯,不過這個沒有關係,只要有其中一個就可以了,我們不是修改原始碼,閱讀原始碼只要能在一個框架上執行就可以了。本系列主要關注的是 .net core 上的程式碼流程。 以上步驟全部完成後,就可以利用 vs 開啟研究了。 ### 專案架構 通過 vs 開啟 Serilog.sln 檔案後,整個專案如下所示。 ![Serilog 原始碼結構](https://img2020.cnblogs.com/blog/1077681/202009/1077681-20200903232912298-943125532.jpg) 可以發現,其結構比較清晰。 - assert資料夾:一般稱為資產資料夾,這裡通常儲存的是一些說明性檔案以及配置檔案。比如說README.md、build.ps1以及build.sh等,這裡和本系列沒有太多關係,可以忽略。 - src資料夾:src是source的簡寫,裡面儲存的是Serilog的原始碼檔案,是本次研究的重點,基本上大多工作都在這裡面進行。 - test資料夾:裡面儲存的是針對原始碼的測試功能程式碼檔案。該資料夾下包含3個專案,Serilog.PerformanceTests應該是Serilog效能測試的專案,Serilog.Tests應該是原始碼功能測試專案,最後的TestDummies是為相關測試準備的資料類和功能類。考慮到測試程式碼使用了測試框架,該部分不是本文重點,因而對這塊不會過多涉及。當然,如果大家對軟體測試有所瞭解,測試程式碼能夠幫你快速理解某些函式的具體功能。 接下來,我們看下 src 內具有有些什麼。Serilog 組織者對這部分的程式碼維護較為仔細,基本上一個資料夾負責一個功能,這一點和之前的 LogDemo 一樣,因此大部分資料夾可以一眼看出大概負責什麼功能。 + 根目錄:根目錄包含四個檔案,這點和LogDemo差不多,從LogDemo的結構和之前的使用經驗可以猜出來,`ILogger`是核心功能介面,`Log`是靜態類,它有著類似於`ILogger`的呼叫方法,`LogConfiguration`和`LogBuilder`一樣,專門用來構造`ILogger`的物件,而`LogExtensions`則是擴充套件方法。 + Core目錄:從名字上可以猜的出來大概是 Serilog 專案最為核心的處理邏輯。 + Events目錄:在 Serilog 中,日誌的記錄不叫日誌訊息而叫日誌事件,Events 內部儲存的應該是和描述日誌事件相關的結構,類似於 LogDemo 中的 Data 資料夾。 + Confiuration目錄:從名字以及`LogConfiguration`上可以看出,它內部應該是用於設定相關配置功能。 + Debugging目錄:用於除錯功能。 + Filters目錄:用於設定過濾器。 + Properties目錄:這個目錄很多專案裡面都有,裡面儲存了`AssemblyInfo`類,該類主要用於描述當前程式集的一些相關資訊,沒有太大作用,可以忽略。 # 總結 今天這篇文章到這裡就結束了,本文主要講述了兩個內容,一個是 serilog 的需求分析,它需要有哪些功能。另一個則是 Serilog 專案的原始碼,做了極其淺顯的猜測和分析,為後面的分析提供基礎。從下一篇開始,我們就正式進入專案的源

版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1604664062.html