自定义ILoggerProvider中日期格式本地化问题求助
自定义ILoggerProvider日期格式化问题
我实现了一个自定义ILoggerProvider,当前遇到的问题是:传入ILogger的Date、DateTime、DateTimeOffset参数,会被内部的Microsoft.Logging.Extensions.LogValuesFormatter类使用InvariantCulture转换为字符串,而非应用程序的CurrentCulture。我们位于英国,使用dd/MM/yyyy格式,而非不变区域性的MM/dd/yyyy格式,甚至yyyy-MM-dd都比美式格式更适用。
该问题出现在示例代码的Log<TState>方法中,其中TState是内部的Microsoft.Logging.Extensions.LogValuesFormatter类,无法直接解析,只能传入提供的formatter。请问是否可以替换LogValuesFormatter类?支持富参数日志的提供程序能实现该功能,我希望自定义日志也能做到。
曾参考相关方案,但均存在严重弊端:
- 使用反射(性能低且在.NET7+中失效)
- 将state转换为
IReadOnlyList<KeyValuePair<string, object>>(因类为内部类而无效) - 序列化/反序列化state(效率极低且效果有限)
- 实现自定义LogInformation扩展方法(需重写大量日志条目,且破坏ILogger的通用性)
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Text; namespace XXXXX.Base; /// <summary> /// Custom ILoggerProvider, writes logs to text files with the option to cycle the filename daily. /// Daily File change is checked and handled by a call to CustomFileLoggerProvider.CheckTimeToChangeFile() /// which for efficiency should be done via an occasional background worker or a Scoped Page Request (middleware) /// </summary> [ProviderAlias("CustomFile")] public sealed class CustomFileLoggerProvider : ILoggerProvider { private readonly ConcurrentDictionary<string, CustomFileLogger> _loggers = new(StringComparer.OrdinalIgnoreCase); private CustomFileLoggerConfiguration _currentConfig; private readonly IDisposable? _onChangeToken; private readonly object _lock = new(); private StreamWriter? _logFileWriter; private bool _stopLogging = false; /// <summary> /// If messages come in prior to initialisation via the first request (middleware) then stores values here /// </summary> private readonly List<string> _initialCache = new(32); public CustomFileLoggerProvider(IOptionsMonitor<CustomFileLoggerConfiguration> config) { _currentConfig = config.CurrentValue; _onChangeToken = config.OnChange(config => _currentConfig = config); } public void Flush() { _logFileWriter?.Flush(); } public void Write(string text) { if (_stopLogging) return; lock (_lock) { if (_logFileWriter is null) { OpenLogFile(); if (_logFileWriter is null) { if (_initialCache.Count < 100) _initialCache.Add(text); else _stopLogging = true; } } if (_logFileWriter is not null) { _logFileWriter.WriteLine(text); _logFileWriter.Flush(); } } } public ILogger CreateLogger(string categoryName) => _loggers.GetOrAdd(categoryName, name => new CustomFileLogger(categoryName, Write)); /// <summary> /// Opens a new log file. This should be called while under lock. /// </summary> private bool OpenLogFile() { var filename = _currentConfig.LogFile; if (filename is null) return false; try { _logFileWriter = new StreamWriter(filename, append: true, encoding: Encoding.UTF8); _logFileWriter.WriteLine(); _logFileWriter.WriteLine(); _logFileWriter.WriteLine($"======== {DateTime.UtcNow:dd/MM/yyyy HH:mm:ss} ========"); _logFileWriter.Flush(); _stopLogging = false; return true; } catch (IOException ex) { ... } } /// <summary> /// Closes the current log file. This should be called while under lock. /// </summary> private void CloseLogFile(string reason) { if (_logFileWriter is not null) { if (!string.IsNullOrEmpty(reason)) _logFileWriter.WriteLine(reason); _logFileWriter.WriteLine("======== END ========"); _logFileWriter.Flush(); _logFileWriter.Dispose(); _logFileWriter = null; } } public void Dispose() { lock (_lock) { CloseLogFile("Closing Log File due to Provider being disposed (e.g. application shutdown)"); _loggers.Clear(); _onChangeToken?.Dispose(); } } } /// <summary> /// Custom ILogger, writes logs to text files /// </summary> public sealed class CustomFileLogger(string categoryName, Action<string> writeFunc) : ILogger { private readonly string _categoryName = categoryName; private readonly Action<string> _writeFunc = writeFunc; public IDisposable BeginScope<TState>(TState state) where TState : notnull => default!; public bool IsEnabled(LogLevel logLevel) => // Ensure that only information level and higher logs are recorded logLevel >= LogLevel.Information; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { // Ensure that only information level and higher logs are recorded if (!IsEnabled(logLevel)) return; // Get the formatted log message var message = formatter(state, exception); //var now = DateTimeOffset.Now; //Write log messages to text file _writeFunc($"{LL(logLevel)} [{_categoryName}] {message}"); } private static string LL(LogLevel l) => l switch { LogLevel.Information => "INF", LogLevel.Trace => "TRC", LogLevel.Debug => "DBG", LogLevel.Warning => "WRN", LogLevel.Error => "ERR", LogLevel.Critical => "CRITICAL", LogLevel.None => "", _ => throw new ArgumentOutOfRangeException(nameof(l)) }; } /// <summary> /// By convention, extension methods on ILoggingBuilder are used to register the custom provider /// </summary> public static class CustomFileLoggerExtensions { public static ILoggingBuilder AddCustomFileLogger(this ILoggingBuilder builder) { builder.AddConfiguration(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, CustomFileLoggerProvider>()); LoggerProviderOptions.RegisterProviderOptions<CustomFileLoggerConfiguration, CustomFileLoggerProvider>(builder.Services); return builder; } public static ILoggingBuilder AddCustomFileLogger(this ILoggingBuilder builder, Action<CustomFileLoggerConfiguration> configure) { builder.AddCustomFileLogger(); builder.Services.Configure(configure); return builder; } }
内容的提问来源于stack exchange,提问作者Chris Walsh
相关产品推荐
相关产品推荐

