var services = new ServiceCollection() .AddLogging(builder => // builder.AddConsole() builder.AddFile() );实施
public sealed class FileLoggingOptions { public string LogsDirectory { get; set; } = "Logs"; public string FileFormat { get; set; } = "app-logs-{date}.log"; public LogLevel MinimumLevel { get; set; } = LogLevel.Information; public Func<string, LogLevel, Exception?, string, DateTimeOffset, string?>? LogFormatter { get; set; } }API 使用上保持和 AddConsole 之类的风格,我们添加一个 AddFile 的扩展方法,基于 ILoggerBuild 进行扩展,并且提供一个可选的委托参数用来自定义配置。
public static ILoggingBuilder AddFile(this ILoggingBuilder loggingBuilder, Action<FileLoggingOptions>? optionsConfigure = null) { var options = new FileLoggingOptions(); optionsConfigure?.Invoke(options); return loggingBuilder.AddProvider(new FileLoggerProvider(options)); }扩展方法和自定义配置是 public 的部分,接着来实现非 public 的部分,logging 的重要组成部分分为三部分:
3.ILogger
而我们的 file logging 只是其中一种 ILoggerProvider,主要提供 ILogger 示例来记录具体的日志[ProviderAlias("File")] internal sealed class FileLoggerProvider : ILoggerProvider { private readonly FileLoggingOptions _options; private readonly FileLoggingProcessor _loggingProcessor; private readonly ConcurrentDictionary<string, FileLogger> _loggers = new(); public FileLoggerProvider(FileLoggingOptions options) { _options = options; _options.LogFormatter ??= (category, level, exception, msg, timestamp) => JsonConvert.SerializeObject(new { level, timestamp = timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"), category, msg, exception = exception?.ToString() }, JsonSerializeExtension.DefaultSerializerSettings); _loggingProcessor = new FileLoggingProcessor(options); } public void Dispose() => _loggingProcessor.Dispose(); public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, category => new FileLogger(category, _options, _loggingProcessor)); } }FileLogger 实现如下:
internal sealed class FileLogger(string categoryName, FileLoggingOptions options, FileLoggingProcessor processor) : ILogger { public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if (logLevel < options.MinimumLevel) return; // 堆代码 duidaima.com var timestamp = DateTimeOffset.Now; var msg = formatter(state, exception); var log = options.LogFormatter!.Invoke(categoryName, logLevel, exception, msg, timestamp); if (log is not null) { processor.EnqueueLog(log, timestamp); } } public bool IsEnabled(LogLevel logLevel) => logLevel >= options.MinimumLevel; IDisposable ILogger.BeginScope<TState>(TState state) => NullScope.Instance; }这里的逻辑可以看到还是比较简单的,主要的逻辑看来是 FileLoggingProcessor log 的处理都在这个 FileLoggingProcessor 之中,实现如下:
internal sealed class FileLoggingProcessor : DisposableBase { private readonly FileLoggingOptions _options; private readonly BlockingCollection<(string log, DateTimeOffset timestamp)> _messageQueue = []; private readonly Thread _outputThread; private FileStream? _fileStream; private string? _logFileName; public FileLoggingProcessor(FileLoggingOptions options) { if (!Directory.Exists(options.LogsDirectory)) { try { Directory.CreateDirectory(options.LogsDirectory); } catch (Exception e) { throw new InvalidOperationException("Failed to create log directory", e); } } _options = options; _outputThread = new Thread(ProcessLogQueue) { IsBackground = true, Priority = ThreadPriority.BelowNormal, Name = "FileLoggingProcessor" }; _outputThread.Start(); } public void EnqueueLog(string log, DateTimeOffset dateTimeOffset) { if (_messageQueue.IsAddingCompleted) return; try { _messageQueue.Add((log, dateTimeOffset)); } catch (InvalidOperationException) { } } protected override void Dispose(bool disposing) { if (!disposing) return; _messageQueue.CompleteAdding(); _fileStream?.Flush(); _fileStream?.Dispose(); _messageQueue.Dispose(); } private void ProcessLogQueue() { try { foreach (var message in _messageQueue.GetConsumingEnumerable()) { WriteLoggingEvent(message.log, message.timestamp); } } catch { try { _messageQueue.CompleteAdding(); } catch { // ignored } } } private void WriteLoggingEvent(string log, DateTimeOffset timestamp) { var fileName = _options.FileFormat.Replace("{date}", timestamp.ToString("yyyyMMdd")); var fileInfo = new FileInfo(Path.Combine(_options.LogsDirectory, fileName)); try { var previousFileName = Interlocked.CompareExchange(ref _logFileName, fileInfo.FullName, _logFileName); if (_logFileName != previousFileName) { // file name changed var fs = File.OpenWrite(fileInfo.FullName); var originalWriter = Interlocked.Exchange(ref _fileStream, fs); if (originalWriter is not null) { originalWriter.Flush(); originalWriter.Dispose(); } } Guard.NotNull(_fileStream); var bytes = Encoding.UTF8.GetBytes(log); _fileStream.Write(bytes, 0, bytes.Length); _fileStream.Flush(); } catch (Exception ex) { Console.WriteLine($@"Error when trying to log to file({fileInfo.FullName}) \n" + log + Environment.NewLine + ex); } } }FileLoggingProcessor 主要参考了 Console 里的 processor,为了避免多线程写文件冲突,我们只在一个线程中写文件。为了支持 rolling update 我们会根据 log file format 判断当前要写入的文件名称是否发生变化,如果发生了变化需要先将之前的文件流释放,针对新文件开启新的文件流。
using var services = new ServiceCollection() .AddLogging(builder => builder.AddFile() ) .BuildServiceProvider(); var logger = services.GetRequiredService<ILoggerFactory>() .CreateLogger("test"); while (!ApplicationHelper.ExitToken.IsCancellationRequested) { logger.LogInformation("Echo time: {Time}", DateTimeOffset.Now); Thread.Sleep(500); }这里写了一个简单的 log 示例,输出一下当前的时间
using var services = new ServiceCollection() .AddLogging(builder => // builder.AddConsole() builder.AddFile(options => options.LogFormatter = (category, level, exception, msg, timestamp) => $"{timestamp} - [{category}] {level} - {msg}\n{exception}") ) .BuildServiceProvider();此时输出的日志格式就不再是 JSON 格式: