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 日志库是如何处理这些日志数据的。

posted @ 2020-11-12 15:39  iskcal  阅读(1085)  评论(0编辑  收藏  举报