Serilog 原始碼解析——資料的儲存(中)
阿新 • • 發佈:2020-11-19
[上一篇](https://www.cnblogs.com/iskcal/p/saving-of-log-data-1.html)文章中揭露了日誌資料的繫結邏輯,主要說明了日誌資料繫結的結果資訊,即`EventProperty`結構體和`LogEventProperty`類,以及日誌資料與具名屬性Token的繫結類`PropertyBinder`。在本文中,我們主要對`PropertyValueConverter`類及其涉及的其他相關類進行說明。關注的重點在如何利用具名屬性 Token 以及對應的日誌資料來構造對應的`LogEventPropertyValue`類物件。([系列目錄](https://www.cnblogs.com/iskcal/p/introduction-to-the-source-code-of-serilog.html#目錄))
## `PropertyValueConverter`類
`PropertyValueConverter`類是一個非常複雜的類。Serilog將其分成兩個程式碼檔案儲存,分別為`./Capturing/PropertyValueConverter.cs`以及`./Capturing/DepthLimiter.cs`檔案。先看下`PropertyValueConverter`有什麼欄位和屬性。
```csharp
partial class PropertyValueConverter : ILogEventPropertyFactory, ILogEventPropertyValueFactory
{
readonly IDestructuringPolicy[] _destructuringPolicies;
readonly IScalarConversionPolicy[] _scalarConversionPolicies;
readonly DepthLimiter _depthLimiter;
readonly int _maximumStringLength;
readonly int _maximumCollectionCount;
readonly bool _propagateExceptions;
...
}
```
好傢伙,一次性來了一大堆第一次見的玩意。一個個看,一個個猜,先弄懂大意再說。
## `ILogEventPropertyFactory`和`ILogEventPropertyValueFactory`介面
首先就是`ILogEventPropertyFactory`和`ILogEventPropertyValueFactory`介面,從`Factory`名稱大概猜出來,屬於工廠模式的一種設計,是構造對應的`LogEventProperty`和`LogEventPropertyValue`物件。這些介面位於`./Core`資料夾內部,表明是非常重要的介面。
```csharp
public interface ILogEventPropertyValueFactory
{
LogEventPropertyValue CreatePropertyValue(object value, bool destructureObjects = false);
}
public interface ILogEventPropertyFactory
{
LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false);
}
```
從函式簽名上看,基本和猜測一致,用於構建對應的物件,最後一個輸入引數指明是否以解構物件的方式進行構建。
## `IDestructuringPolicy`和`IScalarConversionPolicy`介面
在`PropertyValueConverter`類中,還保有對`IDestructuringPolicy`和`IScalarConversionPolicy`介面陣列的引用。字面意義上,這兩個介面均描述的是一個策略,且儲存在`./Core`資料夾下。這兩個介面有什麼用,做什麼,先看下程式碼吧。
```csharp
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`類的內部。換句話來說,就是`DepthLimiter`是`PropertyValueConveter`的內部類。
```csharp
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`介面。然而,`DepthLimiter`和`PropertyValueConverter`不同之處在於:
1. `DepthLimiter`只負責對`LogEventPropertyValue`的建立,而`PropertyValueConveter`不僅負責前者的構建,還負責對`LogEventProperty`的建立,這一點從所繼承的介面數目可以看出來。也就是說,`PropertyValueConverter`的作用範圍比`DepthLimiter`大。
2. `DepthLimiter`類不負責具體的構建,這一點可以從其包含`_propertyValueConverter`欄位可以看出來,具體對日誌資料的渲染還是交給內部的`PropertyValueConverter`類物件來處理。那麼`DepthLimiter`負責什麼呢?它負責記錄處理的深度,這一點從`_currentDepth`這個變數可以看出來。
考慮這樣一個數據:
```csharp
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通過`PropertyValueConverter`和`DepthLimiter`的相互引用配合達到遞迴轉換的目的。也就是說,`DepthLimiter`負責維護遞迴轉換時當前轉換資料所處的深度。
## 剩餘引數
在`PropertyValueConverter`中,還剩下一些較為簡單的資料引數。在瞭解了`DepthLimiter`之後,剩餘的三個引數可以很明顯看出來。
- `_maximumStringLength`:指的是構造日誌資訊字串的最大長度
- `_maximumCollectionCount`: 指的是日誌資料集合解析的最大個數
- `_propgateExceptions`: 該值是一個bool型別,表示在儲存日誌資料的過程中,若發生了異常,則相關異常是否被丟擲。
## 介面函式
接下來,我們把注意力再回到`PropertyValueConverter`類中對`IDestructuringPolicy`和`IScalarConversionPolicy`介面函式實現的部分。
```csharp
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行)。下面是該函式的核心部分程式碼。
```csharp
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`類的進一步裝飾,讓資料的解析添加了深度資訊。在下一篇中,我們進一步來看`IScalarConversionPolicy`、`IDestructuringPolicy`以及它們的實