闽公网安备 35020302035485号
                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 格式: