Created
March 16, 2026 07:57
-
-
Save duncansmart/73edd7657d4938fa28280847010126c4 to your computer and use it in GitHub Desktop.
Microsoft.Extensions.Logging.ILogger implementation using Channels
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Threading.Channels; | |
| using Microsoft.Extensions.DependencyInjection.Extensions; | |
| using Microsoft.Extensions.Logging.Configuration; | |
| using Microsoft.Extensions.Options; | |
| /// <summary> | |
| /// Simple file logger. | |
| /// </summary> | |
| /// <remarks>This provider batches log entries using a channel and writes them asynchronously to the specified | |
| /// file. Log entries are flushed in batches to optimize performance, and all pending entries are written before | |
| /// disposal to prevent data loss. Thread safety is ensured for logger creation and log entry queuing. The log | |
| /// file path can be configured via options or environment variables.</remarks> | |
| [ProviderAlias("SimpleFile")] | |
| class SimpleFileLoggerProvider : ILoggerProvider, ISupportExternalScope | |
| { | |
| readonly Channel<SimpleFileLogEntry> _channel = Channel.CreateUnbounded<SimpleFileLogEntry>(new UnboundedChannelOptions { SingleReader = true }); | |
| readonly Task _writerTask; | |
| IExternalScopeProvider? _scopeProvider; | |
| public SimpleFileLoggerProvider(IOptionsMonitor<SimpleFileLoggerOptions> optionsMonitor) | |
| { | |
| var filePath = Environment.ExpandEnvironmentVariables(optionsMonitor.CurrentValue.Path ?? $@"%TEMP%\{DateTime.UtcNow:yyyy-MM-dd_HHmmss}_{Environment.ProcessId}.log"); | |
| _writerTask = Task.Run(() => WriteLogsAsync(filePath, _channel.Reader)); | |
| } | |
| public ILogger CreateLogger(string categoryName) => | |
| new SimpleFileLogger(categoryName, _channel.Writer) { ScopeProvider = _scopeProvider }; | |
| public void SetScopeProvider(IExternalScopeProvider scopeProvider) => | |
| _scopeProvider = scopeProvider; | |
| public void Dispose() | |
| { | |
| // complete the channel writer (signals EOF to WaitToReadAsync) | |
| _channel.Writer.TryComplete(); | |
| // synchronously wait for the writer task to drain, so no log entries are lost on shutdown | |
| _writerTask.GetAwaiter().GetResult(); | |
| } | |
| static async Task WriteLogsAsync(string filePath, ChannelReader<SimpleFileLogEntry> channelReader) | |
| { | |
| var directory = Path.GetDirectoryName(filePath); | |
| if (!string.IsNullOrWhiteSpace(directory)) | |
| Directory.CreateDirectory(directory); | |
| using var stream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read); | |
| using var streamWriter = new StreamWriter(stream); | |
| // Exit when the channel is completed and all entries have been read | |
| while (await channelReader.WaitToReadAsync()) | |
| { | |
| // Drain all items currently available in one batch, then flush once | |
| while (channelReader.TryRead(out var entry)) | |
| { | |
| var logLevelShort = entry.LogLevel switch | |
| { | |
| LogLevel.Trace => "trce", | |
| LogLevel.Debug => "dbug", | |
| LogLevel.Information => "info", | |
| LogLevel.Warning => "warn", | |
| LogLevel.Error => "fail", | |
| LogLevel.Critical => "crit", | |
| _ => entry.LogLevel.ToString().ToLowerInvariant()[..4] | |
| }; | |
| //await streamWriter.WriteLineAsync($"{entry.TimestampUtc:yyyy-MM-dd HH:mm:ss.fff} {logLevelShort}: [{entry.CategoryName}] {entry.Message}"); | |
| await streamWriter.WriteAsync(entry.TimestampUtc.ToString("yyyy-MM-dd HH:mm:ss.fff")); | |
| await streamWriter.WriteAsync(' '); | |
| await streamWriter.WriteAsync(logLevelShort); | |
| await streamWriter.WriteAsync(": ["); | |
| await streamWriter.WriteAsync(entry.CategoryName); | |
| await streamWriter.WriteAsync("] "); | |
| await streamWriter.WriteLineAsync(entry.Message); | |
| if (entry.Exception is not null) | |
| await streamWriter.WriteLineAsync(entry.Exception.ToString()); | |
| } | |
| await streamWriter.FlushAsync(); | |
| } | |
| await streamWriter.FlushAsync(); | |
| } | |
| } | |
| class SimpleFileLogger : ILogger | |
| { | |
| readonly string _categoryName; | |
| readonly ChannelWriter<SimpleFileLogEntry> _channelWriter; | |
| internal IExternalScopeProvider? ScopeProvider { get; set; } | |
| public SimpleFileLogger(string categoryName, ChannelWriter<SimpleFileLogEntry> channelWriter) | |
| { | |
| _categoryName = categoryName; | |
| _channelWriter = channelWriter; | |
| } | |
| public IDisposable? BeginScope<TState>(TState state) where TState : notnull => | |
| ScopeProvider?.Push(state); | |
| public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; | |
| public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) | |
| { | |
| if (!IsEnabled(logLevel)) | |
| return; | |
| var message = formatter(state, exception); | |
| _channelWriter.TryWrite(new SimpleFileLogEntry(DateTime.UtcNow, logLevel, _categoryName, message, exception)); | |
| } | |
| } | |
| readonly record struct SimpleFileLogEntry( | |
| DateTime TimestampUtc, | |
| LogLevel LogLevel, | |
| string CategoryName, | |
| string Message, | |
| Exception? Exception | |
| ); | |
| public sealed class SimpleFileLoggerOptions | |
| { | |
| public string? Path { get; set; } | |
| } | |
| static class SimpleFileLoggerExtensions | |
| { | |
| public static ILoggingBuilder AddSimpleFile(this ILoggingBuilder builder, string? filePath = null) | |
| { | |
| if (!string.IsNullOrWhiteSpace(filePath)) | |
| { | |
| builder.Services.Configure<SimpleFileLoggerOptions>(options => | |
| { | |
| options.Path = filePath; | |
| }); | |
| } | |
| builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, SimpleFileLoggerProvider>()); | |
| LoggerProviderOptions.RegisterProviderOptions<SimpleFileLoggerOptions, SimpleFileLoggerProvider>(builder.Services); | |
| return builder; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment