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