You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 06:54:57