1. 程式人生 > 實用技巧 >Serilog 原始碼解析——資料的儲存(中)

Serilog 原始碼解析——資料的儲存(中)

上一篇文章中揭露了日誌資料的繫結邏輯,主要說明了日誌資料繫結的結果資訊,即EventProperty結構體和LogEventProperty類,以及日誌資料與具名屬性Token的繫結類PropertyBinder。在本文中,我們主要對PropertyValueConverter類及其涉及的其他相關類進行說明。關注的重點在如何利用具名屬性 Token 以及對應的日誌資料來構造對應的LogEventPropertyValue類物件。(系列目錄

PropertyValueConverter

PropertyValueConverter類是一個非常複雜的類。Serilog將其分成兩個程式碼檔案儲存,分別為./Capturing/PropertyValueConverter.cs

以及./Capturing/DepthLimiter.cs檔案。先看下PropertyValueConverter有什麼欄位和屬性。

partial class PropertyValueConverter : ILogEventPropertyFactory, ILogEventPropertyValueFactory
{
    readonly IDestructuringPolicy[] _destructuringPolicies;
    readonly IScalarConversionPolicy[] _scalarConversionPolicies;
    readonly DepthLimiter _depthLimiter;
    readonly int _maximumStringLength;
    readonly int _maximumCollectionCount;
    readonly bool _propagateExceptions;
    ...
}

好傢伙,一次性來了一大堆第一次見的玩意。一個個看,一個個猜,先弄懂大意再說。

ILogEventPropertyFactoryILogEventPropertyValueFactory介面

首先就是ILogEventPropertyFactoryILogEventPropertyValueFactory介面,從Factory名稱大概猜出來,屬於工廠模式的一種設計,是構造對應的LogEventPropertyLogEventPropertyValue物件。這些介面位於./Core資料夾內部,表明是非常重要的介面。

public interface ILogEventPropertyValueFactory
{
    LogEventPropertyValue CreatePropertyValue(object value, bool destructureObjects = false);
}

public interface ILogEventPropertyFactory
{
    LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false);
}

從函式簽名上看,基本和猜測一致,用於構建對應的物件,最後一個輸入引數指明是否以解構物件的方式進行構建。

IDestructuringPolicyIScalarConversionPolicy介面

PropertyValueConverter類中,還保有對IDestructuringPolicyIScalarConversionPolicy介面陣列的引用。字面意義上,這兩個介面均描述的是一個策略,且儲存在./Core資料夾下。這兩個介面有什麼用,做什麼,先看下程式碼吧。

public interface IDestructuringPolicy
{
    bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result);
}
public interface IScalarConversionPolicy
{
    bool TryConvertToScalar(object value, out ScalarValue result);
}

從這兩個介面內的函式簽名可以猜測出它們分別將日誌資料轉化成對應的資料型別。bool返回值標註該轉換是否成功,輸入引數中被 out 修飾的變數則可以看成是轉換成功後的結果變數。以此,可以發現,IDestrucuringPolicy介面用於將資料轉換成LogEventPropertyValue物件,而IScalarConversionPolicy介面則將資料轉換成ScalarValue

DepthLimiter

DepthLimiter類物件是ProertyValueConverter所持有的最後一個複雜的類物件。有意思的是,該類的作用範圍放在PropertyValueConverter類的內部。換句話來說,就是DepthLimiterPropertyValueConveter的內部類。

class DepthLimiter : ILogEventPropertyValueFactory
{
    [ThreadStatic]
    static int _currentDepth;
    readonly int _maximumDestructingDepth;
    readonly PropertyValueConverter _propertyValueConverter;

    public static void SetCurrentDepth(int depth)
    {
        _currentDepth = depth;
    }
    public LogEventPropertyValue CreatePropertyValue(object value, Destructuring destructuring)
    {
        var storedDepth = _currentDepth;

        var result = DefaultIfMaximumDepth(storedDepth) ??
            _propertyValueConverter.CreatePropertyValue(value, destructuring, storedDepth + 1);

        _currentDepth = storedDepth;

        return result;
    }
    ...
}

可以看到,DepthLimiter也是一個處理日誌訊息資料與LogEventPropertyValue相繫結的過程類。和PropertyValueConverter類一樣,它也繼承於ILogEventPropertyValueFactory介面。然而,DepthLimiterPropertyValueConverter不同之處在於:

  1. DepthLimiter只負責對LogEventPropertyValue的建立,而PropertyValueConveter不僅負責前者的構建,還負責對LogEventProperty的建立,這一點從所繼承的介面數目可以看出來。也就是說,PropertyValueConverter的作用範圍比DepthLimiter大。
  2. DepthLimiter類不負責具體的構建,這一點可以從其包含_propertyValueConverter欄位可以看出來,具體對日誌資料的渲染還是交給內部的PropertyValueConverter類物件來處理。那麼DepthLimiter負責什麼呢?它負責記錄處理的深度,這一點從_currentDepth這個變數可以看出來。

考慮這樣一個數據:

class A
{
    public B B { get; }
}

class B
{
    public int C { get; }
}

資料A具有非常複雜的形式,A 中有 B 的屬性,B 內有int型別的C屬性,一共三層。最外層為 A,最內層為 C。如果採用解構的方式記錄資料 A 的物件,那麼需要深入 3 層迭代轉換。比如說C資料應該放在ScalarValue中,B和A應該放在StructValue中。Serilog通過PropertyValueConverterDepthLimiter的相互引用配合達到遞迴轉換的目的。也就是說,DepthLimiter負責維護遞迴轉換時當前轉換資料所處的深度。

