上一篇中,我们主要研究了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结构体。

Serilog 源码解析——数据的保存(上)的更多相关文章

  1. Serilog 源码解析——数据的保存(下)

    上一篇中,我们提到了日志数据是如何进行解析了.然而,Serilog 灵活采用了不同的策略(Policy)决定一个日志对象如何解析到LogEventPropertyValue的子类对象中,即采用了ISc ...

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

    上一篇文章中揭露了日志数据的绑定逻辑,主要说明了日志数据绑定的结果信息,即EventProperty结构体和LogEventProperty类,以及日志数据与具名属性Token的绑定类Property ...

  3. Serilog 源码解析——总览

    背景 大家好,考虑到在最近这些天,闲来无事,找了个类库好好研究下别人写的高质量代码,颇有收获,打算和大家分享下.考虑到最近在自学 ASP.NET Core 的相关开发,对 Serilog 这个日志记录 ...

  4. Serilog源码解析——使用方法

    在上两篇文章(链接1和链接2)中,我们通过一个简易 demo 了解到了一个简单的日志记录类库所需要的功能,即一条日志有哪些数据,以及如何通过一次记录的方式将同一条日志消息记录到多个日志媒介中.在本文中 ...

  5. Vue源码解析---数据的双向绑定

    本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...

  6. 渣渣菜鸡的 ElasticSearch 源码解析 —— 启动流程(上)

    关注我 转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/08/11/es-code02/ 前提 上篇文章写了 ElasticSearch 源码解析 -- ...

  7. Serilog 源码解析——解析字符串模板

    大家好啊,上一篇中我们谈到 Serilog 是如何决定日志记录的目的地的,那么从这篇开始,我们着重于 Serilog 是向 Sinks 中记录什么的,这个大功能比较复杂,我尝试再将其再拆分成几个小块方 ...

  8. hibernate部分源码解析and解决工作上关于hibernate的一个问题例子(包含oracle中新建表为何列名全转为大写且通过hibernate取数时如何不用再次遍历将列名(key)值转为小写)

    最近在研究系统启动时将数据加载到内存非常耗时,想着是否有办法优化!经过日志打印测试发现查询时间(查询时间:将数据库数据查询到系统中并转为List<Map>或List<*.Class& ...

  9. Serilog 源码解析——Sink 的实现

    在上一篇中,我们简单地查看了 Serilog 的整体需求和大体结构.从这一篇开始,本文开始涉及 Serilog 内的相关实现,着重解决第一个问题,即 Serilog 向哪里写入日志数据的.(系列目录) ...

随机推荐

  1. 多测师讲解pyhon__hashlib_高级讲师肖sir

    一.加密,加密成16进制的字符串 # import hashlib # 导入hashlib模块# md = hashlib.md5() # 获取一个md5加密算法对象# md.update('需要加密 ...

  2. jenkins 构建 job 并获取其状态的实现

    目录 BACKGROUND INVESTIGATION I 1. 连续触发的相同 job 构建不会重复入队 2. 连续触发的不同 job 构建会各自入队 3. 参数变动的相同 job 构建将分别入队 ...

  3. lambda函数小结

    C++中的lambda函数 lambda函数是函数式编程中的概念,由C++11引入,成为现代C++中重要的特性. 所谓lambda函数就是匿名函数,语法结构: [capture list] (para ...

  4. 【二分】CF Round #587 (Div. 3)E2 Numerical Sequence (hard version)

    题目大意 有一个无限长的数字序列,其组成为1 1 2 1 2 3 1.......1 2 ... n...,即重复的1~1,1~2....1~n,给你一个\(k\),求第\(k(k<=10^{1 ...

  5. 如何获取前端提交来得json格式数据

    composer.json { "require": { "guzzlehttp/guzzle": "~6.0" } } composer ...

  6. linux(centos8):用systemctl管理war包形式的jenkins(java 14 / jenkins 2.257)

    一,如何安装jenkins? 参见: https://www.cnblogs.com/architectforest/p/13685904.html 说明:刘宏缔的架构森林是一个专注架构的博客,地址: ...

  7. C# Webservice中如何实现方法重载--(方法名同名时出现的问题)

    本文摘抄自:http://blog.sina.com.cn/s/blog_53b720bb0100voh3.html 1.Webservice中的方法重载问题(1)在要重载的WebMethod上打个M ...

  8. Linux入门到放弃之四《磁盘管理》

    一,磁盘管理 1.添加一个新磁盘/dev/sdb,用fdisk工具给磁盘分区,要求:一个主分区,一个扩展分区,两个逻辑分区: (1)去虚拟机设置添加一块硬盘,大小自定义 (2)重启系统 命令:rebo ...

  9. node.js操作MySQL数据库

    MySQL数据库作为最流行的开源数据库.基本上是每个web开发者必须要掌握的数据库程序之一了. 基本使用 node.js上,最受欢迎的mysql包就是mysql模块. npm install mysq ...

  10. swd composer.json

    { "require": { "php-amqplib/php-amqplib": "2.6.*", "tmtbe/swooled ...