在上一篇中,我們簡單地查看了 Serilog 的整體需求和大體結構。從這一篇開始,本文開始涉及 Serilog 內的相關實現,着重解決第一個問題,即 Serilog 向哪里寫入日志數據的。(系列目錄)
基礎功能
在開始看 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方法暴露出來,如下。
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類包含一些描述日志事件的數據。
public class LogEvent
{
public DateTimeOffset Timestamp { get; }
public LogEventLevel Level { get; }
public Exception Exception { get; }
public MessageTemplate MessageTemplate { get; }
private readonly Dictionary<string, LogEventPropertyValue> _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定義統一的日志記錄接口。該接口如下所示。
public interface ILogEventSink
{
void Emit(LogEvent logEvent);
}
該接口形式簡單,只有一個函數,輸入參數為LogEvent對象,無返回值,這一點和 LogDemo 中的ILogTarget接口很像。如果想實現一個 ConsoleSink,只需要將繼承該接口並將LogEvent對象字符串數據寫入到Console即可。實際上,在 Serilog.Sinks.Console 中其核心功能就是這么實現的。
Logger類
Logger類是對ILogger接口的默認實現。類似於 LogDemo 中的Logger,該類給所有日志記錄的使用提供了 API 方法。考慮到本篇只關心日志向哪里寫入的。因此,我們只關心其內部的部分字段屬性和方法。
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);
}
}
考慮到篇幅,這里我去掉了部分和當前功能無關的代碼,只保留最為核心的代碼。
-
首先,我們看下繼承關系,
Logger類除繼承ILogger之外,還繼承ILogEventSink接口,這個繼承關系看起來很奇怪,但細想也覺得正常,一個日志記錄器不光可以當日志事件的發生器,也可以當其接收器。換而言之,可以將一條日志事件寫到另一個日志記錄器中,由另一個日志記錄器記錄到其他 Sinks 中。此外,該類還繼承了IDisposable接口,按照邏輯需求來講,Logger是沒有東西需要釋放的,其需要釋放的通常是內部包含的一些對象,比如說 FileSink 如果長時間維持一個文件句柄的話,則需要在Logger回收后被動釋放,因此,這導致了Logger需要維護一組待釋放的對象進行釋放。在Logger內部中,通過添加Action函數鈎子的方式進行釋放。 -
之后,我們會發現所有的寫入日志方法直接或間接地調用上面給出的Write方法。在該方法的邏輯中,第一行用來判斷日志的等級是否滿足條件,也就是一類全局的過濾條件,第二行則是判斷是否給出日志的輸出模板。隨后
_messageTemplateProcessor看這個意思是解析模板和數據(暫且不明,不過多關注)。再往下,則是構造對應的LogEvent對象。最后通過Dispatch方法將日志分發到ILogEventSink。在Dispatch中,前半部分邏輯和本篇關系不大,最后通過ILogEventSink將日志消息發送出去。
看到這里,可能會有人好奇一點,Logger應該擁有一組ILogEventSink對象才對,這樣才能夠實現一次向多個 Sink 中寫入日志信息,但Logger只維護一個ILogEventSink對象,它是怎么做到一次向多個 Sink 中寫入日志的呢?我們接着往下看。
功能性 Sink
在 Serilog 的 ./Core/Sinks 文件夾中可以發現,這里面有非常多的ILogEventSink的實現類。這些實現類都不是向具體的媒介(控制台、文件等)寫入日志,反而,他們都是給其他的Sink擴展新功能,典型裝飾模式的一種實現。在這個文件夾下,我把部分核心功能摘錄出來,如下。(v2.10.0又添加了一些其他的裝飾類,這里就不過多說明了)。
class ConditionalSink : ILogEventSink
{
readonly ILogEventSink _warpped;
readonly Func<LogEvent, bool> _condition;
...
public void Emit(LogEvent logEvent)
{
if (_condition(logEvent)) _wrapped.Emit(logEvent);
}
...
}
ConditionalSink功能非常簡單,它也包含了一個ILogEventSink對象,此外,還包含一個Func<LogEvent, bool>的泛型委托。這個委托可以按照LogEvent對象滿足某種指定要求做過濾。從Emit函數內可以看出,只有在滿足條件時才會將日志事件發送到對應的 Sink 中。它可以看成是帶有條件寫入的 Sink,這一點和也就是局部過濾功能實現的核心之處。
public interface ILogEventFilter
{
bool IsEnabled(LogEvent logEvent);
}
FilteringSink所作的事情和ConditiaonalSink一樣,除了 Sink 對象外,它還維護了一組ILogEventFilter數組用來指定多個日志過濾條件,而ILogEventFilter接口如上所示,其內部就是按日志對象進行過濾。而RestrictedSink內除ILogEventSink對象外,還有一個LoggingLevelSwitch對象,這個對象用來描述日志記錄器能夠記錄的最小日志等級,所以RestrictedSink所實現的是依照日志等級的比較判斷是否輸出日志。
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對象並不一定需要釋放,通常由父日志記錄器所創建出來的子日志記錄器不需要釋放,其資源釋放可以由父日志記錄器進行管理。
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的設計負擔。主要體現在兩個方面。
-
通過裝飾模式實現帶有復雜功能的 Sink,通常通過繼承
ILogEventSink並內部保有一個ILogEventSink對象來進行功能擴展,前面所提到的ConditionalSink、FilteringSink、RestrictedSink等都屬於帶有擴展功能的Sink,可以看到,其構造函數均需要外界提供額外的ILogEventSink對象。 此外,這些裝飾類還可以嵌套,即一個裝飾類可以擁有另一個裝飾類對象,實現功能的聚合。 -
通過組合模式將一組 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中的字符串信息的。
