1. 程式人生 > >Serilog 原始碼解析——Sink 的實現

Serilog 原始碼解析——Sink 的實現

在[上一篇](https://www.cnblogs.com/p/usage-and-structure-of-serilog)中,我們簡單地查看了 Serilog 的整體需求和大體結構。從這一篇開始,本文開始涉及 Serilog 內的相關實現,著重解決第一個問題,即 Serilog 向哪裡寫入日誌資料的。([系列目錄](https://www.cnblogs.com/iskcal/p/introduction-to-the-source-code-of-serilog.html#目錄)) ## 基礎功能 在開始看 Serilog 怎麼將日誌記錄到 Sinks 之前,先看下整體框架。首先,我們需要了解 Serilog 中最常用的一個介面`ILogger`,它提供了對外記錄日誌的所有功能 API 方法。 ### `ILogger`(核心介面) 在 Serilog 根目錄下,儲存有 4 個程式碼檔案。類似於 LogDemo,`ILogger`內包含各種功能API方法,`LogConfiguration`用於構建對應的`ILogger`物件。另外,`LogExtensions`是向`ILogger`中新增新方法,不是`LogConfiguration`。 。 為了方便,我們首先看如何使用,在理解完使用方法,再回過頭來看怎麼建立。首先是`ILogger`, 它提供了大量的使用方法,按照功能主要分成以下三類。 | 方法名 | 說明 | | ---- | ---- | | ForContext系列 | 構造子日誌記錄物件,並新增額外資料 | | Write系列,XXX(日誌等級名)系列 | 日誌記錄功能 | | BindXXX 系列 | 輸出模板、屬性繫結相關 | 這裡面的方法,對我們而言,第二類方法是用的最多地,我們就先看 Serilog 是如何記錄日誌的吧。 ### `Log`(靜態方法類) 這是一個靜態類,可以看到內部本質上是對`ILogger`的進一步包裝,並將所有API方法暴露出來,如下。 ```csharp public static class Log { static ILogger _logger = SilentLogger.Instance; public static Logger { get => _logger; set => _logger = value ?? throw ... } public static void Write(LogEventLevel level, string messageTemplate) ... } ``` 順帶提一句,類庫中的`SilentLogger`類是對`ILogger`的一個空實現,它可以看成是一個具有呼叫功能的空類。 在瞭解到了最為核心的`ILogger`介面後,接下來需要了解的是描述日誌事件的`LogEvent`類,該類在 Events 資料夾下,其作為`Write`的輸入引數,可以將其想象成LogDemo中的`LogData`類,只不過它包含了更多的資料資訊。另外,`LogEventLevel`是一個列舉,同樣位於 Events 資料夾下,該類的內容和 LogDemo 中的`LogLevel`完全一致。 ### `LogEvent`(日誌事件類) 在 Serilog 中,每當我們發生一次日誌記錄的行為時,Serilog 都將其封裝到一個類中方便使用,即`LogEvent`類。和 LogDemo 中的`LogData`一樣,`LogEvent`類包含一些描述日誌事件的資料。 ```csharp public class LogEvent { public DateTimeOffset Timestamp { get; } public LogEventLevel Level { get; } public Exception Exception { get; } public MessageTemplate MessageTemplate { get; } private readonly Dictionary _properties; internal LogEvent Copy() { ... } } ``` 可以看到,在LogEvent中,有若干欄位和屬性描述一個日誌事件。`Timestamp`屬性描述日誌記錄的時間,採用`DateTimeOffset`這一型別可以統一不同時區下的伺服器時間點,確保時間上的統一。`Level`就不用多說,描述日誌的等級。`Exception`屬性可以儲存任意異常類資料,該屬性常用在 Error 和 Fatal 等級中,需要儲存異常資訊時使用。至於後續的`MessageTemplate`和`LogEventPropertyValue`,從字面意義上看,屬於字串訊息模板和記錄資料時所用到,目前我們主力研究記錄到 Sink 的處理邏輯,故這兩塊暫時不關心。 此外,在`LogEvent`類中,有一個很特別的函式,名為`Copy`函式,這個函式是根據當前`LogEvent`物件複製出了一個相同的`LogEvent`物件。這個方法可以看成是設計模式中原型模式的一種實現,只不過這個類沒有利用`IClonable`介面來實現。 # Core 目錄下的功能類 ### `ILogEventSink`介面 在 LogDemo 中,我們通過`ILogTarget`介面定義不同的日誌記錄目的地。類似地,在 Serilog 中,所有的 Sink 通過`ILogEventSink`定義統一的日誌記錄介面。該介面如下所示。 ```csharp public interface ILogEventSink { void Emit(LogEvent logEvent); } ``` 該介面形式簡單,只有一個函式,輸入引數為`LogEvent`物件,無返回值,這一點和 LogDemo 中的`ILogTarget`介面很像。如果想實現一個 ConsoleSink,只需要將繼承該介面並將`LogEvent`物件字串資料寫入到`Console`即可。實際上,在 Serilog.Sinks.Console 中其核心功能就是這麼實現的。 ### `Logger`類 `Logger`類是對`ILogger`介面的預設實現。類似於 LogDemo 中的`Logger`,該類給所有日誌記錄的使用提供了 API 方法。考慮到本篇只關心日誌向哪裡寫入的。因此,我們只關心其內部的部分欄位屬性和方法。 ```csharp public sealed class Logger : ILogger, ILogEventSink, IDisposable { readonly ILogEventSink _sink; readonly Action _dispose; readonly LogEventLevel _minimumLevel; // 361行到375行 public void Write(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues) { if (!IsEnabled(level)) return; if (messageTemplate == null) return; if (propertyValues != null && propertyValues.GetType() != typeof(object[])) propertyValues = new object[] {propertyValues}; // 解析日誌模板 _messageTemplateProcessor.Process(messageTemplate, propertyValues, out var parsedTemplate, out var boundProperties); // 構造日誌事件物件 var logEvent = new LogEvent(DateTimeOffset.Now, level, exception, parsedTemplate, boundProperties); // 將日誌事件分發出去 Dispatch(logEvent); } public void Dispatch(LogEvent logEvent) { ... // 將日誌事件交給Sink進行記錄 _sink.Emit(logEvent); } } ``` 考慮到篇幅,這裡我去掉了部分和當前功能無關的程式碼,只保留最為核心的程式碼。 1. 首先,我們看下繼承關係,`Logger`類除繼承`ILogger`之外,還繼承`ILogEventSink`介面,這個繼承關係看起來很奇怪,但細想也覺得正常,一個日誌記錄器不光可以當日志事件的發生器,也可以當其接收器。換而言之,可以將一條日誌事件寫到另一個日誌記錄器中,由另一個日誌記錄器記錄到其他 Sinks 中。此外,該類還繼承了`IDisposable`介面,按照邏輯需求來講,`Logger`是沒有東西需要釋放的,其需要釋放的通常是內部包含的一些物件,比如說 FileSink 如果長時間維持一個檔案控制代碼的話,則需要在`Logger`回收後被動釋放,因此,這導致了`Logger`需要維護一組待釋放的物件進行釋放。在`Logger`內部中,通過新增`Action`函式鉤子的方式進行釋放。 2. 之後,我們會發現所有的寫入日誌方法直接或間接地呼叫上面給出的Write方法。在該方法的邏輯中,第一行用來判斷日誌的等級是否滿足條件,也就是一類全域性的過濾條件,第二行則是判斷是否給出日誌的輸出模板。隨後`_messageTemplateProcessor`看這個意思是解析模板和資料(暫且不明,不過多關注)。再往下,則是構造對應的`LogEvent`物件。最後通過`Dispatch`方法將日誌分發到`ILogEventSink`。在`Dispatch`中,前半部分邏輯和本篇關係不大,最後通過`ILogEventSink`將日誌訊息傳送出去。 看到這裡,可能會有人好奇一點,`Logger`應該擁有一組`ILogEventSink`物件才對,這樣才能夠實現一次向多個 Sink 中寫入日誌資訊,但`Logger`只維護一個`ILogEventSink`物件,它是怎麼做到一次向多個 Sink 中寫入日誌的呢?我們接著往下看。 # 功能性 Sink 在 Serilog 的 ./Core/Sinks 資料夾中可以發現,這裡面有非常多的`ILogEventSink`的實現類。這些實現類都不是向具體的媒介(控制檯、檔案等)寫入日誌,反而,他們都是給其他的Sink擴充套件新功能,典型裝飾模式的一種實現。在這個資料夾下,我把部分核心功能摘錄出來,如下。(v2.10.0又添加了一些其他的裝飾類,這裡就不過多說明了)。 ```csharp class ConditionalSink : ILogEventSink { readonly ILogEventSink _warpped; readonly Func _condition; ... public void Emit(LogEvent logEvent) { if (_condition(logEvent)) _wrapped.Emit(logEvent); } ... } ``` `ConditionalSink`功能非常簡單,它也包含了一個`ILogEventSink`物件,此外,還包含一個`Func`的泛型委託。這個委託可以按照`LogEvent`物件滿足某種指定要求做過濾。從Emit函式內可以看出,只有在滿足條件時才會將日誌事件傳送到對應的 Sink 中。它可以看成是帶有條件寫入的 Sink,這一點和也就是區域性過濾功能實現的核心之處。 ```csharp public interface ILogEventFilter { bool IsEnabled(LogEvent logEvent); } ``` `FilteringSink`所作的事情和`ConditiaonalSink`一樣,除了 Sink 物件外,它還維護了一組`ILogEventFilter`陣列用來指定多個日誌過濾條件,而`ILogEventFilter`介面如上所示,其內部就是按日誌物件進行過濾。而`RestrictedSink`內除`ILogEventSink`物件外,還有一個`LoggingLevelSwitch`物件,這個物件用來描述日誌記錄器能夠記錄的最小日誌等級,所以`RestrictedSink`所實現的是依照日誌等級的比較判斷是否輸出日誌。 ```csharp sealed class SecondaryLoggerSink : ILogEventSink { readonly ILogger _logger; readonly bool _attemptDispose; ... public void Emit(LogEvent logEvent) { ... var copy = logEvent.Copy(); _logger.Write(copy); } } ``` 和上述其他的`ILogEventSink`的繼承類相比,`SecondaryLoggerSink`在其內部並沒有保留對某個`ILogEventSink`的引用。相反,它保留對給定的`ILogger`物件的引用,這種好處是我們可以讓一個日誌記錄器作為另一個日誌記錄的`Sink`。該類另外的一個變數`_attemptDispose`表示該類是否需要執行內部`ILogger`物件的釋放,之所以這樣做是因為有的時候`Logger`物件並不一定需要釋放,通常由父日誌記錄器所創建出來的子日誌記錄器不需要釋放,其資源釋放可以由父日誌記錄器進行管理。 ```csharp class SafeAggregateSink : ILogEventSink { readonly ILogEventSink[] _sinks; ... public void Emit(LogEvent logEvent) { foreach (var sink in _sinks) { ... sink.Emit(logEvent); ... } } } ``` 除此之外,還剩下`AggregrateSink`和`SafeAggregrateSink`這兩個 Sink 也繼承`ILogEventSink`介面,且內部都引用了`ILogEventSink`陣列,且在`Emit`函式中基本都是對陣列內的`ILogEventSink`物件遍歷,並呼叫這些物件內的`Emit`函式。二者均在`Emit`函式內將所有異常捕捉起來,但`AggregateSink`會在捕捉後將這些異常以`AggreateException`異常再次丟擲。這兩個類與之前的類不同,它們將多個 Sink 集合起來,讓外界仍以單一的 Sink 來使用。其好處在於,`Logger`的設計者不需要關注到底有一個還是多個 Sink,如果有多個 Sink,只需要用這兩個類將多個 Sink 包裹起來,外界將這一組 Sink 當成一個 Sink 來使用。 為什麼要這樣設計?實際上,對`Logger`類來說,它並不需要關心記錄的 Sink 有一個還是多個,是什麼樣的狀態,達到什麼樣的條件才能記錄,畢竟這些都非常的複雜。對於`Logger`來講,它要做的只有一件事,只要將日誌事件向`ILogEventSink`物件中發出即可。為達到這樣的目的,Serilog 利用設計模式中的裝飾模式和組合模式來降低`Logger`的設計負擔。主要體現在兩個方面。 1. 通過裝飾模式實現帶有複雜功能的 Sink,通常通過繼承`ILogEventSink`並內部保有一個`ILogEventSink`物件來進行功能擴充套件,前面所提到的`ConditionalSink`、`FilteringSink`、`RestrictedSink`等都屬於帶有擴充套件功能的Sink,可以看到,其建構函式均需要外界提供額外的`ILogEventSink`物件。 此外,這些裝飾類還可以巢狀,即一個裝飾類可以擁有另一個裝飾類物件,實現功能的聚合。 2. 通過組合模式將一組 Sink 以單一 Sink 物件的方式暴露出來,`AggregrateSink`和`SafeAggregrateSink`做的就是這件事。就算`Logger`需要將日誌記錄到多個Sink中,從`Logger`的角度來看,它也只是寫入到一個`ILogEventSink`物件中,這讓`Logger`設計者不需要為了到底是一個還是多個 Sink 而頭疼。舉個例子,假如你有一個 ConsoleSink,它的作用是將日誌輸出到控制檯,以及一個將日誌輸出到檔案的 FileSink。如果想利用`Logger`物件將日誌同時輸出到控制檯和檔案,我們只需要構建一個`AggregateSink`並將 ConsoleSink 和 FileSink 物件放置到其內部的陣列中,再將`AggregrateSink`作為`Logger`中的`ILogEventSink`的物件,那麼`Logger`能自動將日誌分別記錄到這兩個地方。 # 總結 以上就是整個 Sink 功能的說明,可以看到的是,這塊和之前提到的 LogDemo 專案非常的像。我相信如果在之前對 LogDemo 能夠理解的人在這塊能夠找到非常熟悉的感覺。從下一篇開始,我將開始揭露 Serilog 是如何將 LogEvent 這樣的日誌事件轉換成最終寫入到各個Sink中的字串信