Serilog 源码解析——解析字符串模板
大家好啊,上一篇中我们谈到 Serilog 是如何决定日志记录的目的地的,那么从这篇开始,我们着重于 Serilog 是向 Sinks 中记录什么的,这个大功能比较复杂,我尝试再将其再拆分成几个小块方便大家理解。(系列目录)
本篇要解决什么
之前提到,在Logger
类中构造对应的LogEvent
对象之前,日志记录器通过MessageTemplateProcessor
类对象的Process
方法处理字符串模板和传入进来的数据信息。这个方法内部只是做了两件事:
- 解析消息模板,分析哪些是字符串字面值哪些是需要转换的属性值
- 构造相关的数据对象
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
类对象内所拥有的NameProperties
和PositionProperties
均描述一组属性 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,PropertyName
为version
,Destructuring
值为Default,Format
值为000
,Alignment
为默认值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
方法中,它给出了具体的解析逻辑:
- 如果当前字符串的解析数据被哈希表所记录下来,那么直接从对应的位置提取解析好的
MessageTemplate
对象并返回。 - 如果没有,则利用内部维护的
_innerParser
对其解析 - 将解析后的
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
类。整个字符串模板通过MessageTemplateProcessor
的Process
函数进行解析,而其内部,利用装饰模式给处理行为添加缓存机制,即MessageTemplateCache
类,真正的解析处理逻辑则放在MessageTemplateParser
类中,同时这两个类实现IMessageTemplateParser
接口,方便第三方进行替换。
这篇文章主要注重对模板数据的解析,然而,在日志记录的过程中,除了日志模板外,日志记录通常还会输入一些日志数据,这些数据常用来替换属性 Token 中的文本。在下一篇中,我们将着重研究 Serilog 日志库是如何处理这些日志数据的。