zoukankan      html  css  js  c++  java
  • Serilog 源码解析——数据的保存(上)

    上一篇中,我们主要研究了Serilog是如何解析字符串模板的,它只是单独对字符串模板的处理,对于日志记录时所附带的数据没有做任何的操作。在本篇中,我们着重研究日志数据的存储方式。(系列目录)

    本篇所解决的内容

    本文主要讲述在Serilog中日志记录器是如何记录数据的,即在上一篇文章中解析部分的第二件事。和之前的文章架构一样,本篇文章主要从数据存储和行为逻辑两个方面做阐述。

    public void Process(string messageTemplate, object[] messageTemplateParameters, out MessageTemplate parsedTemplate, out EventProperty[] properties)
    {
        parsedTemplate = _parser.Parse(messageTemplate);  // 第一件事
        properties = _propertyBinder.ConstructProperties(parsedTemplate, messageTemplateParameters);  // 第二件事
    }
    

    考虑到数据保存的逻辑比较复杂,涉及到的类结构比较多,计划将该部分逻辑拆成两个部分,方便理解。

    EventProperty结构体

    首先看下数据存储所使用到的数据类。ConstructProperties方法返回的是EventProperty结构体数组。数组比较好理解,一个数据对应一个EventProperty结构。EventProerty结构从字面意思上可以看出来,下面是EventProperty核心部分。

    readonly struct EventProperty
    {
        public string Name { get; }
        public LogEventPropertyValue Value { get; }
    }
    

    这个结构体非常的简单,内部只记录该属性的名称和对应的数据,Name好理解,它是该数据的名称,为字符串类型。另一个则是LogEventPropertyValue对象,它保存了对应数据。另外,该类被readonly所修饰,表明该类是一个只读的结构体,一旦被创建出来,就无法修改内部的数据。

    LogEventProperty

    在 Serilog 中,有一个和EventProperty结构体功能差不多的类,即LogEventProperty类。从下面的代码可以看出,二者没有太大的差别。和上面的结构一样,这两个代码文件均位于 Event 文件夹中,都是和数据相关的。

    public class LogEventProperty
    {
        public string Name { get; }
        public LogEventPropertyValue Value { get; }
    }
    

    LogEventPropertyValue类及其继承类

    在上一节,我们认为LogEventPropertyValue是保存相关数据的。在说明这个类之前,不知道有没有人会很好奇一点,为什么会有LogEventPropertyValue这个类?按道理,保存数据对象没必要那么大费周章,只需要用object类即可,毕竟object类是万物所有类的基类,没有任何必要额外构建新类。那么,在 Serilog 中,为什么要使用LogEventPropertyValue来保存数据呢?我们先看下这个类有什么。

    public abstract class LogEventPropertyValue : IFormattable
    {
        public abstract void Render(TextWriter output, string format = null, IFormatProvider formatProvider = null);
        public string ToString() => ToString(null, null);
        public string ToString(string format, IFormatProvider formatProvider)
        {
            var output = new StringWriter();
            Render(output, format, formatProvider);
            return output.Tostring();
        }
    }
    

    可以看到,LogEventPropertyValue类是一个抽象类,它继承于IFormattable接口,从其内部的函数可以看出,似乎都是和渲染相关,看不出来和数据保存有什么关系。是我们弄错了么?LogEventPropertyValue根本不是保存数据用的?

    这里我自己有一个回答,不一定保证正确。首先,回到上一个问题,为什么不采用object而是使用新类。实际上,如果只从记录数据的角度来看,object类足够用了。然而,使用object类型有一个非常麻烦的问题,那就是不同的数据类型有不同的渲染方式,对于一个object类型的数据如何进行渲染是一个很麻烦的操作。对于原始数据类型,我们只需要调用其ToString方法将其转换成字符串,数组则将数据渲染到[]中,字典则是将数据渲染到{}中,而更加复杂的数据类型类型,考虑其渲染形式,可能利用其ToString方法渲染($操作符),也有可能解构该对象渲染(@操作符),具体渲染形式由字符串模板内给出。对于这样一个复杂的渲染逻辑,如果只使用object对象,那么在渲染阶段会构造一段非常复杂且难以维护的if-else语句块。

    public string Render(object obj)
    {
        if (obj.GetType() == typeof(int) || obj.GetType() == typeof(double) || ...)
        {
            return obj.ToString();
        }
        else if (obj.GetGenericTypeDefinition() == typeof(IEnumerable<>))
        {
            ...
        }
    }
    

    更好的办法,就是将不同的渲染策略封装到对应的类中,即通过策略模式在不同的继承类中重写对应的渲染逻辑。在 Serilog 中所展现出来的就是,以LogEventPropertyValue为根类,若干不同渲染方法的继承类ScalarValueSquenceValueDictionaryValueStructureValue。明白了这点后,就可以明白LogEventPropertyValue所提供的函数了,其抽象函数Render就表示子类需要重写的渲染逻辑。Serilog 将数据的渲染逻辑分成四大类:

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

    以上为PropertyBinder的部分代码。首先是_valueConverter这个PropertyValueConverter对象,有什么功能,做什么事暂时不清楚,先放一放。向下继续,ConstructProperties函数,该函数作为PropertyBinder的唯一公开函数,提供了整个绑定功能。往下,ConstructNamedProperties函数提供了绑定具名属性 Token 和日志数据的功能。内部主要做了三件事:

    1. 获取解析后的MessageTemplate中具名属性Token对象以及其数目;
    2. 针对每个具名属性Token在对应的位置构造对应的EventProperty结构
    3. 如果消息记录时提供了多于解析出具名属性Token数目的消息数据时,则把后续部分仍保留下来,且设置其Name__加当前序号。

    最后,在构造对应某个EventProperty结构时,采用ConstrctProperty函数进行构造。可以看到,通过构造函数,将具名属性Token的属性名称传给Name值,而具体构造哪种LogEventPropertyValue对象,则有PropertyValueConverterCreatePropertyValue方法进行构造。由此可见,PropertyValueConverter有点类似于工厂,指明当前消息数据应构造什么LogEventPropertyValue派生类。至于PropertyValueConverter类具体如何做到的,将留到下一篇再讲解吧。

    总结

    本文对字符串模板解析后的属性 Token 与日志数据的绑定做了大概的介绍。首先说明的是绑定最终得到了什么结果,即EventProperty结构体以及LogEventProperty类。在这些结构体/类的内部,通过LogEventPropertValue保存每一个日志数据,该类是一个抽象类,不同的渲染方式有着不同的继承类。之后,简要描述了下绑定过程,即通过PropertyBinder将每一个具名属性 Token 与对应的日志数据对象绑定。然而,具体的绑定过程没有进行交代,这也是下一篇文章的主要内容,即给定一个属性 Token 与一个日志对象,如何生成对应的EventProperty结构体。

  • 相关阅读:
    How to function call using 'this' inside forEach loop
    jquery.validate.unobtrusive not working with dynamic injected elements
    Difference between jQuery.extend and jQuery.fn.extend?
    Methods, Computed, and Watchers in Vue.js
    Caution using watchers for objects in Vue
    How to Watch Deep Data Structures in Vue (Arrays and Objects)
    Page: DOMContentLoaded, load, beforeunload, unload
    linux bridge
    linux bridge
    EVE-NG网卡桥接
  • 原文地址:https://www.cnblogs.com/iskcal/p/saving-of-log-data-1.html
Copyright © 2011-2022 走看看