Serilog 源碼解析——Sink 的實現


上一篇中,我們簡單地查看了 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 等級中,需要保存異常信息時使用。至於后續的MessageTemplateLogEventPropertyValue,從字面意義上看,屬於字符串消息模板和記錄數據時所用到,目前我們主力研究記錄到 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);
    }
}

考慮到篇幅,這里我去掉了部分和當前功能無關的代碼,只保留最為核心的代碼。

  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又添加了一些其他的裝飾類,這里就不過多說明了)。

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);
            ...
        }
    }
}

除此之外,還剩下AggregrateSinkSafeAggregrateSink這兩個 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對象來進行功能擴展,前面所提到的ConditionalSinkFilteringSinkRestrictedSink等都屬於帶有擴展功能的Sink,可以看到,其構造函數均需要外界提供額外的ILogEventSink對象。 此外,這些裝飾類還可以嵌套,即一個裝飾類可以擁有另一個裝飾類對象,實現功能的聚合。

  2. 通過組合模式將一組 Sink 以單一 Sink 對象的方式暴露出來,AggregrateSinkSafeAggregrateSink做的就是這件事。就算Logger需要將日志記錄到多個Sink中,從Logger的角度來看,它也只是寫入到一個ILogEventSink對象中,這讓Logger設計者不需要為了到底是一個還是多個 Sink 而頭疼。舉個例子,假如你有一個 ConsoleSink,它的作用是將日志輸出到控制台,以及一個將日志輸出到文件的 FileSink。如果想利用Logger對象將日志同時輸出到控制台和文件,我們只需要構建一個AggregateSink並將 ConsoleSink 和 FileSink 對象放置到其內部的數組中,再將AggregrateSink作為Logger中的ILogEventSink的對象,那么Logger能自動將日志分別記錄到這兩個地方。

總結

以上就是整個 Sink 功能的說明,可以看到的是,這塊和之前提到的 LogDemo 項目非常的像。我相信如果在之前對 LogDemo 能夠理解的人在這塊能夠找到非常熟悉的感覺。從下一篇開始,我將開始揭露 Serilog 是如何將 LogEvent 這樣的日志事件轉換成最終寫入到各個Sink中的字符串信息的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM