天天看點

.NET Core下的日志(2):日志模型詳解一、Logger 二、LoggerProvider三、LoggerFactory

NET

Core的日志模型主要由三個核心對象構成,它們分别是Logger、LoggerProvider和LoggerFactory。總的來說,LoggerProvider提供一個具體的Logger對象将格式化的日志消息寫入相應的目的地,但是我們在程式設計過程中使用的Logger對象則由LoggerFactory建立,這個Logger利用注冊到LoggerFactory的LoggerProvider來提供真正具有日志寫入功能的Logger,并委托後者來記錄日志。

目錄 一、Logger     擴充方法LogXxx與BeginScope     Logger<TCategoryName> 二、LoggerProvider 三、LoggerFactory     Logger提供的同一性     Logger類型     LoggerFactory類型     依賴注入

日志模型的Logger泛指所有實作了ILogger接口的所有類型以及對應對象,該接口定義在NuGet包“Microsoft.Extensions.Logging.Abstractions”中,這個NuGet包同時定義了分别代表LoggerProvider和LoggerFactory的接口ILoggerProvider和ILoggerFactory。ILogger接口中定義了如下三個方法Log、IsEnabled和BeginScope。

Logger對日志消息的寫入實作在Log方法中。Log方法的logLevel代表寫入日志消息的等級,而日志消息的原始内容通過參數state和exception這兩個參數來承載承載,前者代表一個原始的日志條目(Log

Entry),後者代表與之關聯的異常。日志在被寫入之前必須格式成一個字元串,由于日志原始資訊分别由一個Object和Exception對象對象來表示,是以日志的“格式化器”自然展現為一個Func<object,

Exception, string>類型的委托對象。

一條寫入的日志消息會關聯着一個日志記錄事件,後者則通過一個EventId對象來辨別,Log方法的eventId參數類型就是EventId。如下面的代碼片段所示,EventId被定義成一個結構,它具有兩個基本的屬性Id和Name,前者代表必需的唯一辨別,後者則是一個可選的名稱。除此之外,整形到EventId類型之間還存在一個隐式類型轉換,是以在需要使用EventId對象的地方,我們可以使用一個整數來代替。

對于任意一次日志消息寫入請求,Logger并不會直接調用Log方法将日志消息寫入對應的目的地,它會根據提供日志消息的等級判斷是否應該執行寫入操作,判斷的邏輯實作在IsEnabled方法中,隻有當這個方法傳回True的時候它的Log方法才會被執行。

在預設的情況下,每次調用Logger的Log方法所進行的日志記錄操作都是互相獨立的,但是有時候我們需要将相關的多次日志記錄做一個邏輯關聯,或者說我們需要為多次日志記錄操作建立一個共同的上下文範圍。這樣一個關聯上下文範圍可以通過BeginScope<TState>方法來建立,該方法将該上下文範圍與參數state表示的對象進行關聯。被建立的這個關聯上下文展現為一個IDisposable對象,我們需要調用其Dispose方法将其釋放回收,也就是說被建立的關聯上下文的生命周期終止于Dispose方法的調用。

當我們調用Logger的Log方法記錄日志時必須指定日志消息采用的等級,出于調用便利性考慮,日志模型還為ILogger接口定義了一系列針對不同日志等級的擴充方法,比如LogDebug、LogTrace、LogInformation、LogWarning、LogError和LogCritical等。下面的代碼片段列出了整個日志等級Debug三個LogDebug方法重載的定義,針對其他日志等級的擴充方法的定義與之類似。對于這些擴充方法來說,如果它們沒有定義表示日志事件ID的參數eventId,預設使用的事件ID為0。

對于定義在ILogger接口中的Log方法來說,原始日志消息的内容通過Object類型的參數state和Exception類型的參數exception來承載,并通過一個Func<object,

Exception,

string>類型的委托對象來将它們格式化成可以寫入的字元串。上述這些擴充方法對此作了簡化,它利用一個包含占位符的字元串模闆(對應參數message)和用于替換占位符的參數清單(對應參數args)來承載原始的日志消息,日志消息的格式化展現在如何使用提供的參數替換模闆中相應的占位符進而生成一個完整的消息。值得一提的是,定義在模闆中的占位符通過花括号括起來,可以使用零基連續整數(比如“{0}”、“{1}”和“{2}”等),也可以使用任意字元串(比如“{Minimum}”和“Maximum”等)。

定義在ILogger接口的泛型方法BeginScope<TState>為多次相關的日志記錄操作建立一個相同的執行上下文範圍,并将其上下文範圍與一個TState對象進行關聯。ILogger接口還具有如下一個同名的擴充方法,它采用與上面類似的方式将建立的上下文範圍與一個字元串進行關聯,該字元串是指定的模闆與參數清單格式化後的結果。

