上一篇中,我们简单地查看了 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中的字符串信息的。

Serilog 源码解析——Sink 的实现的更多相关文章

  1. Serilog 源码解析——总览

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

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

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

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

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

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

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

  5. Serilog 源码解析——数据的保存(上)

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

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

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

  7. Flume-ng源码解析之Sink组件

    作为启动流程中第二个启动的组件,我们今天来看看Sink的细节 1 Sink Sink在agent中扮演的角色是消费者,将event输送到特定的位置 首先依然是看代码,由代码我们可以看出Sink是一个接 ...

  8. OKHttp源码解析

    http://frodoking.github.io/2015/03/12/android-okhttp/ Android为我们提供了两种HTTP交互的方式:HttpURLConnection 和 A ...

  9. Flume-ng源码解析之Channel组件

    如果还没看过Flume-ng源码解析之启动流程,可以点击Flume-ng源码解析之启动流程 查看 1 接口介绍 组件的分析顺序是按照上一篇中启动顺序来分析的,首先是Channel,然后是Sink,最后 ...

随机推荐

  1. appium 环境安装指引

    1.安装Appium-Python-Client Pip install Appium-Python-Client 2.安装nodejs https://nodejs.org/ 安装成功验证:node ...

  2. thinkphp5.1 阿里云短信接口

    1.首先声明,我个人是没有,accessKeyId    accessKeySecret   SignName     TemplateCode这些参数是需要自己去,阿里云注册,生成的. 我用的密钥( ...

  3. 多路复用select和epoll的区别(转)

    先说下本文框架,先是问题引出,然后概括两个机制的区别和联系,最后介绍每个接口的用法 一.问题引出 联系区别 问题的引出,当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一 ...

  4. ASP课程实例1——简易的手机号抽奖

    本程序用到了最基本的vbscript函数. 请大家注意它们的用法并熟悉asp网页的基本结构. inputbox,mid() ,replace(),rnd(),fix(),document.write ...

  5. day02 Pyhton学习

    1.昨日内容回顾 1.python是一门解释型,弱类型的高级编程语言 优点: 1.优雅简单明确 2.短小快,代码短,代码量小,开发效率高 缺点: 1.运行效率低(相对) 2.python解释器 Cpy ...

  6. day09 Pyhton学习

    一.昨日内容回顾 文件操作 open(文件路径,mode="模式",encoding="编码") 文件路径: 1.绝对路径 从磁盘根目录寻找 2.相对路径 相对 ...

  7. 为什么大部分的程序员学编程,都会选择从C语言开始?

    软件行业经过几十年的发展,编程语言的种类已经越来越多了,而且很多新的编程语言已经在这个领域从开始的默默无闻到如今风风火火,整个编程语言朝着集成化方向发展,这样会导致很多的初学者选择上不像以前那么单一了 ...

  8. 谈谈FTP

    一.关于FTP 1.FTP是什么? FTP,全称"文件传输协议".属于TCP/IP四层模型中的应用层. 2.TCP/IP五层模型有哪些? 如图所示: 用文字叙述(从高层到底层): ...

  9. 转 RabbitMQ 入门教程(PHP版) 使用rabbitmq-delayed-message-exchange插件实现延迟功能

    延迟任务应用场景 场景一:物联网系统经常会遇到向终端下发命令,如果命令一段时间没有应答,就需要设置成超时. 场景二:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单. 场景三:过1分钟给新 ...

  10. 第十九章 DHCP原理介绍

    一.为什么使用DHCP 1.手动为局域网中大量主机配置IP地址.掩码.网关等参数的工作繁琐,容易出错 2.DHCP可以自动为局域网中主机完成TCP/IP协议配置 3.DHCP自动配置避免了IP地址冲突 ...