Serilog 原始碼解析——解析字串模板
阿新 • • 發佈:2020-11-12
大家好啊,[上一篇](https://www.cnblogs.com/iskcal/p/implementation-of-the-sink.html)中我們談到 Serilog 是如何決定日誌記錄的目的地的,那麼從這篇開始,我們著重於 Serilog 是向 Sinks 中記錄什麼的,這個大功能比較複雜,我嘗試再將其再拆分成幾個小塊方便大家理解。([系列目錄](https://www.cnblogs.com/iskcal/p/introduction-to-the-source-code-of-serilog.html#目錄))
## 本篇要解決什麼
之前提到,在`Logger`類中構造對應的`LogEvent`物件之前,日誌記錄器通過`MessageTemplateProcessor`類物件的`Process`方法處理字串模板和傳入進來的資料資訊。這個方法內部只是做了兩件事:
1. 解析訊息模板,分析哪些是字串字面值哪些是需要轉換的屬性值
2. 構造相關的資料物件
```csharp
public void Process(string messageTemplate, object[] messageTemplateParameters, out MessageTemplate parsedTemplate, out EventProperty[] properties)
{
parsedTemplate = _parser.Parse(messageTemplate); // 第一件事
properties = _propertyBinder.ConstructProperties(parsedTemplate, messageTemplateParameters); // 第二件事
}
```
這篇文章主要分析第一件事的處理方法。之後將對應的資料與模板資訊繫結內容則放在下一篇中。
## `MessageTemplate`類
在分析如何處理之前,需要弄明白這個功能函式的輸入是什麼,輸出是什麼,在對生成什麼東西有一定了解後,才能更加方便了解其執行機理。這裡,在第一行程式碼可以發現,輸入是一個字串,而輸出則是一個`MessageTemplate`類物件。因此,有必要對`MessageTemplate`類深入研究。`MessageTemplate`類儲存在 Core 資料夾下,和`LogEvent`類一樣,都是儲存資料而用。這也就說明,`MessageTemplate`也是`LogEvent`中的一個屬性,表明它是日誌事件資料中的一部分。
`MessageTemplate`類中有很多的屬性和方法,這裡僅考慮一些較為重要的屬性。
```csharp
public class MessageTemplate
{
public string Text { get; }
readonly MessageTemplateToken[] _tokens;
internal ProertyToken[] NamedProperties { get; }
internal ProertyToken[] PositionalProerties { get; }
...
}
```
`Text`屬性不用多說,該值為傳入的字串模板資料。接下來是`MessageTemplateToken`物件,該物件描述的是模板解析的結果,主要包含兩類 Token,一個是文字 Token,即`TextToken`類,它描述的是模板中的文字資訊,另一個是屬性 Token,即`PropertyToken`類,描述的是模板內需要替換的屬性資料名。這些類均是描述解析後的結果資訊,且類檔案均位於在 Parsing 資料夾中,且都繼承於`MessageTemplateToken`類。在`MessageTemplate`類中,通過引用`MessageTemplateToken`陣列來達到保有模板解析的結果資訊。從變數名上可以發現,`MessageTemplate`類物件內所擁有的`NameProperties`和`PositionProperties`均描述一組屬性 Token,二者的區別在於:前者描述的是具名的屬性Token,該Token在字串中具有具體的名字;後者描述的是位置的屬性Token,即它在字串模板中以位置資料出現。
舉個例子,如果字串模板為`版本{version}`,那麼其中`版本`就是文字 Token,`version`是具名屬性 Token;如果字串模板為`版本{0}`,那麼`0`則是位置的屬性Token,它表示使用後續第一個值作為它的資料。
## `MessageTemplateToken`類及其繼承類
前面提到了 Token 這一描述結果的型別,接下來就是看描述這些 Token 是如何實現自己的功能的。
作為描述字串解析結果的基類`MessageTemplateToken`,它主要包含兩大屬性,`StartIndex`描述該Token在字串模板中的起始位置,`Length`描述該Token的長度。另外,這個類是一個抽象類,不允許直接例項化該類。
```csharp
public abstract class MessageTemplateToken
{
public int StartIndex { get; }
public abstract int Length { get; }
}
```
接下來是文字 Token,即`TextToken`類。這個類非常簡單,既然文字 Token 只描述模板中的文字部分,它只需要包含描述文字的`Text`屬性,其長度也就被設定為文字的長度。
```csharp
public sealed class TextToken : MessageTemplateToken
{
public string Text { get; }
public override int Length => Text.Length;
}
```
之後是屬性 Token,即`PropertyToken`類。
```csharp
public sealed class PropertyToken : MessageTemplateToken
{
readonly string _rawText;
readonly int? _position;
public override int Length => _rawText.Length;
public string PropertyName { get; }
public Destructuring Destructuring { get; }
public string Format { get; }
public Alignment? Alignment { get; }
public bool IsPositional => _position.HasValue;
}
```
從上面的程式碼可以看出來,該類要比`TextToken`複雜。這裡一個個來分析:`_rawText`變數顧名思義,表示字串模板中屬性字串,通常為花括號所括起來的部分。`position`作為一個可空int型資料,描述該屬性Token的位置,這裡只有位置的屬性Token才有該值,具名的屬性Token該值為空,二者的從`IsPositional`屬性來區分。`Length`表示原始字串的長度。`PropertyName`屬性記錄的是屬性 Token 的名字。而`Destructuring`屬性指明該屬性值應該如何渲染(模板中的變數採用$還是@渲染,即採用資料本身類的`ToString`方法還是將資料物件解構再渲染),`Format`指明輸出的格式化字串,`Alignment`屬性指明對其的方式,預設左對齊,通過設定可以讓日誌右對齊。舉個例子,比如字串模板為`{version: 000}`,那麼其`_rawText`值為`{version: 000}`, `_position`為null, `Length`為14,`PropertyName`為`version`,`Destructuring`值為Default,`Format`值為`000`,`Alignment`為預設值null,`IsPositional`為false。
總的來說,`MessageTemplate`類描述字串模板解析後的資料,自然也是`LogEvent`類中的一個重要屬性。在`MessageTemplate`中,維護一組經解析後的`MessageTemplateToken`陣列,不同的 Token 用不同的類來描述,即描述文字資訊的`TextToken`以及描述屬性資訊的`PropertyToken`。
## `MessageTemplateCache`類
在瞭解完資料的儲存部分後,接下來需要弄清楚的就是處理生成這些資料類的行為類。在`MessageTemplateProcessor`類的`Process`函式中,負責處理字串模板解析的是`_parser`欄位,它屬於`MessageTemplateCache`類。那麼首先看下其內部的結構。
```csharp
interface IMessageTemplateParser
{
MessageTemplate Parse(string messageTemplate);
}
class MessageTemplateCache : IMessageTemplateParser
{
readonly IMessageTemplateParser _innerParser;
readonly object _templatesLock = new object();
readonly HashTable _templates = new HashTable();
public MessageTemplateCache(IMessageTemplateParser innerParser)
{
_innerParser = innerParser;
}
public MessageTemplate Parse(string messageTemplate)
{
...
// 第一步
var result = (MessageTemplate)_templates[messageTemplate];
if (result != null) return result;
// 第二步
result = _innerParser.Parse(messageTemplate);
// 第三步
lock (_templatesLock)
{
...
_templates[messageTemplate] = result;
}
}
}
```
首先,`MessageTemplateCache`類繼承`IMessageTemplateParser`介面,該介面位於Core資料夾下,表示是一個解析字串模板的核心介面,內部包含解析函式`Parse`,該函式的輸入是字串模板的字串資料,輸出是`MessageTemplate`類。其次,看下繼承類`MessageTemplateCache`的實現,從名稱上來看,可以看出它帶有快取的解析。當然,內部的實現也是這樣的,在該類內部,有一個`_innerParser`的同類介面物件,感覺有點熟悉。繼續往下,`_templates`是一個雜湊表,它是字典類的非泛型實現,通過它可以尋找字串模板對應的`MessageTemplate`物件,可以將其看成是一個快取。建構函式附帶一個對應訊息解析物件,並給`_innerParser`賦值。在其核心的`Parser`方法中,它給出了具體的解析邏輯:
1. 如果當前字串的解析資料被雜湊表所記錄下來,那麼直接從對應的位置提取解析好的`MessageTemplate`物件並返回。
2. 如果沒有,則利用內部維護的`_innerParser`對其解析
3. 將解析後的`MessageTemplate`物件新增到雜湊表中,為後續同一個訊息模板中提供快取資料。
可以發現,這種程式碼結構和之前的 Sink 邏輯非常像,它也是裝飾模式的一個實現。即無論採用何種具體解析訊息模板的邏輯,通過`MessageTemplateCache`類可以為其動態新增快取記錄的功能,對於常用的訊息模板場合下可以提高解析的效率,縮短執行時間。換句話來說,解析這一操作行為是一個純函式,即給定的輸入就能給定輸出,不存在副作用,該函式的處理結果可以快取下來。
## `MessageTemplateParser`類
那麼在 Serilog 有提供具體的解析類麼?有的,它是位於 Parsing 資料夾下的`MessageTemplateParser`類。
```csharp
public class MessageTemplateParser : IMessageTemplateParser
{
public MessageTemplate Parse(string messageTemplate)
{
...
return new MessageTemplate(messageTemplate, Tokenize(messageTemplate));
}
}
```
可以看到,這個類做的就是直接構造對應的`MessageTemplate`類物件,這裡的`Tokenize`函式則是將字串模板轉換成一個或多個`MessageTemplateToken`物件,其核心思想就是從左到右依次掃描字串中的每個字元,判斷其是否是屬性Token起始的`{`,然後將其分割。如果感興趣的話請閱讀具體原始碼,考慮到這段程式碼是一個過程性程式碼,通過除錯一步步讀下去即可,這裡就不進行詳述了。
# 總結
本篇主要講述字串解析過程的程式碼結構,該結構較為簡單,模板解析的資料均儲存在`MessageTemplate`類中,主要以`MessageTemplateToken`類物件的形式存在。解析後的 Token 主要分為兩類,只用於描述文字資訊的`TextToken`類以及描述屬性資料的`PropertyToken`類。整個字串模板通過`MessageTemplateProcessor`的`Process`函式進行解析,而其內部,利用裝飾模式給處理行為新增快取機制,即`MessageTemplateCache`類,真正的解析處理邏輯則放在`MessageTemplateParser`類中,同時這兩個類實現`IMessageTemplateParser`介面,方便第三方進行替換。
這篇文章主要注重對模板資料的解析,然而,在日誌記錄的過程中,除了日誌模板外,日誌記錄通常還會輸入一些日誌資料,這些資料常用來替換屬性 Token 中的文字。在下一篇中,我們將著重研究 Serilog 日誌庫是如何處理這些日誌數