16

Serilog 源码解析——数据的保存(中)

 3 years ago
source link: http://www.cnblogs.com/iskcal/p/saving-of-log-data-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇文章中揭露了日志数据的绑定逻辑,主要说明了日志数据绑定的结果信息,即 EventProperty 结构体和 LogEventProperty 类,以及日志数据与具名属性Token的绑定类 PropertyBinder 。在本文中,我们主要对 PropertyValueConverter 类及其涉及的其他相关类进行说明。关注的重点在如何利用具名属性 Token 以及对应的日志数据来构造对应的 LogEventPropertyValue 类对象。(系列目录)

PropertyValueConverter

PropertyValueConverter 类是一个非常复杂的类。Serilog将其分成两个代码文件存储,分别为 ./Capturing/PropertyValueConverter.cs 以及 ./Capturing/DepthLimiter.cs 文件。先看下 PropertyValueConverter 有什么字段和属性。

partial class PropertyValueConverter : ILogEventPropertyFactory, ILogEventPropertyValueFactory
{
    readonly IDestructuringPolicy[] _destructuringPolicies;
    readonly IScalarConversionPolicy[] _scalarConversionPolicies;
    readonly DepthLimiter _depthLimiter;
    readonly int _maximumStringLength;
    readonly int _maximumCollectionCount;
    readonly bool _propagateExceptions;
    ...
}

好家伙,一次性来了一大堆第一次见的玩意。一个个看,一个个猜,先弄懂大意再说。

ILogEventPropertyFactoryILogEventPropertyValueFactory 接口

首先就是 ILogEventPropertyFactoryILogEventPropertyValueFactory 接口,从 Factory 名称大概猜出来,属于工厂模式的一种设计,是构造对应的 LogEventPropertyLogEventPropertyValue 对象。这些接口位于 ./Core 文件夹内部,表明是非常重要的接口。

public interface ILogEventPropertyValueFactory
{
    LogEventPropertyValue CreatePropertyValue(object value, bool destructureObjects = false);
}

public interface ILogEventPropertyFactory
{
    LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false);
}

从函数签名上看,基本和猜测一致,用于构建对应的对象,最后一个输入参数指明是否以解构对象的方式进行构建。

IDestructuringPolicyIScalarConversionPolicy 接口

PropertyValueConverter 类中,还保有对 IDestructuringPolicyIScalarConversionPolicy 接口数组的引用。字面意义上,这两个接口均描述的是一个策略,且保存在 ./Core 文件夹下。这两个接口有什么用,做什么,先看下代码吧。

public interface IDestructuringPolicy
{
    bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result);
}
public interface IScalarConversionPolicy
{
    bool TryConvertToScalar(object value, out ScalarValue result);
}

从这两个接口内的函数签名可以猜测出它们分别将日志数据转化成对应的数据类型。 bool 返回值标注该转换是否成功,输入参数中被 out 修饰的变量则可以看成是转换成功后的结果变量。以此,可以发现, IDestrucuringPolicy 接口用于将数据转换成 LogEventPropertyValue 对象,而 IScalarConversionPolicy 接口则将数据转换成 ScalarValue

DepthLimiter

DepthLimiter 类对象是 ProertyValueConverter 所持有的最后一个复杂的类对象。有意思的是,该类的作用范围放在 PropertyValueConverter 类的内部。换句话来说,就是 DepthLimiterPropertyValueConveter 的内部类。

class DepthLimiter : ILogEventPropertyValueFactory
{
    [ThreadStatic]
    static int _currentDepth;
    readonly int _maximumDestructingDepth;
    readonly PropertyValueConverter _propertyValueConverter;

    public static void SetCurrentDepth(int depth)
    {
        _currentDepth = depth;
    }
    public LogEventPropertyValue CreatePropertyValue(object value, Destructuring destructuring)
    {
        var storedDepth = _currentDepth;

        var result = DefaultIfMaximumDepth(storedDepth) ??
            _propertyValueConverter.CreatePropertyValue(value, destructuring, storedDepth + 1);

        _currentDepth = storedDepth;

        return result;
    }
    ...
}

可以看到, DepthLimiter 也是一个处理日志消息数据与 LogEventPropertyValue 相绑定的过程类。和 PropertyValueConverter 类一样,它也继承于 ILogEventPropertyValueFactory 接口。然而, DepthLimiterPropertyValueConverter 不同之处在于:

  1. DepthLimiter 只负责对 LogEventPropertyValue 的创建,而 PropertyValueConveter 不仅负责前者的构建,还负责对 LogEventProperty 的创建,这一点从所继承的接口数目可以看出来。也就是说, PropertyValueConverter 的作用范围比 DepthLimiter 大。
  2. DepthLimiter 类不负责具体的构建,这一点可以从其包含 _propertyValueConverter 字段可以看出来,具体对日志数据的渲染还是交给内部的 PropertyValueConverter 类对象来处理。那么 DepthLimiter 负责什么呢?它负责记录处理的深度,这一点从 _currentDepth 这个变量可以看出来。

考虑这样一个数据:

class A
{
    public B B { get; }
}

class B
{
    public int C { get; }
}

