前面几章介绍了 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(new ConcurrentQueue ()) : new BlockingCollection (new ConcurrentQueue (), _queueSize.Value); _cancellationTokenSource = new CancellationTokenSource(); _outputTask = Task.Factory.StartNew ( 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 类,用来根据配置的文件名,来获取滚动频率,比如我们指定日志文件名为 Logs\my-{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(); 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, Funcfilter) { ... } public void Log (LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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(IOptionsMonitoroptions) { _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 时,使用别名即可,然后使用了 模式,监控配置的变化,并进行更新,而不用去重启Web服务器。
FileLoggerFactoryExtensions
最后便是提供扩展方法,方便我们在 Program 中对日志系统进行配置。而扩展方法的实现只是很简单的将我们定义的 FileProvider 注入进去:
public static class FileLoggerFactoryExtensions{ ... public static ILoggingBuilder AddFile(this ILoggingBuilder builder, IConfiguration configuration) { builder.Services.AddSingleton>(new ConfigurationChangeTokenSource (configuration)); builder.Services.Configure (configuration); builder.Services.AddSingleton >(new FileLoggerConfigureOptions(configuration)); builder.AddFile(); return builder; } ...}
只提供了 ILoggingBuilder
的扩展,而不再提供 ILoggerFactory 的扩展方法,全力拥抱 .NET Core 2.0。
总结
通过对网上各种流行的开源日志框架学习借鉴,写了一个 ASP.NET Core 的 Logging 系统的文件扩展,还有很多不足之处,但更多的是一种探索,学习,借此也对 Logging 系统更加了解。而后续会再研究一下分布式日记系统。
最后附上本文所示代码地址:。