每條日志消息都關聯着一個具體的類型(Category),這個類型實際上建立這條日志消息的“源”,我們一般将日志記錄所在的應用或者元件名稱作為類型。除了ILogger這個基本的接口,日志模型中還定義了如下一個泛型的ILogger

<TCategoryName>接口,它派生與ILogger接口并将泛型參數的類型名稱作為由它寫入的日志消息的類型。

Logger<TCategoryName>實作了ILogger

<TCategoryName>接口。一個Logger<TCategoryName>對象可以視為是對另一個Logger對象的封裝,它使用泛型參數類型來确定寫入日志的類型,而采用這個内部封裝的Logger對象完成具體的日志寫入操作。如下面的代碼片段所示,Logger<TCategoryName>的構造函數接受一個LoggerFactory作為輸入參數,上述的這個内部封裝的Logger對象就是由它建立的。

在利用指定的LoggerFactory建立Logger對象時,泛型參數TCategoryName的類型被用來計算日志類型。對于具有簡寫形式的基元類型(比如Int32、Boolean和Decimal等)來說,類型的簡寫形式(比如int、bool和decimal等)直接作為日志類型名稱。對于一般的類型來說,日志類型名稱就是該類型的全名(命名空間+類型名)。如果該類型内嵌于另一個類型之中(比如“Foo.Bar+Baz”),表示内嵌的“+”需要替換成“.”(比如“Foo.Bar.Baz”)。如果該類型是一個泛型類型(比如Foobar<T1,T2>),泛型參數部分将不包含在日志類型名稱中(日志類型為“Foobar”)。

除了調用構造函數建立一個Logger<TCategoryName>對象之外,我們還可以調用針對ILoggerFactory接口的擴充方法CreateLogger<T>來建立它。如下面的代碼片段所示,除了這個CreateLogger<T>方法之外,另一個CreateLogger方法直接指定一個Type類型的參數,雖然傳回類型不同,但是由此兩個方法建立的Logger在日志記錄行為上是等效的。

日志模型的LoggerProvider泛指所有實作了接口ILoggerProvider的類型和對應的對象,從其命名我們不難看出LoggerProvider的目的在于“提供”真正具有日志寫入功能的Logger。如下面的代碼片段所示,ILoggerProvider繼承了IDisposable,如果某個具體的LoggerProvider需要釋放某種資源,可以将相關的操作實作在Dispose方法中。

LoggerProvider針對Logger的提供實作在唯一的方法CreateLogger中,該方法的參數categoryName自然代表上面我們所說的日志消息的類型。這個CreateLogger方法傳回類型為ILogger,代表根據指定日志類型建立的Logger對象。

從命名的角度來講,LoggerProvider和LoggerFactory最終都是為了提供一個Logger對象,但是兩者提供的Logger對象在本質上是不同的。一個LoggerProvider一般針對某種具體的日志目的地類型(比如控制台、檔案或者Event

Log等)提供對應的Logger,而LoggerFactory僅僅為我們建立日志程式設計所用的那個Logger對象。

日志模型中的LoggerFactory泛指所有實作了ILoggerFactory接口的所有類型及其對應的對象。如下面的代碼片段所示,ILoggerFactory具有兩個簡單的方法,針對Logger的建立實作在CreateLogger方法中。我們通過調用AddProvider方法将某個LoggerProvider對象注冊到LoggerFactory之上,CreateLogger方法建立的Logger需要利用這些注冊的LoggerProvider來提供真正具有日志寫入功能的Logger對象,并借助後者來完成對日志的寫入操作。

日志模型中定義了一個實作了ILoggerFactory接口的類型,這就是我們在上面示範執行個體中使用的LoggerFactory類,由它建立的是一個類型為Logger的對象,這兩個類型均定義在NuGet包“Microsoft.Extensions.Logging”之中。到目前為止,我們認識了日志模型中的三個接口(ILogger、ILoggerProvider和ILoggerFactory)和其中兩個的實作者(Logger和LoggerFactory),右圖所示的UML展現了它們之間的關系。

.NET Core下的日志(2):日志模型詳解一、Logger 二、LoggerProvider三、LoggerFactory

上圖所示的UML基本上展現了Logger和LoggerFactory這兩個類型的實作邏輯,這個邏輯我們在上面已經提到過多次,現在我們通過代碼實作的方式來對它做進一步地說明。在這之前,我們有必要了解LoggerFactory類型建立Logger過程中所展現出的一個重要特性,即對于CreateLogger方法的多次調用,如果我們指定的日志類型(categoryName參數)相同(不區分大小寫),該方法傳回的實際是同一個對象。

如上面的代碼片段所示,我們利用同一個LoggerFactory對象針對相同的日志類型(“App”)先後得到三個Logger對象,雖然這三個Logger被建立的時候LoggerFactory具有不同的狀态(注冊到它上面的LoggerProvider逐次增多),但是它們其實是同一個對象。換句話說,LoggerFactory和由它建立的Logger對象并不是兩個孤立的對象,它們之間存在着一種動态的關聯,當LoggerFactory自身的狀态發生改變時(注冊新的LoggerProvider),它會主動改變Logger的狀态使之與自身同步。

我們定義了一個精簡版本的同名類型來模拟真實Logger類的實作邏輯。如下面的代碼片段所示,我們建立一個Logger對象的時候需要指定建立它的LoggerFactory對象和日志類型。它的字段loggers代表由它封裝的一組具有真正日志寫入功能的Logger對象,它們由注冊到LoggerFactory的LoggerProvider(展現為LoggerFactory的LoggerProviders屬性)來提供。

IsEnabled方法實作了針對等級的日志過濾,如果指定的日志等級能夠通過任一Logger的過濾條件,該方法就傳回True。至于真正用于實作日志消息記錄的Log方法,它隻需要調用每個Logger對象的同名方法即可。除此之外,Logger類還定義了一個AddProvider方法,它利用指定的LoggerProvider來建立對應的Logger,并将後者添加到封裝的Logger清單中。一旦新的LoggerProvider注冊到LoggerFactory之上,LoggerFactory正是調用這個方法将新注冊的LoggerProvider應用到由它建立的Logger對象之上。

一個Logger對象是對一組具有真正日志寫入功能的Logger對象的封裝,由它的BeginScope方法建立的日志上下文範圍則是對這組Logger建立的上下文範圍的封裝。當這個日志上下文範圍因調用Dispose方法被釋放的時候,這些内部封裝的上下文範圍同時被釋放。如下所示的代碼基本展現了定義在BeginScope方法中建立日志上下文範圍的邏輯。

我們同樣采用最精簡的代碼來模拟實作在LoggerFactory類型中的Logger建立邏輯。如下面的代碼片段所示,處于線程安全方面的考慮,我們定義了一個ConcurrentBag<ILoggerProvider>類型的屬性LoggerProviders來儲存注冊到LogggerFactory上的LoggerProvider。另一個ConcurrentDictionary<string,

Logger>類型的字段loggers則用來儲存自身建立的Logger對象,該對象的Key表示日志消息類型。

當LoggerFactory的CreateLogger方法的時候,如果根據指定的日志類型能夠在loggers字段表示的字典中找到一個Logger對象,則直接将它作為傳回值。隻有在根據指定的日志類型找不到

對應的Logger的情況下,LoggerFactory才會真正去建立一個新的Logger對象,并在傳回之前将它添加到該字典之中。針對相同的日志類型,LoggerFactory之是以總是傳回同一個Logger,根源就在于此。

對于用于注冊LoggerProvider的AddProvider方法來說,LoggerFactory除了将指定的LoggerProvider添加到LoggerProviders屬性表示的清單之中,它還會調用每個已經建立的Logger對象的AddProvider方法。正是源于對這個方法的調用,我們新注冊到LoggerFactory上的LoggerProvider才會自動應用到所有已經建立的Logger對象中。

LoggerProvider類型都實作了IDisposable接口,針對它們的Dispose方法的調用被放在LoggerFactory的同名方法中。換句話說,當LoggerFactory被釋放的時候,注冊到它之上的所有LoggerProvider會自動被釋放。

在一個真正的.NET

Core應用中,架構内部會借助ServiceProvider以依賴注入的形式向我們提供用于建立Logger對象的LoggerFactory。這樣一個ServiceProvider在根據一個ServiceCollection對象建構之前,我們必然需要在後者之上實施針對LoggerFactory的服務注冊,這樣的服務注冊可以通過針對接口IServiceCollection的擴充方法AddLogging來完成。

如上面的代碼片段所示,擴充方法AddLogging除了以Singleton模式注冊了ILoggerFactory接口與實作它的LoggerFactory類型之間的映射之外,還以同樣的模式注冊了ILogger<>接口和Logger<>類型的映射。如果建立ServiceProvider的ServiceCollection具有這兩個服務注冊,我們可以利用ServiceProvider直接提供一個Logger<T>,而不需要間接地利用ServiceProvider提供的LoggerFactory來建立它。下面的代碼片段展示了Logger<T>的這兩種建立方式。

作者:蔣金楠

微信公衆賬号:大内老A

如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識别二維碼)關注個人公衆号(原來公衆帳号蔣金楠的自媒體将會停用)。

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

<a href="http://www.cnblogs.com/artech/p/inside-net-core-logging-2.html" target="_blank">原文連結</a>