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
以及它們的實現類。