背景
先前一段时间用RichTextBox实现了Microsoft.Extension.Logger的日志显示。虽然是用RichTextBox总感觉哪里不对劲,想要添加过滤显得非常复杂。最近了解并学习了ObservableCollection这个库(有点火星救援了啊),遂想到了一个更好的实现方式。
引入ObservableCollections库
- 在包管理中引入ObservableCollections库
可观察的日志
- 首先定义一个Log实体
LogMessage
。为了后面更好实现Filiter功能,添加LogLevel,EventId等属性。 - 在应用程序运行期间,需要有个存储
LogMessage
的地方,这里定义接口ILogMessageHolder
, 使用ObservableFixedSizeRingBuffer
做容器(环形数组,可以使用指定大小的size),整个Observable Logs就是这个ObservableCollections库里的容器。
public struct LogMessage { public LogLevel LogLevel { get; set;} public EventId EventId { get; set;} public string Category { get; set;} public DateTime Time { get; set;} public string Message { get; set;} } public interface ILoggerMessageHolder { public ObservableFixedSizeRingBuffer<LogMessage> LogMessages {get;} }
实现Logger等其他东西
- 实现LogMessageProcessor, 这里依然参照ConsoleLoggerProcessor的实现。(有个疑问,直接往LogMessage容器里添加新项,性能能开销应该不大,这里还需要使用工作线程来执行Enqueue操作吗?)
internal class LogMessageProcessor { //... 省略字段和其余方法实现 public LogMessageProcessor(ILogMessageHolder logMessageHolder, LoggerQueueFullMode fullMode, int maxQueueLength) { _logMessageHolder = logMessageHolder; _messageQueue = new(); FullMode = fullMode; MaxQueueLength = maxQueueLength; _outputThread = new Thread(ProcessMessageQueue) { IsBackground = true, Name = "LogMessage queue processing thread" }; _outputThread.Start(); } //将 WriteMessage 中写入Console的部分修改为往LogMessage容器里添加LogMessage internal void WriteMessage(LogMessage message) { try { _logMessageHolder.LogMessages.AddLast(message); } catch { CompleteAdding(); } } }
- 实现Logger。其实这里的LogFormatter属性可要可不要,因为这里的Formatter只对TState做格式化,可以直接做成一个内部的格式化器。当然,这里使用LogFormatter方便后期直接在配置中直接替换实现
internal class Logger : ILogger { private readonly string _category; private readonly LogMessageProcessor _processor; private StringWriter? t_stringWriter; internal IExternalScopeProvider ScopeProvider { get; set; } public Logger(string category, LogMessageProcessor processor, LogFormatter formatter,IExternalScopeProvider scopeProvider) { _category = category; _processor = processor; Formatter = formatter; ScopeProvider = scopeProvider; } public LogFormatter Formatter { get; set; } public IDisposable? BeginScope<TState>(TState state) where TState : notnull { return ScopeProvider.Push(state) ?? NullScope.Instance; } public bool IsEnabled(LogLevel logLevel) { return true; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { t_stringWriter ??= new StringWriter(); var entry = new LogEntry<TState>(logLevel, _category, eventId, state, exception, formatter); Formatter.Write(entry, t_stringWriter); var sb = t_stringWriter.GetStringBuilder(); var computedString = sb.ToString(); sb.Clear(); _processor.WriteMessage(new LogMessage() { Time = DateTime.Now, Id = eventId, Level = logLevel, Category = _category, Message = computedString, }); } }
- 实现LoggerProvider和LoggingBuilderExtension
internal class LoggerProvider : ILoggerProvider { private readonly LogFormatter _formatter; private readonly ConcurrentDictionary<string, Logger> _loggers = []; private readonly LogMessageProcessor _processor; public LoggerProvider(ILogMessageHolder holder, LogFormatter formatter) { _formatter = formatter; _processor = new LogMessageProcessor(holder, LoggerQueueFullMode.Wait, 2500); } public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, new Logger(categoryName, _processor, _formatter)); } public void Dispose() { _processor.Dispose(); } } public static class LoggingBuilderExtension { public static ILoggingBuilder AddObservableLogs(this ILoggingBuilder builder) { builder.Services.AddSingleton<ILoggerProvider, LoggerProvider>(); builder.Services.AddSingleton<IMfgLoggerProvider, LoggerProvider>(); builder.Services.AddTransient<LogFormatter, SimpleLogFormatter>(); builder.Services.AddSingleton<ILogMessageHolder, LogMessageHolder>(); return builder; } }
- 至此,Logger的核心已经完成,接下来是在View中显示Log
简单实现LogViewer
- 创建LogWindow,使用ItemsControl显示LogMessage。
实际上,LogMessage的Formatter,是下文的LogMessageConverter。
public class LogMessageConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is not LogMessage message) { return ""; } return $"{message.Time:yyyy-MM-dd HH:mm:ss,fff} {GetLogLevelString(message.LogLevel)}: {message.Category} [{message.EventId}]\r\n{message.Message}"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } private static string GetLogLevelString(LogLevel logLevel) => logLevel switch { LogLevel.Trace => "trce", LogLevel.Debug => "dbug", LogLevel.Information => "info", LogLevel.Warning => "warn", LogLevel.Error => "fail", LogLevel.Critical => "crit", _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) }; }
<!--省略Window的命名空间和其余属性--> <ItemsControl ItemsSource="{Binding LogMessages}"> <ItemsControl.ItemTemplate> <DataTemplate> <!--如果想要显示日志等级的颜色,需要其他TextBlock和添加trigger--> <TextBlock Text="{Binding ., Converter={StaticResource LogMessageConverter}}"></TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
- 创建LogWindow的DataContext
这里就涉及到ObservableCollections库的知识了,最后绑定到View上的是LogMessages属性。如果想对LogMessages进行过滤,比如选取LogLevel.Information等,使用_viewList的AttachFiliter就好啦。
public class LogWindowViewModel { private readonly ISynchronizedView<LogMessage, LogMessage> _viewList; private readonly ILogMessageHolder _logMessageHolder; public LogWindowViewModel(ILogMessageHolder logMessageHolder) { _logMessageHolder = logMessageHolder; _viewList = logMessageHolder.LogMessages.CreateView(x => x); LogMessages = _viewList.ToNotifyCollectionChanged(); } public INotifyCollectionChangedSynchronizedViewList<LogMessage> LogMessages { get; } }
结尾
使用ObservableCollections可以非常简单的将LogMessage显示到某个UI上,并且LogMessages的生命周期是跟随应用程序,随时可以打开查看应用程序的日志。由于ObservableCollections是纯C#实现,理论上是可以在Avalonia中使用的。
这一切,似未曾拥有