• ASP.NET Core 源码学习之 Logging[4]:FileProvider


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

    IMessageWriter

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

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

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

    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 来写入文件 ,并立即刷新到磁盘。

    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 来持久化到磁盘。

    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 地址。

    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();
        }
    
        ...
    }
    

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

    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服务器。

    FileLoggerFactoryExtensions

    最后便是提供扩展方法,方便我们在 Program 中对日志系统进行配置。而扩展方法的实现只是很简单的将我们定义的 FileProvider 注入进去:

    public static class FileLoggerFactoryExtensions
    {
        ...
    
        public static ILoggingBuilder AddFile(this ILoggingBuilder builder, IConfiguration configuration)
        {
            builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration));
            builder.Services.Configure<FileLoggerOptions>(configuration);
            builder.Services.AddSingleton<IConfigureOptions<FileLoggerOptions>>(new FileLoggerConfigureOptions(configuration));
            builder.AddFile();
            return builder;
        }
    
        ...
    }
    

    只提供了 ILoggingBuilder 的扩展,而不再提供 ILoggerFactory 的扩展方法,全力拥抱 .NET Core 2.0

    总结

    通过对网上各种流行的开源日志框架学习借鉴,写了一个 ASP.NET Core 的 Logging 系统的文件扩展,还有很多不足之处,但更多的是一种探索,学习,借此也对 Logging 系统更加了解。而后续会再研究一下分布式日记系统。

    最后附上本文所示代码地址:zero-logging

  • 相关阅读:
    Hibernate使用固定值关联表
    使用spring-data-JPA调用存储过程
    angularjs动态添加节点时,绑定到$scope中
    JPA子查询
    表格表头固定的一种实现方式
    Unicode、UTF8与UTF16
    primefaces4.0基本教程以及增删改查
    tomcat发布webservice
    KonBoot – 只要5K映象文件轻易绕过您的WindowsXP/VISTA/7系统的密码
    Setting .xap MIME Type for Silverlight
  • 原文地址:https://www.cnblogs.com/RainingNight/p/asp-net-core-logging-file.html
Copyright © 2020-2023  润新知