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

基础功能

在开始看 Serilog 怎么将日志记录到 Sinks 之前,先看下整体框架。首先,我们需要了解 Serilog 中最常用的一个接口ILogger,它提供了对外记录日志的所有功能 API 方法。

ILogger(核心接口)

在 Serilog 根目录下,保存有 4 个代码文件。类似于 LogDemo,ILogger内包含各种功能API方法,LogConfiguration用于构建对应的ILogger对象。另外,LogExtensions是向ILogger中添加新方法,不是LogConfiguration

为了方便,我们首先看如何使用,在理解完使用方法,再回过头来看怎么创建。首先是ILogger, 它提供了大量的使用方法,按照功能主要分成以下三类。

方法名 说明
ForContext系列 构造子日志记录对象,并添加额外数据
Write系列,XXX(日志等级名)系列 日志记录功能
BindXXX 系列 输出模板、属性绑定相关

这里面的方法,对我们而言,第二类方法是用的最多地,我们就先看 Serilog 是如何记录日志的吧。

Log(静态方法类)

这是一个静态类,可以看到内部本质上是对ILogger的进一步包装,并将所有API方法暴露出来,如下。

public static class Log
{
static ILogger _logger = SilentLogger.Instance; public static Logger
{
get => _logger;
set => _logger = value ?? throw ...
} public static void Write(LogEventLevel level, string messageTemplate)
...
}

顺带提一句,类库中的SilentLogger类是对ILogger的一个空实现,它可以看成是一个具有调用功能的空类。

在了解到了最为核心的ILogger接口后,接下来需要了解的是描述日志事件的LogEvent类,该类在 Events 文件夹下,其作为Write的输入参数,可以将其想象成LogDemo中的LogData类,只不过它包含了更多的数据信息。另外,LogEventLevel是一个枚举,同样位于 Events 文件夹下,该类的内容和 LogDemo 中的LogLevel完全一致。

LogEvent(日志事件类)

在 Serilog 中,每当我们发生一次日志记录的行为时,Serilog 都将其封装到一个类中方便使用,即LogEvent类。和 LogDemo 中的LogData一样,LogEvent类包含一些描述日志事件的数据。

public class LogEvent
{
public DateTimeOffset Timestamp { get; }
public LogEventLevel Level { get; }
public Exception Exception { get; }
public MessageTemplate MessageTemplate { get; } private readonly Dictionary<string, LogEventPropertyValue> _properties; internal LogEvent Copy()
{
...
}
}

可以看到,在LogEvent中,有若干字段和属性描述一个日志事件。Timestamp属性描述日志记录的时间,采用DateTimeOffset这一类型可以统一不同时区下的服务器时间点,确保时间上的统一。Level就不用多说,描述日志的等级。Exception属性可以保存任意异常类数据,该属性常用在 Error 和 Fatal 等级中,需要保存异常信息时使用。至于后续的MessageTemplateLogEventPropertyValue,从字面意义上看,属于字符串消息模板和记录数据时所用到,目前我们主力研究记录到 Sink 的处理逻辑,故这两块暂时不关心。

此外,在LogEvent类中,有一个很特别的函数,名为Copy函数,这个函数是根据当前LogEvent对象复制出了一个相同的LogEvent对象。这个方法可以看成是设计模式中原型模式的一种实现,只不过这个类没有利用IClonable接口来实现。

Core 目录下的功能类

ILogEventSink接口

在 LogDemo 中,我们通过ILogTarget接口定义不同的日志记录目的地。类似地,在 Serilog 中,所有的 Sink 通过ILogEventSink定义统一的日志记录接口。该接口如下所示。

public interface ILogEventSink
{
void Emit(LogEvent logEvent);
}

该接口形式简单,只有一个函数,输入参数为LogEvent对象,无返回值,这一点和 LogDemo 中的ILogTarget接口很像。如果想实现一个 ConsoleSink,只需要将继承该接口并将LogEvent对象字符串数据写入到Console即可。实际上,在 Serilog.Sinks.Console 中其核心功能就是这么实现的。

Logger

Logger类是对ILogger接口的默认实现。类似于 LogDemo 中的Logger,该类给所有日志记录的使用提供了 API 方法。考虑到本篇只关心日志向哪里写入的。因此,我们只关心其内部的部分字段属性和方法。

public sealed class Logger : ILogger, ILogEventSink, IDisposable
{
readonly ILogEventSink _sink;
readonly Action _dispose;
readonly LogEventLevel _minimumLevel; // 361行到375行
public void Write(LogEventLevel level, Exception exception, string messageTemplate, params object[] propertyValues)
{
if (!IsEnabled(level)) return;
if (messageTemplate == null) return; if (propertyValues != null && propertyValues.GetType() != typeof(object[]))
propertyValues = new object[] {propertyValues}; // 解析日志模板
_messageTemplateProcessor.Process(messageTemplate, propertyValues, out var parsedTemplate, out var boundProperties); // 构造日志事件对象
var logEvent = new LogEvent(DateTimeOffset.Now, level, exception, parsedTemplate, boundProperties);
// 将日志事件分发出去
Dispatch(logEvent);
} public void Dispatch(LogEvent logEvent)
{
...
// 将日志事件交给Sink进行记录
_sink.Emit(logEvent);
}
}

考虑到篇幅,这里我去掉了部分和当前功能无关的代码,只保留最为核心的代码。

  1. 首先,我们看下继承关系,Logger类除继承ILogger之外,还继承ILogEventSink接口,这个继承关系看起来很奇怪,但细想也觉得正常,一个日志记录器不光可以当日志事件的发生器,也可以当其接收器。换而言之,可以将一条日志事件写到另一个日志记录器中,由另一个日志记录器记录到其他 Sinks 中。此外,该类还继承了IDisposable接口,按照逻辑需求来讲,Logger是没有东西需要释放的,其需要释放的通常是内部包含的一些对象,比如说 FileSink 如果长时间维持一个文件句柄的话,则需要在Logger回收后被动释放,因此,这导致了Logger需要维护一组待释放的对象进行释放。在Logger内部中,通过添加Action函数钩子的方式进行释放。

  2. 之后,我们会发现所有的写入日志方法直接或间接地调用上面给出的Write方法。在该方法的逻辑中,第一行用来判断日志的等级是否满足条件,也就是一类全局的过滤条件,第二行则是判断是否给出日志的输出模板。随后_messageTemplateProcessor看这个意思是解析模板和数据(暂且不明,不过多关注)。再往下,则是构造对应的LogEvent对象。最后通过Dispatch方法将日志分发到ILogEventSink。在Dispatch中,前半部分逻辑和本篇关系不大,最后通过ILogEventSink将日志消息发送出去。

看到这里,可能会有人好奇一点,Logger应该拥有一组ILogEventSink对象才对,这样才能够实现一次向多个 Sink 中写入日志信息,但Logger只维护一个ILogEventSink对象,它是怎么做到一次向多个 Sink 中写入日志的呢?我们接着往下看。

功能性 Sink

在 Serilog 的 ./Core/Sinks 文件夹中可以发现,这里面有非常多的ILogEventSink的实现类。这些实现类都不是向具体的媒介(控制台、文件等)写入日志,反而,他们都是给其他的Sink扩展新功能,典型装饰模式的一种实现。在这个文件夹下,我把部分核心功能摘录出来,如下。(v2.10.0又添加了一些其他的装饰类,这里就不过多说明了)。

class ConditionalSink : ILogEventSink
{
readonly ILogEventSink _warpped;
readonly Func<LogEvent, bool> _condition;
...
public void Emit(LogEvent logEvent)
{
if (_condition(logEvent)) _wrapped.Emit(logEvent);
}
...
}

ConditionalSink功能非常简单,它也包含了一个ILogEventSink对象,此外,还包含一个Func<LogEvent, bool>的泛型委托。这个委托可以按照LogEvent对象满足某种指定要求做过滤。从Emit函数内可以看出,只有在满足条件时才会将日志事件发送到对应的 Sink 中。它可以看成是带有条件写入的 Sink,这一点和也就是局部过滤功能实现的核心之处。

public interface ILogEventFilter
{
bool IsEnabled(LogEvent logEvent);
}

FilteringSink所作的事情和ConditiaonalSink一样,除了 Sink 对象外,它还维护了一组ILogEventFilter数组用来指定多个日志过滤条件,而ILogEventFilter接口如上所示,其内部就是按日志对象进行过滤。而RestrictedSink内除ILogEventSink对象外,还有一个LoggingLevelSwitch对象,这个对象用来描述日志记录器能够记录的最小日志等级,所以RestrictedSink所实现的是依照日志等级的比较判断是否输出日志。

sealed class SecondaryLoggerSink : ILogEventSink
{
readonly ILogger _logger;
readonly bool _attemptDispose;
...
public void Emit(LogEvent logEvent)
{
...
var copy = logEvent.Copy();
_logger.Write(copy);
}
}

和上述其他的ILogEventSink的继承类相比,SecondaryLoggerSink在其内部并没有保留对某个ILogEventSink的引用。相反,它保留对给定的ILogger对象的引用,这种好处是我们可以让一个日志记录器作为另一个日志记录的Sink。该类另外的一个变量_attemptDispose表示该类是否需要执行内部ILogger对象的释放,之所以这样做是因为有的时候Logger对象并不一定需要释放,通常由父日志记录器所创建出来的子日志记录器不需要释放,其资源释放可以由父日志记录器进行管理。

class SafeAggregateSink : ILogEventSink
{
readonly ILogEventSink[] _sinks;
...
public void Emit(LogEvent logEvent)
{
foreach (var sink in _sinks)
{
...
sink.Emit(logEvent);
...
}
}
}

除此之外,还剩下AggregrateSinkSafeAggregrateSink这两个 Sink 也继承ILogEventSink接口,且内部都引用了ILogEventSink数组,且在Emit函数中基本都是对数组内的ILogEventSink对象遍历,并调用这些对象内的Emit函数。二者均在Emit函数内将所有异常捕捉起来,但AggregateSink会在捕捉后将这些异常以AggreateException异常再次抛出。这两个类与之前的类不同,它们将多个 Sink 集合起来,让外界仍以单一的 Sink 来使用。其好处在于,Logger的设计者不需要关注到底有一个还是多个 Sink,如果有多个 Sink,只需要用这两个类将多个 Sink 包裹起来,外界将这一组 Sink 当成一个 Sink 来使用。

为什么要这样设计?实际上,对Logger类来说,它并不需要关心记录的 Sink 有一个还是多个,是什么样的状态,达到什么样的条件才能记录,毕竟这些都非常的复杂。对于Logger来讲,它要做的只有一件事,只要将日志事件向ILogEventSink对象中发出即可。为达到这样的目的,Serilog 利用设计模式中的装饰模式和组合模式来降低Logger的设计负担。主要体现在两个方面。

  1. 通过装饰模式实现带有复杂功能的 Sink,通常通过继承ILogEventSink并内部保有一个ILogEventSink对象来进行功能扩展,前面所提到的ConditionalSinkFilteringSinkRestrictedSink等都属于带有扩展功能的Sink,可以看到,其构造函数均需要外界提供额外的ILogEventSink对象。 此外,这些装饰类还可以嵌套,即一个装饰类可以拥有另一个装饰类对象,实现功能的聚合。

  2. 通过组合模式将一组 Sink 以单一 Sink 对象的方式暴露出来,AggregrateSinkSafeAggregrateSink做的就是这件事。就算Logger需要将日志记录到多个Sink中,从Logger的角度来看,它也只是写入到一个ILogEventSink对象中,这让Logger设计者不需要为了到底是一个还是多个 Sink 而头疼。举个例子,假如你有一个 ConsoleSink,它的作用是将日志输出到控制台,以及一个将日志输出到文件的 FileSink。如果想利用Logger对象将日志同时输出到控制台和文件,我们只需要构建一个AggregateSink并将 ConsoleSink 和 FileSink 对象放置到其内部的数组中,再将AggregrateSink作为Logger中的ILogEventSink的对象,那么Logger能自动将日志分别记录到这两个地方。

总结

以上就是整个 Sink 功能的说明,可以看到的是,这块和之前提到的 LogDemo 项目非常的像。我相信如果在之前对 LogDemo 能够理解的人在这块能够找到非常熟悉的感觉。从下一篇开始,我将开始揭露 Serilog 是如何将 LogEvent 这样的日志事件转换成最终写入到各个Sink中的字符串信息的。

最新文章

  1. iOS BUG: Unbalanced calls to begin/end appearance transitions for &lt;XXXViewController: 0x7fcea3730650&gt;.
  2. TJ2016 CTF Write up
  3. 在thinkphp框架模板中引用session
  4. bitset常用函数用法记录 (转载)
  5. jquery 2.0.3代码结构
  6. JavaScript高级程序设计26.pdf
  7. Linux下USB烧写uImage kernel
  8. javascript内存管理(堆和栈)和javascript运行机制
  9. QQ音乐vkey获取,更新播放url
  10. 第十二次oo作业
  11. 【整理】Java 11新特性总结
  12. 定义action的允许访问方式
  13. python网络编程:socket、服务端、客户端
  14. Android中长度单位和边距
  15. svg矢量图在flex布局中样式扭曲的问题
  16. Lambda动态排序
  17. Scrum立会报告+燃尽图(十月十三日总第四次):前期宣传相关工作
  18. div模拟textarea在ios下不兼容的问题解决
  19. js 获取 本周、上周、本月、上月、本季度、上季度的开始结束日期
  20. java.控制次数,每一组数都要计算。所以有个嵌套

热门文章

  1. matlab中imfinfo 有关图形文件的信息
  2. RHSA-2018:1200-重要: patch 安全更新(代码执行)
  3. TP5发送邮件
  4. 【转】Linux-CentOS7设置程序开启自启步骤!
  5. .NET Core+MongoDB集群搭建与实战
  6. VS code开发工具的使用教程
  7. CPU:Central Processing Unit
  8. vue知识点13
  9. Topsis优劣解距离法 mlx代码
  10. Jquery中$(&quot;&quot;).事件()和$(&quot;&quot;).on(&quot;事件&quot;,&quot;指定的元素&quot;,function(){});的区别(jQuery动态绑定事件)