剩餘引數

PropertyValueConverter中,還剩下一些較為簡單的資料引數。在瞭解了DepthLimiter之後,剩餘的三個引數可以很明顯看出來。

  • _maximumStringLength:指的是構造日誌資訊字串的最大長度
  • _maximumCollectionCount: 指的是日誌資料集合解析的最大個數
  • _propgateExceptions: 該值是一個bool型別,表示在儲存日誌資料的過程中,若發生了異常,則相關異常是否被丟擲。

介面函式

接下來,我們把注意力再回到PropertyValueConverter類中對IDestructuringPolicyIScalarConversionPolicy介面函式實現的部分。

public LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false)
{
    return new LogEventProperty(name, CreatePropertyValue(value, destructureObjects));
}

public LogEventPropertyValue CreatePropertyValue(object value, bool destructureObjects = false)
{
    return CreatePropertyValue(value, destructureObjects, 1);
}

可以看到,無論是哪種介面,其內部直接或間接在呼叫CreatePropertyValue(object value, Destructuring destructuring, int depth)函式(類檔案中第126行)。下面是該函式的核心部分程式碼。

LogEventPropertyValue CreatePropertyValue(object value, Destructuring destructuring, int depth)
{
    if (value == null)
      return new ScalarValue(null);
    ...
    // ScalarValue的轉換策略
    foreach (var scalarConversionPolicy in _scalarConversionPolicies)
    {
        if (scalarConversionPolicy.TryConvertToScalar(value, out var converted))
            return converted;
    }

    // 設定深度
    DepthLimiter.SetCurrentDepth(depth);
    // 如果Token採用解構渲染,則使用解構策略嘗試解析資料
    if (destructuring == Destructuring.Destructure)
    {
        foreach (var destructuringPolicy in _destructuringPolicies)
        {
            if (destructuringPolicy.TryDestructure(value, _depthLimiter, out var result))
                return result;
        }
    }
    // 獲取日誌資料的資料型別,利用內建的解析策略對其解析
    var valueType = value.GetType();
    if (TryConvertEnumerable(value, destructuring, valueType, out var enumerableResult))
        return enumerableResult;
    if (TryConvertValueTuple(value, destructuring, valueType, out var tupleResult))
        return tupleResult;
    if (TryConvertCompilerGeneratedType(value, destructuring, valueType, out var compilerGeneratedResult))
        return compilerGeneratedResult;
    // 如果以上策略都不滿足的話,直接構造
    return new ScalarValue(value.ToString());
}

這個函式非常複雜,但大體上分成以下幾個步驟。

  1. 如果傳入的資料是一個null物件,則直接使用ScalarValue構造。
  2. 利用內部儲存的IScalarConversionPolicy陣列嘗試對日誌資料繫結,如果能繫結,則將結果返回,否則繼續。
  3. 設定深度值,之所以將深度值放在這裡設定,是因為後續操作可能會用到這個深度值。
  4. 利用內部維護的IDestructingPolicy陣列嘗試對日誌資料繫結,如果能繫結,則返回結果值,否則繼續。
  5. 獲取當前日誌資料的型別,並採用3個內建的繫結規則進行繫結。如果能成功,則返回結果值,如果所有都無法成功,則認為該物件無法在現有的規則下繫結,則採用ScalarValue對其繫結。

第一步好理解,如果值為空,則直接採用ScalarValue對其渲染,ScalarValue會將其渲染成null。之所以不返回null物件,則是嘗試對null操作呼叫函式等操作會丟擲異常,而如果在呼叫前編寫判斷的邏輯會大大幹擾程式設計師編寫程式碼的邏輯。因此,直接使用new ScalarValue(null)會更加方便,不易出錯。

第二步則是利用多個IScalarConversionPolicy策略類將傳入的日誌物件資料嘗試轉換成ScalarValue物件,一旦某個策略能夠成功轉換,那麼直接跳出。

第三步和第四步的作用是嘗試利用IDestructuringPolicy策略類對輸入的日誌資料進行轉換。該部分將日誌資料按照策略的要求嘗試轉換成LogEventPropertyValue類物件,和之前一樣,一旦某個策略成功,則直接跳出。Serilog中定義了一組相關策略,其類程式碼儲存在./Policies資料夾中。

最後,如果以上所有策略都沒法滿足時,則嘗試採用內建的策略。

總結

總的來說,當記錄日誌時,其所附帶的日誌資料通過PropertyValueConverter類物件將其轉化成一系列的LogEventPropertyValue物件,最終變成LogEventProperty物件。這些物件有著不同的語義,在渲染的行為方式上表現有所不同,比如說ScalarValue表示的一個原始資料,它採用ToString的方法對資料進行渲染;SequenceValue表示一類陣列,採用[]方式對其渲染;DictionaryValue表示一組鍵值對,採用{}方式對其渲染;StructureValue表示一個複雜且需要解構的日誌資料物件,採用{}方式渲染。除此之外,PropertyValueConverter還包含一個內部類DepthLimiter,該類是對PropertyValueConverter類的進一步裝飾,讓資料的解析添加了深度資訊。在下一篇中,我們進一步來看IScalarConversionPolicyIDestructuringPolicy以及它們的實現類。