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)); } }