zoukankan      html  css  js  c++  java
  • Serilog 源码解析——Sink 的实现

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

  • 相关阅读:
    性能优化
    几种跨域处理
    重温前端基础之-数组去重
    移动端常遇到的问题
    WPF 应用
    WPF 应用
    WPF 应用
    C# 应用
    WPF 应用
    C# 应用
  • 原文地址:https://www.cnblogs.com/iskcal/p/implementation-of-the-sink.html
Copyright © 2011-2022 走看看