Serilog 是一种序列化器。在许多情况下,它具有良好的默认行为,能够满足其目的,但有时也需要指示 Serilog 如何存储附加到日志事件上的属性。
Serilog 使用一些不寻常的术语来指代 .NET 对象如何映射到其内部(与接收器无关的)属性表示。这些术语的详细解释如下,所以如果你打算阅读整页内容,可以跳过这一部分。
-
字符串化(Stringification) 是指获取一个提供.NET 属性,并调用其 ToString() 方法,以便到达接收器的表示是一个简单的字符串。
-
解构(Destructuring) 是指将复杂的 .NET 对象转换为结构的过程,这些结构可能会被表示为 JSON 对象或 XML 块。
-
标量(Scalars) 是指可以表示为原子值的 .NET 类型;大多数值类型如 int 都符合这个描述,但一些引用类型如 Uri 和 string 也符合。
01为什么需要控制表示方式?
记录对象到日志的方式可能有很多种。大多数类型可以很好地表示为字符串或简单值,但有些则更适合记录为集合,还有些则适合记录为具有命名属性的结构体。
日志事件属性的存储表示方式对日志的大小以及获取这些数据所需的内存和处理开销有很大影响。
考虑到这一点,我们来看看如何在简单情况下配置 Serilog。
02默认行为
当在日志事件中指定属性时,Serilog 会尽力确定正确的表示方式。
简单的标量值
var count = 456;
Log.Information("Retrieved {Count} records", count);
在这种情况下,Count 属性的存储方式几乎没有歧义。作为一个简单的整数值,Serilog 会选择这种表示方式。
{ "Count": 456 }
这些示例使用 JSON,但相同的原则也适用于其他格式。
开箱即用,Serilog 识别以下列表作为基本标量类型,无论是否应用了其他策略:
-
布尔值 - bool
-
数值 - byte, short, ushort, int, uint, long, ulong, float, double, decimal
-
字符串 - string, byte[]
-
时间 - DateTime, DateTimeOffset, TimeSpan
-
其他 - Guid, Uri
-
可空类型 - 上述类型的可空版本
集合
如果作为属性传递的对象是IEnumerable,Serilog会将该属性视为集合。
var fruit = new[] { "Apple", "Pear", "Orange" };
Log.Information("In my bowl I have {Fruit}", fruit);
对应的JSON包括一个数组。
{ "Fruit": ["Apple", "Pear", "Orange"] }
Serilog 之所以这样选择,是因为大多数可枚举类型关注的是其元素,而作为结构或字符串表示不佳。
Serilog还识别Dictionary<TKey,TValue>,只要键类型是上面列出的标量类型之一。
var fruit = new Dictionary<string,int> {{ "Apple", 1}, { "Pear", 5 }};
Log.Information("In my bowl I have {Fruit}", fruit);
支持字典的格式器可以将属性记录为字典。
{ "Fruit": { "Apple": 1, "Pear": 5 }}
IDictionary<TKey,TValue> - 实现字典接口的对象不会被序列化为字典。首先,因为在.NET中检查泛型接口兼容性效率较低,其次,一个对象可能实现多个泛型字典接口,从而产生歧义。
对象
除了上述特殊处理的类型之外,Serilog 很难智能地选择数据的渲染和持久化方式。未明确用于序列化的对象往往序列化效果很差。
SqlConnection conn = ...;
Log.Information("Connected to {Connection}", conn);
(哎呀!如何序列化一个 SqlConnection 对象?)
当 Serilog 无法识别该类型且未指定操作符(见下文)时,对象将使用 ToString() 方法进行渲染。
03保留对象结构
在许多情况下,如果可能的话,将日志事件属性序列化为结构化对象是有意义的。数据传输对象(DTOs)、消息、事件和模型通常最好通过将其分解为具有值的属性来进行日志记录。
为此,Serilog 提供了 @ 解构操作符。
var sensorInput = new { Latitude = 25, Longitude = 134 };
Log.Information("Processing {@SensorInput}", sensorInput);
(“解构”一词是从各种编程语言中借用的;它是一种用于从结构化数据中提取值的模式匹配风格。目前,Serilog 中的用法仅与该术语在概念上相关,但未来对该运算符的扩展可能会更准确地匹配其更广泛的定义。)
自定义存储的数据
通常,对复杂对象的只有部分属性是感兴趣的。要自定义 Serilog 如何持久化解构的复杂类型,可以在 LoggerConfiguration 上使用 Destructure 配置对象:
Log.Logger = new LoggerConfiguration()
.Destructure.ByTransforming<HttpRequest>(
r => new { RawUrl = r.RawUrl, Method = r.Method })
.WriteTo...
这个示例将HttpRequest类型的对象转换为仅保留RawUrl和Method属性的新对象。可以使用多种不同的解构策略,也可以通过实现 IDestructuringPolicy 创建自定义策略。
注意:提供给 Destructure.ByTransforming() 的函数必须返回与传入类型不同的类型,否则会递归调用。可以使用自定义的 IDestructuringPolicy 来实现条件转换。
操作符与格式
虽然操作符和格式都影响属性的表示方式,但它们的作用是不同的。操作符在捕获属性时被应用,用于以某种方式保留或结构化数据。而格式则仅在将属性显示为文本时使用,不会影响序列化的表示。
格式化集合和结构
当为复杂属性指定格式字符串时,它们通常会被忽略。只有可枚举类型会考虑格式字符串,并在显示时将其传递给元素。
04强制字符串化
有时,日志记录的对象类型可能不完全确定,或者可能以不希望在日志事件中保留的方式变化。在这些情况下,$ 字符串化操作符将把属性值转换为字符串,然后再进行其他处理,无论其类型或实现的接口是什么。
var unknown = new[] { 1, 2, 3 }
Log.Information("Received {$Data}", unknown);
尽管 unknown 是一个枚举类型,但它被捕获并以字符串形式呈现。
Received "System.Int32[]"
注:相关源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner