Serilog 源碼解析——數據的保存(中)
- 2020 年 11 月 19 日
- 筆記
- c#, 源碼解析, 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;
...
}
好傢夥,一次性來了一大堆第一次見的玩意。一個個看,一個個猜,先弄懂大意再說。
ILogEventPropertyFactory和ILogEventPropertyValueFactory介面
首先就是ILogEventPropertyFactory和ILogEventPropertyValueFactory介面,從Factory名稱大概猜出來,屬於工廠模式的一種設計,是構造對應的LogEventProperty和LogEventPropertyValue對象。這些介面位於./Core文件夾內部,表明是非常重要的介面。
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文件夾下。這兩個介面有什麼用,做什麼,先看下程式碼吧。
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的內部類。
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不同之處在於:
DepthLimiter只負責對LogEventPropertyValue的創建,而PropertyValueConveter不僅負責前者的構建,還負責對LogEventProperty的創建,這一點從所繼承的介面數目可以看出來。也就是說,PropertyValueConverter的作用範圍比DepthLimiter大。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通過PropertyValueConverter和DepthLimiter的相互引用配合達到遞歸轉換的目的。也就是說,DepthLimiter負責維護遞歸轉換時當前轉換數據所處的深度。
剩餘參數
在PropertyValueConverter中,還剩下一些較為簡單的數據參數。在了解了DepthLimiter之後,剩餘的三個參數可以很明顯看出來。
_maximumStringLength:指的是構造日誌資訊字元串的最大長度_maximumCollectionCount: 指的是日誌數據集合解析的最大個數_propgateExceptions: 該值是一個bool類型,表示在保存日誌數據的過程中,若發生了異常,則相關異常是否被拋出。
介面函數
接下來,我們把注意力再回到PropertyValueConverter類中對IDestructuringPolicy和IScalarConversionPolicy介面函數實現的部分。
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());
}
這個函數非常複雜,但大體上分成以下幾個步驟。
- 如果傳入的數據是一個
null對象,則直接使用ScalarValue構造。 - 利用內部保存的
IScalarConversionPolicy數組嘗試對日誌數據綁定,如果能綁定,則將結果返回,否則繼續。 - 設置深度值,之所以將深度值放在這裡設置,是因為後續操作可能會用到這個深度值。
- 利用內部維護的
IDestructingPolicy數組嘗試對日誌數據綁定,如果能綁定,則返回結果值,否則繼續。 - 獲取當前日誌數據的類型,並採用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以及它們的實現類。

