Serilog 源碼解析——數據的保存(上)
- 2020 年 11 月 16 日
- 筆記
- c#, Serilog源碼解析, 源碼解析
在上一篇中,我們主要研究了Serilog是如何解析字元串模板的,它只是單獨對字元串模板的處理,對於日誌記錄時所附帶的數據沒有做任何的操作。在本篇中,我們著重研究日誌數據的存儲方式。(系列目錄)
本篇所解決的內容
本文主要講述在Serilog中日誌記錄器是如何記錄數據的,即在上一篇文章中解析部分的第二件事。和之前的文章架構一樣,本篇文章主要從數據存儲和行為邏輯兩個方面做闡述。
public void Process(string messageTemplate, object[] messageTemplateParameters, out MessageTemplate parsedTemplate, out EventProperty[] properties)
{
parsedTemplate = _parser.Parse(messageTemplate); // 第一件事
properties = _propertyBinder.ConstructProperties(parsedTemplate, messageTemplateParameters); // 第二件事
}
考慮到數據保存的邏輯比較複雜,涉及到的類結構比較多,計劃將該部分邏輯拆成兩個部分,方便理解。
EventProperty
結構體
首先看下數據存儲所使用到的數據類。ConstructProperties
方法返回的是EventProperty
結構體數組。數組比較好理解,一個數據對應一個EventProperty
結構。EventProerty
結構從字面意思上可以看出來,下面是EventProperty
核心部分。
readonly struct EventProperty
{
public string Name { get; }
public LogEventPropertyValue Value { get; }
}
這個結構體非常的簡單,內部只記錄該屬性的名稱和對應的數據,Name
好理解,它是該數據的名稱,為字元串類型。另一個則是LogEventPropertyValue
對象,它保存了對應數據。另外,該類被readonly
所修飾,表明該類是一個只讀的結構體,一旦被創建出來,就無法修改內部的數據。
LogEventProperty
類
在 Serilog 中,有一個和EventProperty
結構體功能差不多的類,即LogEventProperty
類。從下面的程式碼可以看出,二者沒有太大的差別。和上面的結構一樣,這兩個程式碼文件均位於 Event 文件夾中,都是和數據相關的。
public class LogEventProperty
{
public string Name { get; }
public LogEventPropertyValue Value { get; }
}
LogEventPropertyValue
類及其繼承類
在上一節,我們認為LogEventPropertyValue
是保存相關數據的。在說明這個類之前,不知道有沒有人會很好奇一點,為什麼會有LogEventPropertyValue
這個類?按道理,保存數據對象沒必要那麼大費周章,只需要用object
類即可,畢竟object
類是萬物所有類的基類,沒有任何必要額外構建新類。那麼,在 Serilog 中,為什麼要使用LogEventPropertyValue
來保存數據呢?我們先看下這個類有什麼。
public abstract class LogEventPropertyValue : IFormattable
{
public abstract void Render(TextWriter output, string format = null, IFormatProvider formatProvider = null);
public string ToString() => ToString(null, null);
public string ToString(string format, IFormatProvider formatProvider)
{
var output = new StringWriter();
Render(output, format, formatProvider);
return output.Tostring();
}
}
可以看到,LogEventPropertyValue
類是一個抽象類,它繼承於IFormattable
介面,從其內部的函數可以看出,似乎都是和渲染相關,看不出來和數據保存有什麼關係。是我們弄錯了么?LogEventPropertyValue
根本不是保存數據用的?
這裡我自己有一個回答,不一定保證正確。首先,回到上一個問題,為什麼不採用object
而是使用新類。實際上,如果只從記錄數據的角度來看,object
類足夠用了。然而,使用object
類型有一個非常麻煩的問題,那就是不同的數據類型有不同的渲染方式,對於一個object類型的數據如何進行渲染是一個很麻煩的操作。對於原始數據類型,我們只需要調用其ToString
方法將其轉換成字元串,數組則將數據渲染到[]
中,字典則是將數據渲染到{}
中,而更加複雜的數據類型類型,考慮其渲染形式,可能利用其ToString
方法渲染($操作符),也有可能解構該對象渲染(@操作符),具體渲染形式由字元串模板內給出。對於這樣一個複雜的渲染邏輯,如果只使用object
對象,那麼在渲染階段會構造一段非常複雜且難以維護的if-else
語句塊。
public string Render(object obj)
{
if (obj.GetType() == typeof(int) || obj.GetType() == typeof(double) || ...)
{
return obj.ToString();
}
else if (obj.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
...
}
}
更好的辦法,就是將不同的渲染策略封裝到對應的類中,即通過策略模式在不同的繼承類中重寫對應的渲染邏輯。在 Serilog 中所展現出來的就是,以LogEventPropertyValue
為根類,若干不同渲染方法的繼承類ScalarValue
、SquenceValue
、DictionaryValue
、StructureValue
。明白了這點後,就可以明白LogEventPropertyValue
所提供的函數了,其抽象函數Render
就表示子類需要重寫的渲染邏輯。Serilog 將數據的渲染邏輯分成四大類:
ScalarValue
類:該類的渲染邏輯是直接將數據的ToString
方法的結果返回,適用於基礎數據類型和一些強制要求字元串化的複雜數據(字元串模板內以$開頭)。SqeuenceValue
類:該類渲染邏輯是將多個數據渲染到[]
中,通常數據是一個數組或列表。DictionaryValue
類:鍵值對類對象的渲染邏輯,將數據渲染到{}
中,它要求數據鍵(key)應該是ScalarValue
。StructValue
類:將數據類解構,以公開的欄位或屬性名作為鍵值,進行渲染。
解決第一個問題後,再來看下第二個問題,作為各大渲染邏輯的基類,為什麼LogEventProperty
沒有對數據的引用。我個人比較傾向於兩個方面來解釋。一是,沒有很方便的形式表達這個數據。我們知道四大 Value 類分別保存不同的數據,不同的數據採用不同的形式,這就使得在基類中不能很好地指明數據的類型。另一個就是,對於這些 Value 的派生類,它們更關注的是渲染的結果,而不是保存的數據,數據不是該數據結構中的重點,也就沒有必要在基類中指明數據。
從這個角度,我們就就可以著手查看四個派生類的內容了。基本上,四個類保有不同的數據對象並重寫了相應的Render
函數,提供不同的重寫邏輯。
public class ScalarValue : LogEventPropertyValue
{
public oject Value { get; }
...
}
public class SquenceValue : LogEventPropertyValue
{
readonly LogEventPropertyValue[] _elements;
...
}
public class DictionaryValue : LogEventPropertyValue
{
public IReadonlyDictionary<ScalarValue, LogEventPropertyValue> Elements { get; }
}
public class StructureValue : LogEventPropertyValue
{
public LogEventPropertyValue[] _properties;
public string TypeTag { get; }
}
ScalarValue
類:這個類在Serilog算得上是一個比較重要的類,可以看到,其內部維護了一個object
的對象,這和之前我們提到的object
描述數據對象的想法一致,其渲染的方法基本上是利用C#主流的格式化方式輸出的。SequenceValue
類:該類內部維護了一個LogEventPropertyValue
的數組,因為該類主要用於渲染一組數據對象(數組或隊列等)。因此,其內部的每一個元素都是一個LogEventPropertyValue
對象。DictionaryValue
類:該類描述的是一組鍵值對應關係的渲染邏輯,這裡要求鍵的數據類型應該為ScalarValue
。StructureValue
類:該類主要描述以結構的方式輸出某個類對象內所有的公開屬性值,可以看到其內部維護的也是一個數組,這點和SequenceValue
一樣,但它的渲染邏輯和SequenceValue
完全不同。此外,該類還有一個TypeTag
屬性,目前 Serilog 用它來描述該類對象的類型資訊。
到目前為止,描述數據保存的類就這麼多了,它主要通過EventProperty
結構和LogEventProperty
類來描述對應數據,這些結構和類中主要包含兩個部分,一個是用來描述當前屬性Token的名稱Name
,另一個則是保存相關數據資訊的LogEventPropertyValue
對象。LogEventPropertyValue
對象則是一個抽象對象,它需要派生類提供一個具體的渲染方法。Serilog 針對不同的數據類型為LogEventPropertyValue
提供了4類不同的渲染邏輯。最後,EventProperty
結構體數組作為日誌事件的一類數據,也被保存在LogEvent
消息日誌中。
PropertyBinder
類
在了解完對應的結果類後,我們可以看下它是怎麼生成的。Serilog 中,保存日誌數據的功能由PropertyBinder
類提供,從名字上就可以看出它做的是綁定功能,即將字元串模板解析的屬性 Token 和對應的日誌數據進行綁定。也就是說,生成的EventProperty
結構體數組內的每個元素應對應一個屬性 Token,其Name
應該是屬性 Token 的PropertyName
,其Value
應該是對應的某個LogEventPropertyValue
類對象,且該對象包裝了對應的日誌數據。
上一篇中曾經提到,屬性 Token 又主要分為兩類,一類是位置 Token,它在字元串模板中表示為位置序號,表示應該是之後第幾個日誌輸入數據,而另一類則是具名 Token,這類 Token 的數據嚴格按照順序決定,即第一個日誌數據對應第一個具名 Token。Serilog 認為二者不能混用,如果有具名的屬性 Token,則只使用具名 Token。為了降低篇幅,這裡僅分析具名 Token 的綁定邏輯,位置 Token 的綁定邏輯也是差不多的,感興趣的可以直接查看源碼。
class PropertyBinder
{
readonly PropertyValueConverter _valueConverter;
...
public EventProperty[] ConstructProperties(MessageTemplate messageTemplate, object[] messageTemplateParameters)
{
...
return ConstructNamedProperties(messageTemplate, messageTemplateParameters);
}
EventProperty[] ConstructNamedProperties(MessageTemplate template, object[] messageTemplateParameters)
{
// 獲取消息模板中具名屬性Token的個數
var namedProperties = template.NamedProperties;
var matchedRun = namedProperties.Length;
...
// 按照具名屬性Token構造相應的EventProperty結構並賦值
var result = new EventProperty[messageTemplateParameters.Length];
for (var i = 0; i < matchedRun; ++i)
{
var property = template.NamedProperties[i];
var value = messageTemplateParameters[i];
result[i] = ConstructProperty(property, value);
}
// 如果消息數據還有多的話,則繼續構造,其屬性名為__加序號
for (var i = matchedRun; i < messageTemplateParameters.Length; ++i)
{
var value = _valueConverter.CreatePropertyValue(messageTemplateParameters[i]);
result[i] = new EventProperty("__" + i, value);
}
return result;
}
EventProperty ConstructProperty(PropertyToken propertyToken, object value)
{
return new EventProperty(
propertyToken.PropertyName,
_valueConverter.CreatePropertyValue(value, propertyToken.Destructuring));
}
}
以上為PropertyBinder
的部分程式碼。首先是_valueConverter
這個PropertyValueConverter
對象,有什麼功能,做什麼事暫時不清楚,先放一放。向下繼續,ConstructProperties
函數,該函數作為PropertyBinder
的唯一公開函數,提供了整個綁定功能。往下,ConstructNamedProperties
函數提供了綁定具名屬性 Token 和日誌數據的功能。內部主要做了三件事:
- 獲取解析後的
MessageTemplate
中具名屬性Token對象以及其數目; - 針對每個具名屬性Token在對應的位置構造對應的
EventProperty
結構 - 如果消息記錄時提供了多於解析出具名屬性Token數目的消息數據時,則把後續部分仍保留下來,且設置其
Name
為__
加當前序號。
最後,在構造對應某個EventProperty
結構時,採用ConstrctProperty
函數進行構造。可以看到,通過構造函數,將具名屬性Token的屬性名稱傳給Name
值,而具體構造哪種LogEventPropertyValue
對象,則有PropertyValueConverter
的CreatePropertyValue
方法進行構造。由此可見,PropertyValueConverter
有點類似於工廠,指明當前消息數據應構造什麼LogEventPropertyValue
派生類。至於PropertyValueConverter
類具體如何做到的,將留到下一篇再講解吧。
總結
本文對字元串模板解析後的屬性 Token 與日誌數據的綁定做了大概的介紹。首先說明的是綁定最終得到了什麼結果,即EventProperty
結構體以及LogEventProperty
類。在這些結構體/類的內部,通過LogEventPropertValue
保存每一個日誌數據,該類是一個抽象類,不同的渲染方式有著不同的繼承類。之後,簡要描述了下綁定過程,即通過PropertyBinder
將每一個具名屬性 Token 與對應的日誌數據對象綁定。然而,具體的綁定過程沒有進行交代,這也是下一篇文章的主要內容,即給定一個屬性 Token 與一個日誌對象,如何生成對應的EventProperty
結構體。