Serilog 源碼解析——解析字符串模板


大家好啊,上一篇中我們談到 Serilog 是如何決定日志記錄的目的地的,那么從這篇開始,我們着重於 Serilog 是向 Sinks 中記錄什么的,這個大功能比較復雜,我嘗試再將其再拆分成幾個小塊方便大家理解。(系列目錄

本篇要解決什么

之前提到,在Logger類中構造對應的LogEvent對象之前,日志記錄器通過MessageTemplateProcessor類對象的Process方法處理字符串模板和傳入進來的數據信息。這個方法內部只是做了兩件事:

  1. 解析消息模板,分析哪些是字符串字面值哪些是需要轉換的屬性值
  2. 構造相關的數據對象
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類保存在 Event 文件夾下,和LogEvent類一樣,都是保存數據而用。這也就說明,MessageTemplate也是LogEvent中的一個屬性,表明它是日志事件數據中的一部分。

MessageTemplate類中有很多的屬性和方法,這里僅考慮一些較為重要的屬性。

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類對象內所擁有的NamePropertiesPositionProperties均描述一組屬性 Token,二者的區別在於:前者描述的是具名的屬性Token,該Token在字符串中具有具體的名字;后者描述的是位置的屬性Token,即它在字符串模板中以位置數據出現。

舉個例子,如果字符串模板為版本{version},那么其中版本就是文本 Token,version是具名屬性 Token;如果字符串模板為版本{0},那么0則是位置的屬性Token,它表示使用后續第一個值作為它的數據。

MessageTemplateToken類及其繼承類

前面提到了 Token 這一描述結果的類型,接下來就是看描述這些 Token 是如何實現自己的功能的。

作為描述字符串解析結果的基類MessageTemplateToken,它主要包含兩大屬性,StartIndex描述該Token在字符串模板中的起始位置,Length描述該Token的長度。另外,這個類是一個抽象類,不允許直接實例化該類。

public abstract class MessageTemplateToken
{
    public int StartIndex { get; }
    public abstract int Length { get; }
}

接下來是文本 Token,即TextToken類。這個類非常簡單,既然文本 Token 只描述模板中的文本部分,它只需要包含描述文本的Text屬性,其長度也就被設置為文本的長度。

public sealed class TextToken : MessageTemplateToken
{
    public string Text { get; }
    public override int Length => Text.Length;
}

之后是屬性 Token,即PropertyToken類。

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,PropertyNameversionDestructuring值為Default,Format值為000Alignment為默認值null,IsPositional為false。

總的來說,MessageTemplate類描述字符串模板解析后的數據,自然也是LogEvent類中的一個重要屬性。在MessageTemplate中,維護一組經解析后的MessageTemplateToken數組,不同的 Token 用不同的類來描述,即描述文本信息的TextToken以及描述屬性信息的PropertyToken

MessageTemplateCache

在了解完數據的存儲部分后,接下來需要弄清楚的就是處理生成這些數據類的行為類。在MessageTemplateProcessor類的Process函數中,負責處理字符串模板解析的是_parser字段,它屬於MessageTemplateCache類。那么首先看下其內部的結構。

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類。

public class MessageTemplateParser : IMessageTemplateParser
{
    public MessageTemplate Parse(string messageTemplate)
    {
        ...
        return new MessageTemplate(messageTemplate, Tokenize(messageTemplate));
    }
}

可以看到,這個類做的就是直接構造對應的MessageTemplate類對象,這里的Tokenize函數則是將字符串模板轉換成一個或多個MessageTemplateToken對象,其核心思想就是從左到右依次掃描字符串中的每個字符,判斷其是否是屬性Token起始的{,然后將其分割。如果感興趣的話請閱讀具體源碼,考慮到這段代碼是一個過程性代碼,通過調試一步步讀下去即可,這里就不進行詳述了。

總結

本篇主要講述字符串解析過程的代碼結構,該結構較為簡單,模板解析的數據均保存在MessageTemplate類中,主要以MessageTemplateToken類對象的形式存在。解析后的 Token 主要分為兩類,只用於描述文本信息的TextToken類以及描述屬性數據的PropertyToken類。整個字符串模板通過MessageTemplateProcessorProcess函數進行解析,而其內部,利用裝飾模式給處理行為添加緩存機制,即MessageTemplateCache類,真正的解析處理邏輯則放在MessageTemplateParser類中,同時這兩個類實現IMessageTemplateParser接口,方便第三方進行替換。

這篇文章主要注重對模板數據的解析,然而,在日志記錄的過程中,除了日志模板外,日志記錄通常還會輸入一些日志數據,這些數據常用來替換屬性 Token 中的文本。在下一篇中,我們將着重研究 Serilog 日志庫是如何處理這些日志數據的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM