.NET 7实现每个服务实例独立日志文件的最优方案咨询
问题
我正尝试让我的.NET 7应用根据服务实例将日志写入不同文件,希望使用.NET默认日志组件,也愿意接受第三方库建议。我已编写了自定义FileLoggerProvider:
[ProviderAlias("FileLogger")] public class FileLoggerProvider : ILoggerProvider { private readonly FileLoggerOptions _options; public FileLoggerProvider(IOptions<FileLoggerOptions> options) { _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); if(!Directory.Exists(_options.FolderPath)) { Directory.CreateDirectory(_options.FolderPath); } } public ILogger CreateLogger(string categoryName) { return new FileLogger(Path.Combine(_options.FolderPath, _options.FileName)); } public void Dispose() { GC.SuppressFinalize(this); } }
并通过EventId区分写入的日志文件:
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if (!IsEnabled(logLevel)) { return; } var formattedFilePath = _filePath .Replace("{eventName}", eventId.Name ?? "Program") .Replace("{date}", DateTime.Now.ToShortDateString()); var logRecord = string.Format("{0} [{1}] {2} {3}", "[" + DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss+00:00") + "]", logLevel.ToString(), formatter(state, exception), exception != null ? exception.StackTrace : ""); using var fileStream = new FileStream(formattedFilePath, FileMode.Append, FileAccess.Write, FileShare.None); using var streamWriter = new StreamWriter(fileStream); streamWriter.WriteLine(logRecord); }
调用方式如下:
public SynchronizationWorker(ILogger<SynchronizationWorker> logger, SynchronizationItemOptions options) { _logger = logger; _loggerEventId = new EventId((int)options.Region, options.Region.ToString()); _logger.LogInformation(_loggerEventId, "Worker running at: {time}", DateTimeOffset.Now); }
服务注册代码:
foreach (var itemOption in options.Items) { services.AddSingleton<IHostedService, SynchronizationWorker>(serviceProvider => { var logger = serviceProvider.GetRequiredService<ILogger<SynchronizationWorker>>(); return new SynchronizationWorker(logger, itemOption); }); }
我认为当前实现并非最佳方案,想咨询是否有更优的实现方式?
优化方案
一、改进自定义日志实现
1. 优化文件写入性能
当前每次日志都创建新的FileStream和StreamWriter,频繁IO操作会影响性能。建议缓存文件流,对同一文件复用StreamWriter,并改用异步写入:
public class FileLogger : ILogger, IDisposable { private readonly string _baseFilePath; private readonly ConcurrentDictionary<string, Lazy<StreamWriter>> _writers = new(); private readonly object _lock = new(); public FileLogger(string baseFilePath) { _baseFilePath = baseFilePath; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if (!IsEnabled(logLevel)) return; var formattedFilePath = _baseFilePath .Replace("{eventName}", eventId.Name ?? "Program") .Replace("{date}", DateTime.Now.ToShortDateString()); // 确保目录存在 var directory = Path.GetDirectoryName(formattedFilePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { lock (_lock) { if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); } } // 获取或创建复用的StreamWriter var writer = _writers.GetOrAdd(formattedFilePath, path => new Lazy<StreamWriter>(() => new StreamWriter(new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) )).Value; var logRecord = $"[{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss+00:00}] [{logLevel}] {formatter(state, exception)}{(exception != null ? $"\n{exception.StackTrace}" : "")}"; writer.WriteLine(logRecord); writer.Flush(); // 可改为批量Flush进一步提升性能 } public bool IsEnabled(LogLevel logLevel) => true; // 根据需求调整过滤逻辑 public IDisposable BeginScope<TState>(TState state) => NullScope.Instance; public void Dispose() { foreach (var writer in _writers.Values) { if (writer.IsValueCreated) writer.Value.Dispose(); } _writers.Clear(); } }
2. 避免滥用EventId,改用日志上下文/Scope
EventId设计初衷是标识日志事件类型,用来区分服务实例属于滥用。建议用.NET日志的Scope传递实例标识:
// SynchronizationWorker构造函数中创建持久Scope(单例服务可长期持有) public SynchronizationWorker(ILogger<SynchronizationWorker> logger, SynchronizationItemOptions options) { _logger = logger; _region = options.Region.ToString(); _scope = _logger.BeginScope(new Dictionary<string, object> { ["Region"] = _region }); _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); } // FileLogger中读取Scope信息生成文件路径 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if (!IsEnabled(logLevel)) return; // 从Scope中获取Region var region = "Program"; if (TryGetScopeValue("Region", out var regionValue)) { region = regionValue.ToString(); } var formattedFilePath = _baseFilePath .Replace("{region}", region) .Replace("{date}", DateTime.Now.ToShortDateString()); // 后续写入逻辑... } // 辅助方法:从Scope中提取值 private static bool TryGetScopeValue(string key, out object value) { foreach (var scope in LoggerScopes.Current) { if (scope is IEnumerable<KeyValuePair<string, object>> dict && dict.TryGetValue(key, out value)) { return true; } } value = null; return false; }
3. 按实例配置日志参数
当前FileLoggerOptions是全局配置,无法为不同实例指定不同路径。可以修改FileLoggerProvider支持按类别/实例注入不同配置:
public class FileLoggerProvider : ILoggerProvider { private readonly IOptionsMonitor<FileLoggerOptions> _optionsMonitor; private readonly ConcurrentDictionary<string, FileLogger> _loggers = new(); public FileLoggerProvider(IOptionsMonitor<FileLoggerOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => { var options = _optionsMonitor.Get(name); // 按类别获取配置 if (!Directory.Exists(options.FolderPath)) Directory.CreateDirectory(options.FolderPath); return new FileLogger(Path.Combine(options.FolderPath, options.FileName)); }); } public void Dispose() { foreach (var logger in _loggers.Values) logger.Dispose(); _loggers.Clear(); } }
注册时为每个实例的Logger类别单独配置:
foreach (var itemOption in options.Items) { var category = $"SynchronizationWorker-{itemOption.Region}"; services.Configure<FileLoggerOptions>(category, opt => { opt.FolderPath = $"logs/{itemOption.Region}"; opt.FileName = "{date}.log"; }); services.AddSingleton<IHostedService, SynchronizationWorker>(sp => { var loggerFactory = sp.GetRequiredService<ILoggerFactory>(); var logger = loggerFactory.CreateLogger(category); return new SynchronizationWorker(logger, itemOption); }); }
二、使用第三方日志库(推荐Serilog)
Serilog对按属性分文件的支持非常成熟,无需自定义Provider即可实现需求:
1. 安装依赖
Install-Package Serilog.AspNetCore Install-Package Serilog.Sinks.File
2. 配置Serilog
在Program.cs中配置:
var builder = WebApplication.CreateBuilder(args); // 配置Serilog builder.Host.UseSerilog((context, config) => { config.ReadFrom.Configuration(context.Configuration) .Enrich.FromLogContext() // 启用日志上下文 .WriteTo.File( path: "logs/{Region}/{Date:yyyy-MM-dd}.log", // 按Region和日期分文件 rollingInterval: RollingInterval.Day, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss+00:00}] [{Level}] {Message:lj}{NewLine}{Exception}", shared: true // 允许多进程/实例写入同一文件 ); });
3. 服务注册与日志调用
foreach (var itemOption in options.Items) { services.AddSingleton<IHostedService, SynchronizationWorker>(sp => { var loggerFactory = sp.GetRequiredService<ILoggerFactory>(); // 给Logger添加Region属性 var logger = loggerFactory.CreateLogger<SynchronizationWorker>() .ForContext("Region", itemOption.Region.ToString()); return new SynchronizationWorker(logger, itemOption); }); } // SynchronizationWorker中直接写日志,无需手动传递EventId public class SynchronizationWorker : BackgroundService { private readonly ILogger<SynchronizationWorker> _logger; public SynchronizationWorker(ILogger<SynchronizationWorker> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken); } } }
Serilog会自动识别Region属性,将日志写入对应目录的文件中,无需手动处理文件路径和IO操作,大幅减少自定义代码量。
内容的提问来源于stack exchange,提问作者Vojtěch Petřík
相关产品推荐
相关产品推荐

