澳门新浦京娱乐场网站-www.146.net-新浦京娱乐场官网
做最好的网站

logback日志框架,源码学习之

一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net、NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用法及配置文件,这对于有些小工具、小程序、小网站来说,有点“杀鸡焉俺用牛刀”的感觉,而且如果对这些日志框架不了解,可能输出来的日志性能或效果未毕是与自己所想的,鉴于这几个原因,我自己重复造轮子,编写了一个轻量级的异步写日志的实用工具类(LogAsyncWriter),这个类还是比较简单的,实现思路也很简单,就是把消息日志先入内存队列,然后由异步监听线程从队列中取出日志并批量输出到本地文件中,同时参照各大日志框架,对单个文件过大采取分割生成多个日志文件。

对于一个web项目来说,日志框架是必不可少的,日志的记录可以帮助我们在开发以及维护过程中快速的定位错误。相信很多人听说过slf4j,log4j,logback,JDK Logging等跟日志框架有关的词语,所以这里也简单介绍下他们之间的关系。

文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:

DDLog,即CocoaLumberjack是iOS开发用的最多的日志框架,出自大神Robbie Hanson之手(还有诸多知名开源框架如 XMPPFramework、 CocoaAsyncSocket,都是即时通信领域很基础应用很多的框架)。了解DDLog的源码将有助于我们更好的输出代码中的日志信息,便于定位问题,也能对我们在书写自己的日志框架或者其他模块时有所启发。

前面几章介绍了 ASP.NET Core Logging 系统的配置和使用,而对于 Provider ,微软也提供了 Console, Debug, EventSource, TraceSource 等,但是没有我们最常用的 FilePrivider,而比较流行的 Log4Net , NLog 等也对 ASP.NET Core 的 Logging 系统提供了扩展,但是太过于复杂,而且他们本身就是一个完整的日志系统,功能上会有较多的重合,所以我们不妨自己动手,写一个轻量级的完全基于 ASP.NET Core Logging 系统的 FileProvider

经测试发现性能非常不错,先看示例使用代码:(采取并发多线程同时写入1000000万条日志)

文章首发于个人博客:【

1.背景

Log4j作为常用的日志生成工具,其清除日志的策略却十分有限。只有在RollingFileAppender中可以通过设置MaxFileSize和maxBackupIndex属性来指定要保留的日志文件大小以及个数,从而实现自动清除。

 澳门新浦京娱乐场网站 1

但是实际生产中,我们的真实的需求常常是定时生成日志,然后保留最近几天的日志,历史日志需要及时清理。可是Log4j中的DailyRollingFileAppender这个类却不带属性maxBackupIndex,maxFileSize等,所以无法通过直接配置实现。

针对这种情况,一般方法是写一个定时删除日志的脚本等,这里我们讨论一种通过继承FileAppender,重新实现DailyRollingFileAppender类,并且带有按时间顺序清理日志的功能。

此系列文章将分为以下几篇:- DDLog源码解析一:框架结构-DDLog源码解析二:设计初衷- DDLog源码解析三:FileLogger**

IMessageWriter

首先定义一个日志写入接口:

public interface IMessageWriter : IDisposable
{
    Task WriteMessagesAsync(string message, CancellationToken cancellationToken = default(CancellationToken));
}

只有一个异步的写日志方法,用来将日志写入到文件或者队列中。

 

首先slf4j可以理解为规则的制定者,是一个抽象层,定义了日志相关的接口。log4j,logback,JDK Logging都是slf4j的实现层,只是出处不同,当然使用起来也就各有千秋,这里放一张网上的图更明了的解释了他们之间的关系:

2.具体实现

本文将对DDLog支持的众多Logger中值得分析的文件logger(其余logger基本只涉及系统api的调用)进行分析,并简要分析一些杂乱的知识点。

FileWriter

IMessageWriter 最核心的实现,将日志写入到文件中。

public class FileWriter : IMessageWriter, IDisposable
{
    ...

    public FileWriter(string path, long? fileSizeLimit = null)
    {
        ...
        _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
        _output = new StreamWriter(_underlyingStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
    }

    public async Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        if (_maxFileSize > 0 && _underlyingStream.Length > _maxFileSize)
        {
            return;
        }
        await _output.WriteAsync(message);
        FlushToDisk();
    }

    ...
}

其实现很简单,就是使用最基本的文件 Stream 来写入文件 ,并立即刷新到磁盘。

            Task.Factory.StartNew(() =>
            {
                DateTime startTime = DateTime.Now;
                int logCount = 1000000;
                Parallel.For(1, logCount, (i) =>
                {
                    if (i % 2 == 0)
                    {
                        LogAsyncWriter.Default.Error("测试并发写错误日志-"   i.ToString(), "TestClass.TestLog", i.ToString());
                    }
                    else
                    {
                        LogAsyncWriter.Default.Info("测试并发写普通日志-"   i.ToString(), "TestClass.TestLog", i.ToString());
                    }
                });

                this.Invoke(new MethodInvoker(() =>
                {
                    MessageBox.Show(DateTime.Now.ToString()   ","   logCount   "条日志写完了!,耗时:"   (DateTime.Now - startTime).TotalMilliseconds   "ms");
                }));
            });

            MessageBox.Show(DateTime.Now.ToString()   ",同步方法已结束");
        }

澳门新浦京娱乐场网站 2在这里插入图片描述

2.1代码实现

a.完成自定义的日期类和文件过滤类,为进行文件命名和查找做准备。

 澳门新浦京娱乐场网站 3

澳门新浦京娱乐场网站 4

b.继承FileAppender类,定义好文件输出日期格式以及文件备份参数。

 澳门新浦京娱乐场网站 5

c.重写核心的RollOver函数。

澳门新浦京娱乐场网站 6

d.在RollOver函数中完成对备份数量的监测以及历史日志的删除。

澳门新浦京娱乐场网站 7

FileLogger初始化包含两种初始化操作:默认配置和自定义配置

BatchingWriter

上面 FileWriter 最大的弊端就是每次写日志都要进行一次文件IO操作,效率较低,可以使用定时器,来定时刷新到磁盘,来提高性能。不过,在 Logging.AzureAppServices 中发现了更好的实现方式,即使用批量提交:

public class BatchingWriter : IMessageWriter, IDisposable
{
    ...

    public BatchingWriter(IMessageWriter writer, TimeSpan interval, int? batchSize, int? queueSize)
    {
        ...
        Start();
    }

    private void Start()
    {
        _messageQueue = _queueSize == null ?
            new BlockingCollection<string>(new ConcurrentQueue<string>()) :
            new BlockingCollection<string>(new ConcurrentQueue<string>(), _queueSize.Value);

        _cancellationTokenSource = new CancellationTokenSource();
        _outputTask = Task.Factory.StartNew<Task>(
            ProcessLogQueue,
            null,
            TaskCreationOptions.LongRunning);
    }

    private async Task ProcessLogQueue(object state)
    {
        StringBuilder currentBatch = new StringBuilder();
        while (!_cancellationTokenSource.IsCancellationRequested)
        {
            var limit = _batchSize ?? int.MaxValue;
            while (limit > 0 && _messageQueue.TryTake(out var message))
            {
                currentBatch.Append(message);
                limit--;
            }
            if (currentBatch.Length > 0)
            {
                try
                {
                    await _writer.WriteMessagesAsync(currentBatch.ToString(), _cancellationTokenSource.Token);
                }
                catch
                {
                    // ignored
                }
            }
            await IntervalAsync(_interval, _cancellationTokenSource.Token);
        }
    }

    protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
    {
        return Task.Delay(interval, cancellationToken);
    }

    private void Stop()
    {
        ...
    }

    public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        if (!_messageQueue.IsAddingCompleted)
        {
            try
            {
                _messageQueue.Add(message, _cancellationTokenSource.Token);
            }
            catch
            {
                //cancellation token canceled or CompleteAdding called
            }
        }
        return Task.CompletedTask;
    }

    ...
}

首先定义了一个并发队列,每次写入只需要将日志保存到队列当中,通过配置获取执行周期来定期从队列中取出日志,再使用上面的 FileWriter 来持久化到磁盘。

 执行效果如下图示:

可以看到logback是直接实现的slf4j,而其他的中间还有一个适配层,至于原因也很简单,因为logbackslf4j的作者是一个人。关于这几个框架详细介绍信息,在网上找到一篇讲解的通俗易懂的文章,感兴趣的朋友可以了解下

2.2配置实现

 澳门新浦京娱乐场网站 8

- (instancetype)init { DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init]; return [self initWithLogFileManager:defaultLogFileManager];}- (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager { if ((self = [super init])) { _maximumFileSize = kDDDefaultLogMaxFileSize; _rollingFrequency = kDDDefaultLogRollingFrequency; _automaticallyAppendNewlineForCustomFormatters = YES; logFileManager = aLogFileManager; self.logFormatter = [DDLogFileFormatterDefault new]; } return self;}

RollingFileWriter

使用上面两个类,已满足了最基本的写日志功能,但是在 Log4Net 等日志框架中,我们经常会按一定的频度滚动日志记录文件,也就是 RollingFile 功能,可实现将每天或每小时的日志保存到一个文件中,按文件大小进行滚动等功能。

首先是定义了一个 RollingFrequency 类,用来根据配置的文件名,来获取滚动频率,比如我们指定日志文件名为 Logsmy-{Date}.log,则表示每天滚动一次。

public class RollingFrequency
{
    public static readonly RollingFrequency Date = new RollingFrequency("Date", "yyyyMMdd", TimeSpan.FromDays(1));
    public static readonly RollingFrequency Hour = new RollingFrequency("Hour", "yyyyMMddHH", TimeSpan.FromHours(1));

    public string Name { get; }
    public string Format { get; }
    public TimeSpan Interval { get; }

    RollingFrequency(string name, string format, TimeSpan interval)
    {
        if (name == null) throw new ArgumentNullException(nameof(name));
        Format = format ?? throw new ArgumentNullException(nameof(format));
        Name = "{"   name   "}";
        Interval = interval;
    }

    public DateTime GetCurrentCheckpoint(DateTime instant)
    {
        if (this == Hour)
        {
            return instant.Date.AddHours(instant.Hour);
        }
        return instant.Date;
    }

    public DateTime GetNextCheckpoint(DateTime instant) => GetCurrentCheckpoint(instant).Add(Interval);

    public static bool TryGetRollingFrequency(string pathTemplate, out RollingFrequency specifier)
    {
        if (pathTemplate == null) throw new ArgumentNullException(nameof(pathTemplate));
        var frequencies = new[] { Date, Hour }.Where(s => pathTemplate.Contains(s.Name)).ToArray();
        specifier = frequencies.LastOrDefault();
        return specifier != null;
    }
}

再看一下 RollingFileWriter

public class RollingFileWriter : IMessageWriter, IDisposable
{
    ...

    public RollingFileWriter(string pathFormat, long? fileSizeLimitBytes = null, int? retainedFileCountLimit = null)
    {
        ...
    }

    public Task WriteMessagesAsync(string message, CancellationToken cancellationToken)
    {
        AlignFileWriter();
        return _currentFileWriter.WriteMessagesAsync(message, cancellationToken);
    }

    private void AlignFileWriter()
    {
        DateTime now = DateTime.Now;
        if (!_nextCheckpoint.HasValue)
        {
            OpenFileWriter(now);
        }
        else if (now >= _nextCheckpoint.Value)
        {
            CloseFileWriter();
            OpenFileWriter(now);
        }
    }

    private void OpenFileWriter(DateTime now)
    {
        var currentCheckpoint = _roller.GetCurrentCheckpoint(now);
        _nextCheckpoint = _roller.GetNextCheckpoint(now);

        var existingFiles = Enumerable.Empty<string>();
        try
        {
            existingFiles = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern).Select(Path.GetFileName);
        }
        catch (DirectoryNotFoundException) { }

        var latestForThisCheckpoint = _roller
            .SelectMatches(existingFiles)
            .Where(m => m.DateTime == currentCheckpoint)
            .OrderByDescending(m => m.SequenceNumber)
            .FirstOrDefault();

        var sequence = latestForThisCheckpoint != null ? latestForThisCheckpoint.SequenceNumber : 0;

        const int maxAttempts = 3;
        for (var attempt = 0; attempt < maxAttempts; attempt  )
        {
            string path = _roller.GetLogFilePath(now, sequence);
            try
            {
                _currentFileWriter = new FileWriter(path, _maxfileSizeLimit);
            }
            catch (IOException)
            {
                sequence  ;
                continue;
            }
            RollFiles(path);
            return;
        }
    }

    // 删除超出保留文件数的日志文件
    private void RollFiles(string currentFilePath)
    {
        if (_maxRetainedFiles > 0)
        {
            var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.FileSearchPattern)
                .Select(Path.GetFileName);
            var moveFiles = _roller
                .SelectMatches(potentialMatches)
                .OrderByDescending(m => m.DateTime)
                .ThenByDescending(m => m.SequenceNumber)
                .Skip(_maxRetainedFiles.Value)
                .Select(m => m.Filename);
            foreach (var obsolete in moveFiles)
            {
                System.IO.File.Delete(Path.Combine(_roller.LogFileDirectory, obsolete));
            }
        }
    }

    ...
}

根据滚动频率指定应该创建的文件名,然后调用 FileWriter 进行写入,具体代码可以去看文末贴的 GitHub 地址。

澳门新浦京娱乐场网站 9

我使用这个框架是因为一开始接触的时候就用的这个,后来在网上了解到slf4j logback也确实当下最流行的日志框架,并且自己用着也确实很顺手,也就一直用了下来,关于这个框架相比于其它日志框架的优势,因为我没用过其它的框架,这里也就不做那误人子弟的事了,也只是在网上做过了解,这里给出一篇介绍的比较详细的博文

3.Log4j各配置的含义

Log4j由三个重要的组件构成:日志信息的优先级,日志信息的输出目的地,日志信息的输出格式。日志信息的优先级从高到低有ERROR、WARN、 INFO、DEBUG,分别用来指定这条日志信息的重要程度;日志信息的输出目的地指定了日志将打印到控制台还是文件中;而输出格式则控制了日志信息的显示内容。Log4j支持两种配置文件格式,一种是XML格式的文件,一种是Java特性文件(键=值)。这里,我们主要探讨基于XML的配置方式。

FileLogger默认配置由DDLogFileManagerDefault来实现,DDLogFileManagerDefault类中除可以定义日志文件保存路径外,其余信息都属于写死的固定值(包括下面的静态常量):

FileLogger

FileLogger 则是由上一章讲到的 Logger 来调用的,而在这里,它的作用是首先对日志进行过滤,然后将日志组装成字符串,再调用我们前面定义的 IMessageWriter 进行日志的写入:

public class FileLogger : ILogger, IDisposable
{
    ...

    public FileLogger(IMessageWriter writer, string category, Func<string, LogLevel, bool> filter)
    {
        ...
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }
        if (formatter == null)
        {
            throw new ArgumentNullException(nameof(formatter));
        }
        var builder = new StringBuilder();
        builder.Append(DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
        builder.Append(" [");
        builder.Append(GetLogLevelString(logLevel));
        builder.Append("] ");
        builder.Append(_category);
        builder.Append("[");
        builder.Append(eventId);
        builder.Append("]");
        builder.Append(": ");
        builder.AppendLine(formatter(state, exception));
        if (exception != null)
        {
            builder.AppendLine(exception.ToString());
        }
        _writer.WriteMessagesAsync(builder.ToString()).Wait();
    }

    ...
}

在这里,日志的拼装是写死的,后续可以提供一个可配置的日志渲染器,来自定义输出格式。

因为采用异步,故方法先走到结尾,输出了同步的MsgBox,随后弹出的是100W日志输出到文件后的耗时MsgBox,从截图可以看出,不足1S(当然这里的1S不是真实的输出到本地方件,而是把所有的日志推到了Queue中而矣,但不影响不阻塞业务处理),而本地日志文件的大小达到了263MB(设置最大值的MaxSizeBackup,使其不滚动备份),由此看性能是不错的;

添加配置文件

Spring boot使用是非常方便的,不需要我们有什么额外的配置,因为Spring boot默认支持的就是slf4j logback的日志框架,想要灵活的定制日志策略,只需要我们在src/main/resources下添加配置文件即可,只是默认情况下配置文件的命名需要符合以下规则:

  • logback.xml
  • logback-spring.xml

其中logback-spring.xml是官方推荐的,并且只有使用这种命名规则,才可以配置不同环境使用不同的日志策略这一功能。

3.1配置根logger

基本语法是:

 澳门新浦京娱乐场网站 10

其中,level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。Log4j建议只使用四个级别,优 先级从高到低分别是ERROR、WARN、INFO、DEBUG。

这里,每一个appenderName均是下面需要配置的日志信息的名称。

实际例子:

log4j.rootLogger=INFO ,stdout, ROLLING_ERROR_FILE, ROLLING_INFO_FILE

澳门新浦京娱乐场网站 11

// 日志文件数的最大值NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files// 日志文件占用空间最大值unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB// 日志默认路径为沙盒中caches文件中的Logs文件夹- (NSString *)defaultLogsDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString *baseDir = paths.firstObject; NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"Logs"]; return logsDirectory;}

ConsoleLoggerProvider

FileLoggerProvider 的唯一职责就是创建 FileLogger

[ProviderAlias("File")]
public class FileLoggerProvider : ILoggerProvider
{
    ...

    public FileLoggerProvider(IOptionsMonitor<FileLoggerOptions> options)
    {
        _optionsChangeToken = options.OnChange(UpdateOptions);
        UpdateOptions(options.CurrentValue);
    }

    private void UpdateOptions(FileLoggerOptions options)
    {
        if (RollingFrequency.TryGetRollingFrequency(options.Path, out var r))
        {
            _msgWriter = new RollingFileWriter(options.Path, options.FileSizeLimit, options.RetainedFileCountLimit);
        }
        else
        {
            _msgWriter = new FileWriter(options.Path, options.FileSizeLimit);
        }
        if (options.IsEnabledBatching)
        {
            _msgWriter = new BatchingWriter(_msgWriter, options.FlushPeriod, options.BatchSize, options.BackgroundQueueSize);
        }
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new FileLogger(_msgWriter, categoryName, _filter);
    }

    ...
}

首先是 ProviderAlias 特性,为 Provider 指定一个别名,这样,我们在配置文件中指定 Provider 时,使用别名即可,然后使用了 IOptionsMonitor 模式,监控配置的变化,并进行更新,而不用去重启Web服务器。

澳门新浦京娱乐场网站 12

配置文件详解

首先介绍配置文件的关键节点:

<configuration>:根节点,有三个属性:

  1. scan:当配置文件发生修改时,是否重新加载该配置文件,两个可选值true or false,默认为true
  2. scanPeriod:检测配置文件是否修改的时间周期,当没有给出时间单位时默认单位为毫秒,默认值为一分钟,需要注意的是这个属性只有在scan属性值为true时才生效。
  3. debug:是否打印loback内部日志信息,两个可选值true or false,默认为false

根节点<configuration>有三个重要的子节点,正是这三个子节点的不同组合构成配置文件的基本框架,使得logback.xml配置文件具备很强的灵活性:

  • <appender>:定义日志策略的节点,一个日志策略对应一个<appender>,一个配置文件中可以有零个或者多该节点,但一个配置文件如果没有定义至少一个<appender>,虽然程序不会报错,但就不会有任何的日志信息输出,也失去了意义,该节点有两个必要的属性:

    1. name:指定该节点的名称,方便之后的引用。
    2. class:指定该节点的全限定名,所谓的全限定名就是定义该节点为哪种类型的日志策略,比如我们需要将日志输出到控制台,就需要指定class的值为ch.qos.logback.core.ConsoleAppender;需要将日志输出到文件,则class的值为ch.qos.logback.core.FileAppender等,想要了解所有的appender类型,可以查阅官方文档
  • <logger>:用来设置某个包或者类的日志打印级别,并且可以引用<appender>绑定日志策略,有三个属性:

    1. name:用来指定受此<logger>约束的包或者类。
    2. level:可选属性,用来指定日志的输出级别,如果不设置,那么当前<logger>会继承上级的级别。
    3. additivity:是否向上级传递输出信息,两个可选值true or false,默认为true

在该节点内可以添加子节点<appender-ref>,该节点有一个必填的属性ref,值为我们定义的<appender>节点的name属性的值。

  • <root>:根<logger>一个特殊的<logger>,即默认name属性为root<logger>,因为是根<logger>,所以不存在向上传递一说,故没有additivity属性,所以该节点只有一个level属性。

介绍了根节点的三个主要的子节点,下面再介绍两个不那么重要但可以了解的子节点:

  • <contextName>:设置上下文名称,每个<logger>都关联到<logger>上下文,默认上下文名称为default,但可以使用设置成其他名字,用于区分不同应用程序的记录,一旦设置,不能修改,可以通过 %contextName 来打印日志上下文名称,一般来说我们不用这个属性,可有可无。
  • <property>:用来定义变量的节点,定义变量后,可以使${}来使用变量,两个属性,当定义了多个<appender>的时候还是很有用的:
    1. name:变量名
    2. value:变量值

好了,介绍了上边的节点我们就已经可以搭建一个简单的配置文件框架了,如下:

<?xml version="1.0" encoding="UTF-8"?><!-- 一般根节点不需要写属性了,使用默认的就好 --><configuration> <contextName>demo</contextName> <!-- 该变量代表日志文件存放的目录名 --> <property name="log.dir" value="logs"/> <!-- 该变量代表日志文件名 --> <property name="log.appname" value="eran"/> <!--定义一个将日志输出到控制台的appender,名称为STDOUT --> <appender name="STDOUT" > <!-- 内容待定 --> </appender> <!--定义一个将日志输出到文件的appender,名称为FILE_LOG --> <appender name="FILE_LOG" > <!-- 内容待定 --> </appender> <!-- 指定com.demo包下的日志打印级别为INFO,但是由于没有引用appender,所以该logger不会打印日志信息,日志信息向上传递 --> <logger name="com.demo" level="INFO"/> <!-- 指定最基础的日志输出级别为DEBUG,并且绑定了名为STDOUT的appender,表示将日志信息输出到控制台 --> <root level="debug"> <appender-ref ref="STDOUT" /> </root></configuration>

上面搭建了框架,定义了一个输出到控制台的ConsoleAppender以及输出到文件的FileAppender,下面来细说这两个最基本的日志策略,并介绍最常用的滚动文件策略的RollingFileAppender,这三种类型的日志策略足够我们的日常使用。

先给出一个demo

<!--定义一个将日志输出到控制台的appender,名称为STDOUT --><appender name="STDOUT" > <encoder> <pattern>[Eran]
		

本文由澳门新浦京娱乐场网站发布于www.146.net,转载请注明出处:logback日志框架,源码学习之