数据A具有非常复杂的形式,A 中有 B 的属性,B 内有 int 类型的C属性,一共三层。最外层为 A,最内层为 C。如果采用解构的方式记录数据 A 的对象,那么需要深入 3 层迭代转换。比如说C数据应该放在 ScalarValue 中,B和A应该放在 StructValue 中。Serilog通过 PropertyValueConverterDepthLimiter 的相互引用配合达到递归转换的目的。也就是说, DepthLimiter 负责维护递归转换时当前转换数据所处的深度。

剩余参数

PropertyValueConverter 中,还剩下一些较为简单的数据参数。在了解了 DepthLimiter 之后,剩余的三个参数可以很明显看出来。

_maximumStringLength
_maximumCollectionCount
_propgateExceptions

接口函数

接下来,我们把注意力再回到 PropertyValueConverter 类中对 IDestructuringPolicyIScalarConversionPolicy 接口函数实现的部分。

public LogEventProperty CreateProperty(string name, object value, bool destructureObjects = false)
{
    return new LogEventProperty(name, CreatePropertyValue(value, destructureObjects));
}

public LogEventPropertyValue CreatePropertyValue(object value, bool destructureObjects = false)
{
    return CreatePropertyValue(value, destructureObjects, 1);
}

可以看到,无论是哪种接口,其内部直接或间接在调用 CreatePropertyValue(object value, Destructuring destructuring, int depth) 函数(类文件中第126行)。下面是该函数的核心部分代码。

LogEventPropertyValue CreatePropertyValue(object value, Destructuring destructuring, int depth)
{
    if (value == null)
      return new ScalarValue(null);
    ...
    // ScalarValue的转换策略
    foreach (var scalarConversionPolicy in _scalarConversionPolicies)
    {
        if (scalarConversionPolicy.TryConvertToScalar(value, out var converted))
            return converted;
    }

    // 设置深度
    DepthLimiter.SetCurrentDepth(depth);
    // 如果Token采用解构渲染,则使用解构策略尝试解析数据
    if (destructuring == Destructuring.Destructure)
    {
        foreach (var destructuringPolicy in _destructuringPolicies)
        {
            if (destructuringPolicy.TryDestructure(value, _depthLimiter, out var result))
                return result;
        }
    }
    // 获取日志数据的数据类型,利用内置的解析策略对其解析
    var valueType = value.GetType();
    if (TryConvertEnumerable(value, destructuring, valueType, out var enumerableResult))
        return enumerableResult;
    if (TryConvertValueTuple(value, destructuring, valueType, out var tupleResult))
        return tupleResult;
    if (TryConvertCompilerGeneratedType(value, destructuring, valueType, out var compilerGeneratedResult))
        return compilerGeneratedResult;
    // 如果以上策略都不满足的话,直接构造
    return new ScalarValue(value.ToString());
}

这个函数非常复杂,但大体上分成以下几个步骤。

  1. 如果传入的数据是一个 null 对象,则直接使用 ScalarValue 构造。
  2. 利用内部保存的 IScalarConversionPolicy 数组尝试对日志数据绑定,如果能绑定,则将结果返回,否则继续。
  3. 设置深度值,之所以将深度值放在这里设置,是因为后续操作可能会用到这个深度值。
  4. 利用内部维护的 IDestructingPolicy 数组尝试对日志数据绑定,如果能绑定,则返回结果值,否则继续。
  5. 获取当前日志数据的类型,并采用3个内置的绑定规则进行绑定。如果能成功,则返回结果值,如果所有都无法成功,则认为该对象无法在现有的规则下绑定,则采用 ScalarValue 对其绑定。

第一步好理解,如果值为空,则直接采用 ScalarValue 对其渲染, ScalarValue 会将其渲染成 null 。之所以不返回 null 对象,则是尝试对 null 操作调用函数等操作会抛出异常,而如果在调用前编写判断的逻辑会大大干扰程序员编写代码的逻辑。因此,直接使用 new ScalarValue(null) 会更加方便,不易出错。

第二步则是利用多个 IScalarConversionPolicy 策略类将传入的日志对象数据尝试转换成 ScalarValue 对象,一旦某个策略能够成功转换,那么直接跳出。

第三步和第四步的作用是尝试利用 IDestructuringPolicy 策略类对输入的日志数据进行转换。该部分将日志数据按照策略的要求尝试转换成 LogEventPropertyValue 类对象,和之前一样,一旦某个策略成功,则直接跳出。Serilog中定义了一组相关策略,其类代码保存在 ./Policies 文件夹中。

最后,如果以上所有策略都没法满足时,则尝试采用内置的策略。

总结

总的来说,当记录日志时,其所附带的日志数据通过 PropertyValueConverter 类对象将其转化成一系列的 LogEventPropertyValue 对象,最终变成 LogEventProperty 对象。这些对象有着不同的语义,在渲染的行为方式上表现有所不同,比如说 ScalarValue 表示的一个原始数据,它采用 ToString 的方法对数据进行渲染; SequenceValue 表示一类数组,采用 [] 方式对其渲染; DictionaryValue 表示一组键值对,采用 {} 方式对其渲染; StructureValue 表示一个复杂且需要解构的日志数据对象,采用 {} 方式渲染。除此之外, PropertyValueConverter 还包含一个内部类 DepthLimiter ,该类是对 PropertyValueConverter 类的进一步装饰,让数据的解析添加了深度信息。在下一篇中,我们进一步来看 IScalarConversionPolicyIDestructuringPolicy 以及它们的实现类。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK