适用于 WinForms、WPF 和 Avalonia 的 C# & VB 日志查看器控件





5.00/5 (35投票s)
适用于 WinForms、WPF 和 Avalonia 的 .NET Core 日志查看器控件,使用 ILogger 框架,支持 Microsoft Logger、Serilog、NLog 和 Log4Net,适用于 C# & VB,运行于 Windows、Mac OS 和 Linux
- 下载 LogViewerControl_v1.30 - 7.8 MB [最新]
- 下载 LogViewerControl_v1.20a - 7.7 MB [已废弃]
- 下载 LogViewerControl_v1.20 - 7.4 MB [已废弃]
- 下载 LogViewerControl_v1.10 - 8 MB [已废弃]
- 下载 LogViewerControl_v1.00 - 7.2 MB [已废弃]
引言
我正在开发一个解决方案,需要一个应用程序内置的查看器来实时查看 **Logger** 条目,以便了解后台发生的情况。
我想要比控制台输出更漂亮的东西,并且能够添加到 **Winforms**、**WPF** 或 **Avalonia** 应用程序中,感觉像是应用程序的一部分,并且可能需要用户查看的东西——即用户友好,而不是下面的样子。
LoggerViewer
的要求如下:
- 定义为一个 **控件**,可以通过依赖注入添加或注入。
- **原生** 支持 **WinForms**、**WPF** 和 **Avalonia** 应用程序。
- 支持多个操作系统 - **Windows**、**MacOS**、**Linux**。
- 支持多个日志框架 - **Microsoft**(默认)、**Serilog** 和 **NLog**。
- 支持 **着色**(附加自定义颜色)。
- **依赖注入** (DI) 和非 DI 使用。
- **MVVM**(模型-视图-视图模型设计模式)和非 MVVM 使用。
- **历史记录** 可在任何列表控件中查看,如
ListView
/DataGrid
控件。 - 可选择的 **自动滚动** 以保持最新条目可见。
- 支持 **AppSettings.Json** 文件进行可配置日志记录。
- 捕获 **框架 API 日志记录**。
- 与其他 Logger 并行工作。
我们将探讨日志记录 - 它的工作原理,并查看使其工作的框架代码。
由于我们将涵盖 **WPF**、**WinForms** 和 **Avalonia** 项目类型,**Microsoft** 和 **Serilog** 日志记录器,以及使用/不使用依赖注入,因此本文将比较长。
如果您对它的工作原理不感兴趣,请参阅下面的“预览”部分的动画,下载代码,并在适合您用例的语言中运行适用的应用程序。
预览
在开始之前,让我们看看我们想要实现的目标。LogViewerControl
的 **WPF**、**WinForms** 和 **Avalonia** 版本看起来几乎相同,并且对于 **C#** 和 **VB** 版本都工作相同。
这是一个使用依赖注入和数据绑定在 **C#** 中、用于 **WinForms** 版本的 **默认** 着色 GIF。
这是一个使用 **VB**、极简实现、无依赖注入、三行代码的 **WPF** 版本、**自定义** 着色 GIF。
最后,这里证明了您可以使用 **VB**(是的,Visual Basic)使用 **Avalonia** 框架为 **Mac OS** 开发应用程序!虽然 VB 不支持开箱即用,因为除了一个不完整的 Github 仓库 之外,没有包含应用程序、类或控件库模板,但我将介绍如何让 **VB** 使用 **Avalonia** 框架来开发应用程序和控件项目类型。
注意:三个动画 GIF 可能需要一些时间加载...
目录
- 引言
- 必备组件
- 解决方案设置
- 日志记录是如何工作的?
- 自定义 Logger
- LogViewerControl 实现
- 使用 LogViewControl
- 生成示例日志消息
- LoggerMessageAttribute (仅限 C#)
- 摘要
- 参考文献
- 历史
必备组件
本文的代码仅适用于 .NET Core。使用了 7.03 版本并启用了 Nullable。但是,如果需要,可以修改以支持 .NET 3.1 或更高版本。
该解决方案使用 **Visual Studio 2022 v17.4.5** 构建,并使用 **Rider 2022.3.2** 完全测试。
本文使用的 Nuget 包列在本文章末尾的“Nuget 包” 参考部分。
AppSettings
辅助类用于简化从 *appsettings*.json 文件读取配置设置。有一篇文章深入介绍了它的工作原理:.NET App Settings Demystified (C# & VB | CodeProject)。
如果您不熟悉日志记录,请花点时间阅读 Logging in .NET | Microsoft Learn,它涵盖了基础知识。
由于我们正在实现自定义 Logger 和 Provider,并且您不熟悉创建自定义 Logger 和 Provider,请花点时间阅读 Implement a custom logging provider in .NET | Microsoft Learn。
我们还将涵盖依赖注入 (DI)。我提供了使用和不使用 DI 的解决方案,因此 DI 不是必需的。如果您有兴趣了解更多信息,请阅读:Dependency injection in .NET | Microsoft Learn。
最后,我们将涵盖 MVVM(模型-视图-视图模型设计模式)。我提供了使用和不使用 MVVM 的解决方案,因此 MVVM 不是必需的。如果您有兴趣了解更多信息,请阅读:Model-View-ViewModel (MVVM) | Microsoft Learn。
解决方案设置
由于我们涵盖了 3 种项目类型,因此解决方案的结构试图最大限度地减少代码重复。此外,项目分为 4 个部分:应用程序、控件、核心和后台服务。
- 该应用程序演示了如何在您自己的应用程序中实现。
- 控件是您添加到自己的应用程序中的 UI 组件。
- 核心包含公共代码、特定于应用程序类型的代码和自定义 Logger 实现。自定义 Logger 实现独立于控件,您可以选择其中一个或自己编写另一个 Logger 框架。
- 后台服务只是一个虚拟服务,用于模拟生成日志消息。该服务对于所有应用程序类型都是通用的。
日志记录流程
我们可以使用下面的图表来简化设计概念。
根据上面的图表,逻辑流程如下:
- 应用程序以适当的信息记录一个事件(
Trace
、Debug
、Information
、Warning
、Error
或Critical
)。 Logger
框架将Log
事件传递给所有已注册的Logger
,包括我们的自定义 Logger。Logger
将Log
事件存储在DataStore
中。LogViewer
控件接收到数据绑定通知并显示 Log 事件。
应用程序架构
应用程序架构对于所有应用程序类型都相同。
注释
- 应用程序、控件和公共部分依赖于 UI 和应用程序类型。
- Logger Providers 特定于日志记录框架。
- 控件和公共部分特定于应用程序类型。
- Logger Providers、Random Logging Service 和 Controls 彼此独立。
解决方案架构
同时包括 **VB** 和 **C#** 解决方案,并且布局相同。唯一的区别是 **VB** 版本项目名称的末尾有 **VB**。
注释
- 应用程序项目名称由 3 部分组成:[应用程序类型][Logger][实现]
- 应用程序类型:Avalonia、WinForms、Wpf
- Logger:Logger(默认 .NET 实现)或 Serilog
- 实现:DI = 依赖注入;NoDI = 手动 / 无依赖注入
- 对于支持项目,名称后缀标识项目类型:
- .Core 用于公共代码
- .Avalonia、.WinForms、.Wpf 用于特定于应用程序的类型
日志记录是如何工作的?
在我们深入研究解决方案之前,让我们快速了解一下 .NET 日志记录框架的工作原理。
有三个部分:
- Logger
- 注册 Loggers
- 处理日志条目
我们将使用 Microsoft Logger Framework。这将使我们不仅能够捕获应用程序的日志记录,还能捕获所有 **.NET Framework** 和第三方库的日志记录。
本文中的实现将使用一个单例 `DataStore` 进行存储,自定义 Logger 和 Logging Provider。还有一个 `Configuration` 类用于自定义选项,例如自定义着色。
这只是一个简要的总结和内部代码的查看。如果您需要更多信息,请参阅上面提供的链接以及本文末尾的“参考”部分。
Logger 内部
Loggers 由四部分组成:
- Logger - 日志实现
- LoggingProvider - 生成 Logger 实例
- Processor / Storage - Logger 输出日志到的地方
- Configuration (可选) - 生成输出的参数
每次 LoggingFactory
创建 Logger
实例时,LoggingFactory
都会遍历所有已注册的 Logger Providers
并为返回的具体 Logger
生成内部 Logger
实例。对具体 Logger
上的 Log
方法的所有调用都将遍历所有内部 Logger
实例。
为了更好地理解这一点,让我们看一下 **.NET Framework** LoggerFactory
类中创建我们使用的 Logger
实例的代码。
public ILogger CreateLogger(string categoryName)
{
if (CheckDisposed())
{
throw new ObjectDisposedException(nameof(LoggerFactory));
}
lock (_sync)
{
if (!_loggers.TryGetValue(categoryName, out Logger? logger))
{
logger = new Logger(CreateLoggers(categoryName));
(logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);
_loggers[categoryName] = logger;
}
return logger;
}
}
private LoggerInformation[] CreateLoggers(string categoryName)
{
var loggers = new LoggerInformation[_providerRegistrations.Count];
for (int i = 0; i < _providerRegistrations.Count; i++)
{
loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider,
categoryName);
}
return loggers;
}
internal readonly struct LoggerInformation
{
public LoggerInformation(ILoggerProvider provider, string category) : this()
{
ProviderType = provider.GetType();
Logger = provider.CreateLogger(category);
Category = category;
ExternalScope = provider is ISupportExternalScope;
}
public ILogger Logger { get; }
public string Category { get; }
public Type ProviderType { get; }
public bool ExternalScope { get; }
}
在这里,我们看到了所有连接起来的内容,包括 LoggerProvier
通过 CreateLoggers
方法生成内部 Loggers
。
然后,每次我们通过我们的 Logger
记录条目时,信息都会被传递给每一个内部 Logger
。
这是由 LoggerFactory
实例化的具体 **.NET Framework** 内部 Logger
。我们将特别关注 Log
方法。
internal sealed class Logger : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception? exception, Func<TState, Exception?,
string> formatter)
{
MessageLogger[]? loggers = MessageLoggers;
if (loggers == null)
{
return;
}
List<Exception>? exceptions = null;
for (int i = 0; i < loggers.Length; i++)
{
ref readonly MessageLogger loggerInfo = ref loggers[i];
if (!loggerInfo.IsEnabled(logLevel))
{
continue;
}
LoggerLog(logLevel, eventId, loggerInfo.Logger, exception,
formatter, ref exceptions, state);
}
if (exceptions != null && exceptions.Count > 0)
{
ThrowLoggingError(exceptions);
}
static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger,
Exception? exception, Func<TState,
Exception?, string> formatter,
ref List<Exception>? exceptions, in TState state)
{
try
{
logger.Log(logLevel, eventId, state, exception, formatter);
}
catch (Exception ex)
{
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
}
}
// trimmed
}
在这里,我们可以看到它将信息传递给所有已注册的内部 Loggers
。
自定义 Logger
**.NET Framework** 有一个默认的 Microsoft Logger Framework 可供使用。还有许多第三方日志框架。本文将探讨两种日志框架:
- Microsoft Logger Framework(内置)
- Serilog Logger Framework 用于结构化日志记录
LogViewerControl
使用内置日志记录框架。对于 Serilog,我们将探讨如何创建自定义 Sink(Logger)并将其挂钩到内置日志记录框架。
共享日志记录数据
在查看自定义 Logger 的实现之前,我们需要设置日志条目存储和 Logger 配置。
存储 - LogDataStore 和 LogModel 类
public interface ILogDataStore
{
ObservableCollection<LogModel> Entries { get; }
void AddEntry(LogModel logModel);
}
public class LogDataStore : ILogDataStore
{
#region Fields
private static readonly SemaphoreSlim _semaphore = new(initialCount: 1);
#endregion
#region Properties
public ObservableCollection<LogModel> Entries { get; } = new();
#endregion
#region Methods
public virtual void AddEntry(LogModel logModel)
{
// ensure only one operation at time from multiple threads
_semaphore.Wait();
Entries.Add(logModel);
_semaphore.Release();
}
#endregion
}
Public Interface ILogDataStore
ReadOnly Property Entries As ObservableCollection(Of LogModel)
Sub AddEntry(logModel As LogModel)
End Interface
Public Class LogDataStore : Implements ILogDataStore
#Region "Fields"
Private Shared ReadOnly _semaphore = New SemaphoreSlim(initialCount:=1)
#End Region
#Region "Properties"
Public ReadOnly Property Entries As ObservableCollection(Of LogModel) _
= New ObservableCollection(Of LogModel) _
Implements ILogDataStore.Entries
#End Region
#Region "Methods"
Public Overridable Sub AddEntry(logModel As LogModel) _
Implements ILogDataStore.AddEntry
' ensure only one operation at time from multiple threads
_semaphore.Wait()
Entries.Add(logModel)
_semaphore.Release()
End Sub
#End Region
End Class
用于保存每个日志条目的数据模型。
public class LogModel
{
#region Properties
public DateTime Timestamp { get; set; }
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public object? State { get; set; }
public string? Exception { get; set; }
public LogEntryColor? Color { get; set; }
#endregion
}
Public Class LogModel
#Region "Properties"
Public Property Timestamp As Date
Public Property LogLevel As LogLevel
Public Property EventId As EventId
Public Property State As Object
Public Property Exception As String
Public Property Color As LogEntryColor
#End Region
End Class
注意:LogDataStore
类被初始化为单例。为了处理添加到 LogDataStore
类中的任何条目,使用了 ObservableCollection<T>
。应用程序要处理条目,只需要监听此集合的 CollectionChanged
事件。这将在文章后面的 ??? 部分介绍。
配置 - DataStoreLoggerConfiguration 类和 LogEntryColor 类
DataStoreLoggerConfiguration
类用于可选的自定义。
public class DataStoreLoggerConfiguration
{
#region Properties
public EventId EventId { get; set; }
public Dictionary<LogLevel, LogEntryColor> Colors { get; } = new()
{
[LogLevel.Trace] = new() { Foreground = Color.DarkGray },
[LogLevel.Debug] = new() { Foreground = Color.Gray },
[LogLevel.Information] = new(),
[LogLevel.Warning] = new() { Foreground = Color.Orange},
[LogLevel.Error] = new()
{ Foreground = Color.White, Background = Color.OrangeRed },
[LogLevel.Critical] = new()
{ Foreground=Color.White, Background = Color.Red },
[LogLevel.None] = new(),
};
#endregion
}
Public Class DataStoreLoggerConfiguration
#Region "Properties"
Public Property EventId As EventId
Public Property Colors As Dictionary(Of LogLevel, LogEntryColor) = _
New Dictionary(Of LogLevel, LogEntryColor) From
{
{LogLevel.Trace, New LogEntryColor() With {.Foreground = Color.DarkGray}},
{LogLevel.Debug, New LogEntryColor() With {.Foreground = Color.Gray}},
{LogLevel.Information, New LogEntryColor()},
{LogLevel.Warning, New LogEntryColor() With {.Foreground = Color.Orange}},
{LogLevel.Error, New LogEntryColor() With _
{.Foreground = Color.White, .Background = Color.OrangeRed}},
{LogLevel.Critical, New LogEntryColor() With _
{.Foreground = Color.White, .Background = Color.Red}},
{LogLevel.None, New LogEntryColor()}
}
#End Region
End Class
用于保存每个日志级别显示颜色的数据模型。
public class LogEntryColor
{
public Color Foreground { get; set; } = Color.Black;
public Color Background { get; set; } = Color.Transparent;
}
Public Class LogEntryColor
Property Foreground As Color = Color.Black
Property Background As Color = Color.Transparent
End Class
自定义 Microsoft Logger 实现
Microsoft Loggers 在本例中由两部分组成:
Logger
-DataStoreLogger
LoggingProvider
-DataStoreLoggerProvider
,它将生成DataStoreLogger
实例。
Logger - DataStoreLogger 类
public class DataStoreLogger: ILogger
{
#region Constructor
public DataStoreLogger(
string name,
Func<DataStoreLoggerConfiguration> getCurrentConfig,
ILogDataStore dataStore)
{
(_name, _getCurrentConfig) = (name, getCurrentConfig);
_dataStore = dataStore;
}
#endregion
#region Fields
private readonly ILogDataStore _dataStore;
private readonly string _name;
private readonly Func<DataStoreLoggerConfiguration> _getCurrentConfig;
#endregion
#region methods
public IDisposable BeginScope<TState>(TState state)
where TState : notnull => default!;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception, string> formatter)
{
// check if we are logging for passed log level
if (!IsEnabled(logLevel))
return;
DataStoreLoggerConfiguration config = _getCurrentConfig();
_dataStore.AddEntry(new()
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel,
// do we override the default EventId if it exists?
EventId = eventId.Id == 0 && config.EventId != 0 ?
config.EventId : eventId,
State = state,
Exception = exception?.Message ??
(logLevel == LogLevel.Error ? state?.ToString() ?? "" : ""),
Color = config.Colors[logLevel],
});
Debug.WriteLine(
$"--- [{logLevel.ToString()[..3]}]
{_name} - {formatter(state, exception!)}");
}
#endregion
}
Public Class DataStoreLogger : Implements ILogger
#Region "Constructors"
Public Sub New(name As String, getCurrentConfig _
As Func(Of DataStoreLoggerConfiguration), dataStore As ILogDataStore)
_name = name
_getCurrentConfig = getCurrentConfig
_dataStore = dataStore
End Sub
#End Region
#Region "Fields"
Private ReadOnly _dataStore As ILogDataStore
Private ReadOnly _name As String
Private ReadOnly _getCurrentConfig As Func(Of DataStoreLoggerConfiguration)
#End Region
#Region "Methods"
Public Function BeginScope(Of TState)(state As TState) As IDisposable
Implements ILogger.BeginScope
Return Nothing
End Function
Public Function IsEnabled(logLevel As LogLevel) As Boolean
Implements ILogger.IsEnabled
Return True
End Function
Public Overridable Sub Log(Of TState)(
logLevel As LogLevel, eventId As EventId,
state As TState, exception As Exception,
formatter As Func(Of TState, Exception, String))
Implements ILogger.Log
If Not IsEnabled(logLevel) Then
Return
End If
Dim exMessage As String = String.Empty
If exception IsNot Nothing Then
If String.IsNullOrEmpty(exception.Message) Then
If logLevel = LogLevel.Error AndAlso state IsNot Nothing Then
exMessage = state.ToString()
End If
Else
exMessage = exception.Message
End If
End If
Dim internalEventId As EventId = eventId
Dim config As DataStoreLoggerConfiguration = _getCurrentConfig()
If eventId.Id = 0 AndAlso config.EventId.Id <> 0 Then
internalEventId = config.EventId
End If
_dataStore.AddEntry(New LogModel() With
{
.Timestamp = Now,
.LogLevel = logLevel,
.EventId = internalEventId,
.State = state,
.Exception = exMessage,
.Color = config.Colors(logLevel)
})
Debug.WriteLine(
$"--- [{logLevel.ToString()(0.3)}] {_name} - {formatter(state, exception)}")
End Sub
#End Region
End Class
注意:自定义 DataStoreLogger
中的 Log
方法将日志添加到我们的 LogDataStore
。
Logger Provider - DataStoreLoggerProvider 类
public class DataStoreLoggerProvider: ILoggerProvider
{
#region Constructor
public DataStoreLoggerProvider(
IOptionsMonitor<DataStoreLoggerConfiguration> config,
ILogDataStore dataStore)
{
_dataStore = dataStore;
_currentConfig = config.CurrentValue;
_onChangeToken = config.OnChange(
updatedConfig => _currentConfig = updatedConfig);
}
#endregion
#region fields
private DataStoreLoggerConfiguration _currentConfig;
private readonly IDisposable? _onChangeToken;
protected readonly ILogDataStore _dataStore;
protected readonly ConcurrentDictionary<string, DataStoreLogger> _loggers = new();
#endregion
#region Methods
public ILogger CreateLogger(string categoryName)
=> _loggers.GetOrAdd(categoryName, name
=> new DataStoreLogger(name, GetCurrentConfig, _dataStore));
protected DataStoreLoggerConfiguration GetCurrentConfig()
=> _currentConfig;
public void Dispose()
{
_loggers.Clear();
_onChangeToken?.Dispose();
}
#endregion
}
Public Class DataStoreLoggerProvider : Implements ILoggerProvider
#Region "Constructors"
Public Sub New(config As IOptionsMonitor(Of DataStoreLoggerConfiguration),
dataStore As ILogDataStore)
_dataStore = dataStore
_currentConfig = config.CurrentValue
_onChangeToken = config.OnChange(Sub(updatedConfig) _currentConfig = updatedConfig)
End Sub
#End Region
#Region "Fields"
Private _currentConfig As DataStoreLoggerConfiguration
Private ReadOnly _onChangeToken As IDisposable
Protected ReadOnly _dataStore As ILogDataStore
Protected ReadOnly _loggers As ConcurrentDictionary(Of String, DataStoreLogger) =
New ConcurrentDictionary(Of String, DataStoreLogger)()
#End Region
#Region "Methods"
Public Overridable Function CreateLogger(categoryName As String) As ILogger
Implements ILoggerProvider.CreateLogger
Return _loggers.GetOrAdd(categoryName,
Function(name)
New DataStoreLogger(name, AddressOf GetCurrentConfig, _dataStore)
End Function)
End Function
Protected Function GetCurrentConfig() As DataStoreLoggerConfiguration
Return _currentConfig
End Function
Public Sub Dispose() Implements IDisposable.Dispose
_loggers.Clear()
_onChangeToken?.Dispose()
End Sub
#End Region
End Class
注意:创建 DataStoreLogger
时,会注入 DataStoreLoggerConfiguration
和 LogDataStore
。
注册 Microsoft Loggers
Microsoft Loggers 作为框架 HostApplicationBuilder
服务通过 ILoggingBuilder
注册。
这是 **.NET Framework** HostApplicationBuilder
类的精简代码。
/// <summary>
/// A builder for hosted applications and services which helps manage configuration,
/// logging, lifetime and more.
/// </summary>
public sealed class HostApplicationBuilder
{
private readonly ServiceCollection _serviceCollection = new();
// trimmed
public HostApplicationBuilder(HostApplicationBuilderSettings? settings)
{
// trimmed
Logging = new LoggingBuilder(Services);
// trimmed
}
// trimmed
/// <summary>
/// A collection of services for the application to compose. This is useful for
/// adding user provided or framework provided services.
/// </summary>
public IServiceCollection Services => _serviceCollection;
/// <summary>
/// A collection of services for the application to compose. This is useful for
/// adding user provided or framework provided services.
/// </summary>
public IServiceCollection Services => _serviceCollection;
/// <summary>
/// A collection of logging providers for the application to compose.
/// This is useful for adding new logging providers.
/// </summary>
public ILoggingBuilder Logging { get; }
// trimmed
private sealed class LoggingBuilder : ILoggingBuilder
{
public LoggingBuilder(IServiceCollection services)
{
Services = services;
}
public IServiceCollection Services { get; }
}
// trimmed
}
注册 - ServicesExtension 类
LogDataStore
、DataStoreLoggerConfiguration
和 DataStoreLoggerProvider
类的注册被抽象到 ServicesExtension
类中的一个扩展方法。
public static class ServicesExtension
{
public static ILoggingBuilder AddDefaultDataStoreLogger(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider, DataStoreLoggerProvider>());
return builder;
}
public static ILoggingBuilder AddDefaultDataStoreLogger(
this ILoggingBuilder builder,
Action<DataStoreLoggerConfiguration> configure)
{
builder.AddDefaultDataStoreLogger();
builder.Services.Configure(configure);
return builder;
}
}
Public Module ServicesExtension
<Extension>
Public Function AddDefaultDataStoreLogger(builder As ILoggingBuilder) _
As ILoggingBuilder
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton(Of ILoggerProvider, DataStoreLoggerProvider))
Return builder
End Function
<Extension>
Public Function AddDefaultDataStoreLogger( _
builder As ILoggingBuilder,
configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder
builder.AddDefaultDataStoreLogger()
builder.Services.Configure(configure)
Return builder
End Function
End Module
依赖注入
这是使用默认配置连接依赖注入的示例。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddDefaultDataStoreLogger();
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddDefaultDataStoreLogger()
_host = builder.Build()
或者,如果使用自定义配置。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddDefaultDataStoreLogger(options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddDefaultDataStoreLogger(
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
_host = builder.Build()
要创建 Logger,您可以将实例注入到类构造函数中。
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
=> _logger = logger;
#endregion
#region Fields
private readonly ILogger _logger;
#endregion
}
Public Class RandomLoggingService : Inherits BackgroundService
#Region "Constructors"
Public Sub New(logger As ILogger(Of RandomLoggingService))
_logger = logger
End Sub
#End Region
#Region "Fields"
Private _logger As ILogger
#End Region
End Class
或者手动请求实例。
ILogger<class_name> logger
= _host.Services.GetRequiredService<ILogger<class_name>>();
Dim logger As ILogger(Of class_name)
= _host.Services.GetRequiredService(Of ILogger(Of class_name))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
手动(不使用依赖注入)
如果不使用依赖注入,仍然可以注册一个或多个 Logger。我们将需要一个单例类来保存注册信息以及用于生成 Logger
实例的 Factory
方法。
这是本文示例应用程序中使用的 LoggingHelper
类。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>.Current("Logging:LogLevel", "Default")
?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddDataStoreLogger()
// examples of adding other loggers...
.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
})
// set minimum log level from 'appsettings'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String).Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
' visual debugging tools
builder.AddDataStoreLogger()
' example of adding other loggers...
builder.AddSimpleConsole(
Sub(options)
options.SingleLine = True
options.TimestampFormat = "hh:mm:ss "
End Sub)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
或者,如果使用自定义配置。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>.Current("Logging:LogLevel", "Default")
?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddDataStoreLogger(options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
})
// examples of adding other loggers...
.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "hh:mm:ss ";
})
// set minimum log level from 'appsettings'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String).Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
' visual debugging tools
builder.AddDataStoreLogger(
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
' example of adding other loggers...
builder.AddSimpleConsole(
Sub(options)
options.SingleLine = True
options.TimestampFormat = "hh:mm:ss "
End Sub)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
要创建 Logger,请使用上面 LoggingHelper
类的 Factory
方法。
Logger<class_name> logger
= new Logger<class_name>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
= New Logger(Of class_name)(LoggingHelper.Factory)
注意
创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。
将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。
RandomLoggingService service
= new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
= New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
自定义 Serilog Logger 实现
Serilog Sinks (Loggers) 的实现与 Microsoft Logger 实现不同。但是,为了与 Microsoft Logging Framework 一起工作,Serilog 实现 Logger Provider,以便 Microsoft Logging Framework 可以将数据传递给 Serilog Sinks(Logger
实现)。
Logger - DataStoreLoggerSink 类
public class DataStoreLoggerSink : ILogEventSink
{
protected readonly Func<ILogDataStore> _dataStoreProvider;
private readonly IFormatProvider? _formatProvider;
private readonly Func<DataStoreLoggerConfiguration>? _getCurrentConfig;
public DataStoreLoggerSink(Func<ILogDataStore> dataStoreProvider,
Func<DataStoreLoggerConfiguration>?
getCurrentConfig = null,
IFormatProvider? formatProvider = null)
{
_formatProvider = formatProvider;
_dataStoreProvider = dataStoreProvider;
_getCurrentConfig = getCurrentConfig;
}
public void Emit(LogEvent logEvent)
{
LogLevel logLevel = logEvent.Level switch
{
LogEventLevel.Verbose => LogLevel.Trace,
LogEventLevel.Debug => LogLevel.Debug,
LogEventLevel.Warning => LogLevel.Warning,
LogEventLevel.Error => LogLevel.Error,
LogEventLevel.Fatal => LogLevel.Critical,
_ => LogLevel.Information
};
DataStoreLoggerConfiguration config =
_getCurrentConfig?.Invoke() ?? new DataStoreLoggerConfiguration();
EventId eventId = EventIdFactory(logEvent);
if (eventId.Id == 0 && config.EventId != 0)
eventId = config.EventId;
string message = logEvent.RenderMessage(_formatProvider);
string exception =
logEvent.Exception?.Message ?? (logEvent.Level >= LogEventLevel.Error
? message
: string.Empty);
LogEntryColor color = config.Colors[logLevel];
AddLogEntry(logLevel, eventId, message, exception, color);
}
protected virtual void AddLogEntry(
LogLevel logLevel,
EventId eventId,
string message,
string exception,
LogEntryColor color)
{
ILogDataStore? dataStore = _dataStoreProvider.Invoke();
// ReSharper disable once
// ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (dataStore == null)
return; // app is shutting down
dataStore.AddEntry(new()
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel,
EventId = eventId,
State = message,
Exception = exception,
Color = color
});
}
private static EventId EventIdFactory(LogEvent logEvent)
{
EventId eventId;
if (!logEvent.Properties.TryGetValue("EventId", out LogEventPropertyValue? src))
return new();
int? id = null;
string? eventName = null;
StructureValue? value = src as StructureValue;
LogEventProperty? idProperty
= value!.Properties.FirstOrDefault(x => x.Name.Equals("Id"));
if (idProperty is not null)
id = int.Parse(idProperty.Value.ToString());
LogEventProperty? nameProperty
= value.Properties.FirstOrDefault(x => x.Name.Equals("Name"));
if (nameProperty is not null)
eventName = nameProperty.Value.ToString().Trim('"');
eventId = new EventId(id ?? 0, eventName ?? string.Empty);
return eventId;
}
}
Public Class DataStoreLoggerSink : Implements ILogEventSink
Protected ReadOnly _dataStoreProvider As Func(Of ILogDataStore)
Private ReadOnly _formatProvider As IFormatProvider
Private ReadOnly _getCurrentConfig As Func(Of DataStoreLoggerConfiguration)
Public Sub New(dataStoreProvider As Func(Of ILogDataStore),
Optional getCurrentConfig As Func(Of DataStoreLoggerConfiguration) = Nothing,
Optional formatProvider As IFormatProvider = Nothing)
_dataStoreProvider = dataStoreProvider
_formatProvider = formatProvider
_getCurrentConfig = getCurrentConfig
End Sub
Public Sub Emit(logEvent As LogEvent) Implements ILogEventSink.Emit
Dim logLevel As LogLevel
Select Case logEvent.Level
Case LogEventLevel.Verbose : logLevel = LogLevel.Trace
Case LogEventLevel.Debug : logLevel = LogLevel.Debug
Case LogEventLevel.Warning : logLevel = LogLevel.Warning
Case LogEventLevel.Error : logLevel = LogLevel.Error
Case LogEventLevel.Fatal : logLevel = LogLevel.Critical
Case Else : logLevel = LogLevel.Information
End Select
Dim config As DataStoreLoggerConfiguration = If(_getCurrentConfig Is Nothing,
New DataStoreLoggerConfiguration(),
_getCurrentConfig.Invoke())
Dim eventId As EventId = EventIdFactory(logEvent)
If eventId.Id = 0 AndAlso config.EventId <> 0 Then
eventId = config.EventId
End If
Dim message As String = logEvent.RenderMessage(_formatProvider)
Dim exception As String = If(logEvent.Exception Is Nothing,
If(logEvent.Level >= LogEventLevel.Error, message, String.Empty),
logEvent.Exception.Message)
Dim color As LogEntryColor = config.Colors(logLevel)
AddLogEntry(logLevel, eventId, message, exception, color)
End Sub
Protected Overridable Sub AddLogEntry(logLevel As LogLevel, eventId As EventId,
message As String, exception As String,
color As LogEntryColor)
Dim dataStore As ILogDataStore = _dataStoreProvider.Invoke()
If dataStore Is Nothing Then
Return
End If
dataStore.AddEntry(
New LogModel() With
{
.Timestamp = DateTime.UtcNow,
.LogLevel = logLevel,
.EventId = eventId,
.State = message,
.Exception = exception,
.Color = color
})
End Sub
Private Shared Function EventIdFactory(logEvent As LogEvent) As EventId
Dim eventId As EventId
Dim src As LogEventPropertyValue
If Not logEvent.Properties.TryGetValue("EventId", src) Then
Return New EventId()
End If
Dim id As Integer = Nothing
Dim eventName As String = Nothing
' ref: https://stackoverflow.com/a/56722516
Dim value As StructureValue = DirectCast(src, StructureValue)
Dim idProperty As LogEventProperty
= value.Properties.FirstOrDefault(Function(x) x.Name.Equals("Id"))
If idProperty IsNot Nothing Then
id = Integer.Parse(idProperty.Value.ToString())
End If
Dim nameProperty As LogEventProperty
= value.Properties.FirstOrDefault(Function(x) x.Name.Equals("Name"))
If nameProperty IsNot Nothing Then
eventName = nameProperty.Value.ToString().Trim(""""c)
End If
eventId = New EventId(
If(id = Nothing, 0, id),
If(String.IsNullOrWhiteSpace(eventName), String.Empty, eventName))
Return eventId
End Function
End Class
配置自定义 Sink - DataStoreLoggerSinkExtensions 类
与 Microsoft ILoggerProvider
实现不同,将配置传递给自定义 Sink 的方式不同。没有 Provider
,因此我们将过程封装在扩展方法中。
public static class DataStoreLoggerSinkExtensions
{
public static LoggerConfiguration DataStoreLoggerSink
(
this LoggerSinkConfiguration loggerConfiguration,
Func<ILogDataStore> dataStoreProvider,
Action<DataStoreLoggerConfiguration>? configuration = null,
IFormatProvider formatProvider = null!
)
=> loggerConfiguration.Sink(
new DataStoreLoggerSink(
dataStoreProvider,
GetConfig(configuration),
formatProvider));
private static Func<DataStoreLoggerConfiguration> GetConfig(
Action<DataStoreLoggerConfiguration>? configuration)
{
// convert from Action to Func delegate to pass data
DataStoreLoggerConfiguration data = new();
configuration?.Invoke(data);
return () => data;
}
}
Public Module DataStoreLoggerSinkExtensions
<Extension>
Public Function DataStoreLoggerSink(loggerConfiguration As LoggerSinkConfiguration,
dataStoreProvider As Func(Of ILogDataStore),
Optional configuration As Action_
(Of DataStoreLoggerConfiguration) = Nothing,
Optional formatProvider As IFormatProvider = Nothing) _
As LoggerConfiguration
Return loggerConfiguration.Sink(
New DataStoreLoggerSink(dataStoreProvider,
GetConfig(configuration),
formatProvider))
End Function
Private Function GetConfig(configuration As Action(Of DataStoreLoggerConfiguration))
As Func(Of DataStoreLoggerConfiguration)
Dim data As DataStoreLoggerConfiguration = New DataStoreLoggerConfiguration()
If configuration IsNot Nothing Then
configuration.Invoke(data)
End If
Return Function() data
End Function
End Module
注册 Sinks (Loggers)
Serilog 有两种注册 Sinks 的方法:
- 手动在代码中
- 通过
appsetting*
配置文件
由于我们需要注入 Sink 配置,因此我们将使用第一种方法来处理自定义 Sink,但 Serilog 配置和其他 Sink 将通过 appsetting*
配置文件完成。下面是本文使用的配置。
"Logging": {
"LogLevel": {
"Default": "Information",
"System.Net.Http.HttpClient": "Information"
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.File" ],
"LevelSwitches": { "controlSwitch": "Information" },
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Information"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate":
"[{Timestamp:HH:mm:ss} {Level:u3}] {EventId.Name} |
{Message:lj} {NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "c:\\WIP\\LogData\\log-.txt",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"outputTemplate": "{Timestamp:G} {Message}{NewLine:1}{Exception:1}"
}
},
{
"Name": "File",
"Args": {
"path": "c:\\WIP\\LogData\\log-.json",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"formatter": "Serilog.Formatting.Json.JsonFormatter"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ]
}
}
依赖注入
将 Serilog 与 .NET Logging Framework 结合使用的方式与 Microsoft 实现不同。我们需要在主机服务之后手动注入 LogDataStore
引用,但要在服务构建之前创建 Serilog Logger,并通过依赖注入传递配置。我们使用 Lambda 表达式(内联委托方法)来实现这一点,该方法将在每次创建 Logger 实例时调用。
这是使用默认配置连接依赖注入的示例。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
IServiceCollection services = builder.Services;
services.AddLogging(configure: cfg =>
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.WriteTo.DataStoreLoggerSink(
// Use Default Colors
dataStoreProvider: () => _host!.Services.TryGetService<ILogDataStore>()!)
.CreateLogger();
cfg.ClearProviders()
.AddSerilog(Log.Logger);
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
Dim services As IServiceCollection = builder.Services
services.AddLogging(
Sub(cfg)
' Use Default Colors
Log.Logger = New LoggerConfiguration() _
.ReadFrom.Configuration(builder.Configuration) _
.WriteTo.DataStoreLoggerSink(
Function() _host.Services.TryGetService(Of ILogDataStore)) _
.CreateLogger()
cfg.ClearProviders() _
.AddSerilog(Log.Logger)
End Sub)
_host = builder.Build()
或者,如果使用自定义配置。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
IServiceCollection services = builder.Services;
services.AddLogging(configure: cfg =>
{
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.WriteTo.DataStoreLoggerSink(
// Use Custom Colors
dataStoreProvider: () => _host!.Services.TryGetService<ILogDataStore>()!,
options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
})
.CreateLogger();
cfg.ClearProviders()
.AddSerilog(Log.Logger);
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
Dim services As IServiceCollection = builder.Services
services.AddLogging(
Sub(cfg)
' Use Custom Colors
Log.Logger = New LoggerConfiguration() _
.ReadFrom.Configuration(builder.Configuration) _
.WriteTo.DataStoreLoggerSink(
Function() _host.Services.TryGetService(Of ILogDataStore),
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub) _
.CreateLogger()
cfg.ClearProviders() _
.AddSerilog(Log.Logger)
End Sub)
_host = builder.Build()
注意
- 我们存储
Logger
工厂实例的引用,以便在应用程序关闭时,可以刷新所有 Sink 的缓冲区,例如文件或远程存储。
要创建 logger
,您可以将实例注入到类构造函数中。
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
=> _logger = logger;
#endregion
#region Fields
private readonly ILogger _logger;
#endregion
}
Public Class RandomLoggingService : Inherits BackgroundService
#Region "Constructors"
Public Sub New(logger As ILogger(Of RandomLoggingService))
_logger = logger
End Sub
#End Region
#Region "Fields"
Private _logger As ILogger
#End Region
End Class
或者手动请求实例。
ILogger<class> logger = _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name) =
_host.Services.GetRequiredService(Of ILogger(Of class_name))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
手动(不使用依赖注入)
如果不使用依赖注入,仍然可以注册一个或多个 Logger。我们将需要一个单例类来保存注册信息以及用于生成 Logger
实例的 Factory
方法。
这是本文示例应用程序中使用的 LoggingHelper
类。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.WriteTo.DataStoreLoggerSink(
// Use Default Colors
dataStoreProvider: () => MainControlsDataStore.DataStore)
.CreateLogger();
// wire up the loggers
Factory = LoggerFactory.Create(loggingBuilder
=> loggingBuilder.AddSerilog(Log.Logger));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
#region Methods
public static void CloseAndFlush()
=> Log.CloseAndFlush();
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' Use Default Colors
Log.Logger = New LoggerConfiguration() _
.ReadFrom.Configuration(configuration) _
.WriteTo.DataStoreLoggerSink(Function() MainControlsDataStore.DataStore) _
.CreateLogger()
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(LoggingBuilder)
LoggingBuilder.AddSerilog(Log.Logger)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
#Region "Methods"
Friend Sub CloseAndFlush()
Log.CloseAndFlush()
End Sub
#End Region
End Module
或者,如果使用自定义配置。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
// Use Custom Colors
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.WriteTo.DataStoreLoggerSink(
dataStoreProvider: () => MainControlsDataStore.DataStore,
options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
}
)
.CreateLogger();
// wire up the loggers
Factory = LoggerFactory.Create(loggingBuilder =>
loggingBuilder.AddSerilog(Log.Logger));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
#region Methods
public static void CloseAndFlush()
=> Log.CloseAndFlush();
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' Use Custom Colors
Log.Logger = New LoggerConfiguration() _
.ReadFrom.Configuration(configuration) _
.WriteTo.DataStoreLoggerSink(
Function() MainControlsDataStore.DataStore,
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub) _
.CreateLogger()
' wire up the loggers
Factory = LoggerFactory.Create(Sub(LoggingBuilder)
LoggingBuilder.AddSerilog(Log.Logger))
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
#Region "Methods"
Friend Sub CloseAndFlush()
Log.CloseAndFlush()
End Sub
#End Region
End Module
要创建 Logger,请使用上面 LoggingHelper
类的 Factory
方法。
Logger<class> logger = new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name) = New Logger(Of class_name)(LoggingHelper.Factory)
注意
创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。
将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。
RandomLoggingService service =
new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService =
New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
自定义 NLog Target Logger 实现 (最新)
NLog Targets (Loggers) 的实现与 Microsoft Logger 实现不同。但是,为了与 Microsoft Logging Framework 一起工作,NLog 内部实现了 Logger Provider,以便 Microsoft Logging Framework 可以将数据传递给 **NLog** Targets(Logger 实现)。
在实现自定义 NLog Target 时,必须注册该 Target,然后在配置文件中启用它。我们将实现 NLog 配置在 *appsetting*.json 文件中。
Logger - DataStoreLoggerTarget 类
[Target("DataStoreLogger")]
public class DataStoreLoggerTarget : TargetWithLayout
{
#region Fields
private ILogDataStore? _dataStore;
private DataStoreLoggerConfiguration? _config;
#endregion
#region methods
protected override void InitializeTarget()
{
// we need to inject dependencies
IServiceProvider serviceProvider = ResolveService<IServiceProvider>();
// reference the shared instance
_dataStore = serviceProvider.GetRequiredService<ILogDataStore>();
// load the config options
IOptionsMonitor<DataStoreLoggerConfiguration>? options
= serviceProvider.GetService<IOptionsMonitor<DataStoreLoggerConfiguration>>();
_config = options?.CurrentValue ?? new DataStoreLoggerConfiguration();
base.InitializeTarget();
}
protected override void Write(LogEventInfo logEvent)
{
// cast NLog Loglevel to Microsoft LogLevel type
MsLogLevel logLevel
= (MsLogLevel)Enum.ToObject(typeof(MsLogLevel), logEvent.Level.Ordinal);
// format the message
string message = RenderLogEvent(Layout, logEvent);
// retrieve the EventId
EventId eventId = (EventId)logEvent.Properties["EventId"];
// add log entry
_dataStore?.AddEntry(new()
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel,
// do we override the default EventId if it exists?
EventId = eventId.Id == 0 &&
(_config?.EventId.Id ?? 0) != 0
? _config!.EventId
: eventId,
State = message,
Exception = logEvent.Exception?.Message ??
(logLevel == MsLogLevel.Error ? message : ""),
Color = _config!.Colors[logLevel],
});
Debug.WriteLine(
$"--- [{logLevel.ToString()[..3]}]
{message} - {logEvent.Exception?.Message ?? "no error"}");
}
#endregion
}
<target("datastorelogger")>
Public Class DataStoreLoggerTarget : Inherits TargetWithLayout
#Region "Fields"
Private _dataStore As ILogDataStore
Private _config As DataStoreLoggerConfiguration
#End Region
#Region "methods"
Protected Overrides Sub InitializeTarget()
' we need to inject dependencies
Dim serviceProvider As IServiceProvider = ResolveService(Of IServiceProvider)()
' reference the shared instance
_dataStore = serviceProvider.GetRequiredService(Of ILogDataStore)
' load the config options
Dim options As IOptionsMonitor(Of DataStoreLoggerConfiguration) _
= serviceProvider.GetService(Of IOptionsMonitor(Of DataStoreLoggerConfiguration))
_config =
If(options Is Nothing, _
New DataStoreLoggerConfiguration(), _
options.CurrentValue)
MyBase.InitializeTarget()
End Sub
Protected Overrides Sub Write(logEvent As LogEventInfo)
' cast NLog Loglevel to Microsoft LogLevel type
Dim logLevel As MsLogLevel
= [Enum].ToObject(GetType(MsLogLevel), logEvent.Level.Ordinal)
' format the message
Dim message As String = RenderLogEvent(Layout, logEvent)
' retrieve the EventId
Dim eventId As EventId = logEvent.Properties("EventId")
If eventId.Id = 0 AndAlso _config.EventId.Id <> 0 Then
eventId = _config.EventId
End If
Dim exMessage As String = String.Empty
If logEvent.Exception IsNot Nothing Then
If String.IsNullOrEmpty(logEvent.Exception.Message) Then
If logLevel = MsLogLevel.Error AndAlso message IsNot Nothing Then
exMessage = message.ToString()
End If
Else
exMessage = logEvent.Exception.Message
End If
End If
' add log entry
_dataStore.AddEntry(New LogModel() With
{
.Timestamp = Date.UtcNow,
.LogLevel = logLevel,
.EventId = eventId,
.State = message,
.Exception = exMessage,
.Color = _config.Colors(logLevel)
})
Debug.WriteLine(
$"--- [{logLevel.ToString()(0.3)}] {message} - " +
$"{If(String.IsNullOrWhiteSpace(exMessage), "no error", exMessage)}")
MyBase.Write(logEvent)
End Sub
#End Region
End Class
配置自定义 Target - ServicesExtension 类
public static class ServicesExtension
{
public static ILoggingBuilder AddNLogTargets(
this ILoggingBuilder builder, IConfiguration config)
{
LogManager
.Setup()
// Register custom Target
.SetupExtensions(extensionBuilder =>
extensionBuilder.RegisterTarget<DataStoreLoggerTarget>("DataStoreLogger"));
builder
.ClearProviders()
.SetMinimumLevel(MsLogLevel.Trace)
.AddNLog(config,
new NLogProviderOptions
{
IgnoreEmptyEventId = false,
CaptureEventId = EventIdCaptureType.Legacy
});
return builder;
}
public static ILoggingBuilder AddNLogTargets(
this ILoggingBuilder builder, IConfiguration config,
Action<DataStoreLoggerConfiguration> configure)
{
builder.AddNLogTargets(config);
builder.Services.Configure(configure);
return builder;
}
}
Public Module ServicesExtension
<extension>
Public Function AddNLogTargets( _
builder As ILoggingBuilder, config As IConfiguration) As ILoggingBuilder
LogManager _
.Setup() _
.SetupExtensions(
Sub(extensionBuilder) _
extensionBuilder.RegisterTarget(Of DataStoreLoggerTarget)
("DataStoreLogger"))
builder _
.ClearProviders() _
.SetMinimumLevel(MsLogLevel.Trace) _
.AddNLog(config,
New NLogProviderOptions With
{
.IgnoreEmptyEventId = False,
.CaptureEventId = EventIdCaptureType.Legacy
})
Return builder
End Function
<extension>
Public Function AddNLogTargets( _
builder As ILoggingBuilder, config As IConfiguration, _
configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder
builder.AddNLogTargets(config)
builder.Services.Configure(configure)
Return builder
End Function
End Module
注册 Targets (Loggers)
NLog 有两种注册 Sinks 的方法:
- 手动在代码中
- 通过
appsetting*
配置文件
由于我们需要注入 Target 配置,因此我们将使用上述第二种方法(在代码中注册自定义 Target),并且如上所示。下面是本文使用的配置。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"System.Net.Http.HttpClient": "Information"
}
},
"NLog": {
"throwConfigExceptions": true,
"targets": {
"async": true,
"logconsole": {
"type": "Console",
"layout": "${longdate}|${level}|${message} |
${all-event-properties} ${exception:format=tostring}"
},
"DataStoreLogger": {
"type": "DataStoreLogger",
"layout": "${message}"
}
},
"rules": [
{
"logger": "*",
"minLevel": "Info",
"writeTo": "logconsole, DataStoreLogger"
}
]
}
}
依赖注入
ServicesExtension
类和 appsetting*
配置文件连接了 Target 的注册,包括我们的自定义 Target,并配置 NLog 与 .NET Logging Framework 一起工作。现在我们需要告诉 Host 使用 NLog Logging。
这是使用默认配置连接依赖注入的示例。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddNLogTargets(builder.Configuration);
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddNLogTargets(builder.Configuration);
_host = builder.Build()
或者,如果使用自定义配置。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddNLogTargets(builder.Configuration, options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddNLogTargets(
builder.Configuration,
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
_host = builder.Build()
要创建 Logger,您可以将实例注入到类构造函数中。
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
=> _logger = logger;
#endregion
#region Fields
private readonly ILogger _logger;
#endregion
}
Public Class RandomLoggingService : Inherits BackgroundService
#Region "Constructors"
Public Sub New(logger As ILogger(Of RandomLoggingService))
_logger = logger
End Sub
#End Region
#Region "Fields"
Private _logger As ILogger
#End Region
End Class
或者手动请求实例。
ILogger<class> logger
= _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name)
= _host.Services.GetRequiredService(Of ILogger(Of class_name))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
手动(不使用依赖注入)
如果不使用依赖注入,仍然可以注册一个或多个 Logger。
我们需要包装用于依赖注入的 ServicesExtension
来使用非 DI 版本的 LogDataStore
类。
public static class ServicesExtension
{
public static ILoggingBuilder AddNLogTargetsNoDI(
this ILoggingBuilder builder, IConfiguration config)
{
// We need to use a shared instance of the DataStore to
// pass to the LogViewerControl
builder.Services.AddSingleton(MainControlsDataStore.DataStore);
// call core NLog ServiceExtension initializer
builder.AddNLogTargets(config);
return builder;
}
public static ILoggingBuilder AddNLogTargetsNoDI(
this ILoggingBuilder builder, IConfiguration config,
Action<DataStoreLoggerConfiguration> configure)
{
builder.AddNLogTargetsNoDI(config);
builder.Services.Configure(configure);
return builder;
}
}
Public Module ServicesExtension
<extension>
Public Function AddNLogTargetsNoDI( _
builder As ILoggingBuilder, _
config As IConfiguration) As ILoggingBuilder
' We need to use a shared instance of the DataStore to
' pass to the LogViewerControl
builder.Services.AddSingleton(MainControlsDataStore.DataStore)
' call core NLog ServiceExtension initializer
builder.AddNLogTargets(config)
Return builder
End Function
<extension>
Public Function AddNLogTargetsNoDI( _
builder As ILoggingBuilder, config As IConfiguration, _
configure As Action(Of DataStoreLoggerConfiguration)) As ILoggingBuilder
builder.AddNLogTargetsNoDI(config)
builder.Services.Configure(configure)
Return builder
End Function
End Module
我们还需要一个单例类来保存注册信息以及用于生成 Logger
实例的 Factory
方法。这是本文示例应用程序中使用的 LoggingHelper
类。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>
.Current("Logging:LogLevel", "Default") ?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddNLogTargetsNoDI(configuration)
// set minimum log level from 'appsettings*.json'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String) _
.Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
builder.AddNLogTargetsNoDI(configuration)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
或者,如果使用自定义配置。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>
.Current("Logging:LogLevel", "Default") ?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddNLogTargetsNoDI(configuration, options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
})
// set minimum log level from 'appsettings*.json'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String) _
.Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
builder.AddNLogTargetsNoDI(
configuration,
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
要创建 Logger,请使用上面 LoggingHelper
类的 Factory
方法。
Logger<class> logger
= new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
= New Logger(Of class_name)(LoggingHelper.Factory)
注意
- 创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。
将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。
RandomLoggingService service
= new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
= New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
自定义 Apache Log4Net Appender Logger 实现
虽然 Log4Net 支持 .NET Framework(.NET Core 1.0 提供 .NET Standard 1.3),但 Log4Net 在实现时涉及最多,因为有:
- 对日志记录系统和自定义 Appenders 没有原生的依赖注入支持。
- 不支持使用
EventID
或其他自定义属性进行日志记录。
通过研究,我找到了一个开源项目 huorswords / Microsoft.Extensions.Logging.Log4Net.AspNetCore,它支持 .NET Framework 的依赖注入,但缺少以下要求:
- 对自定义 Appenders 没有依赖注入。
- 不支持使用
EventID
或其他自定义属性进行日志记录。
您可以在此处阅读有关此项目的更多信息:How to use Log4Net with ASP.NET Core for logging | DotNetThoughts Blog。请注意,项目名称有点误导。它不只是针对 AspNetCore。它适用于其他应用程序项目类型。
向 Microsoft.Extensions.Logging.Log4Net.AspNetCore 添加缺失的部分
虽然缺少两个关键要求,但这是一个开源项目,因此我们可以用缺失的部分更新它。以下部分将介绍我们如何通过添加向后兼容当前实现的方式来实现这一点,以避免任何中断性更改。
我将为缺失的部分创建一个 pull request。但是,目前,我已将更新后的项目包含在本文章的下载内容中。
添加 EventID 支持
没有关于如何向内部 Log4Net Logger 添加功能的官方文档。幸运的是,我在官方 Log4Net 存储库上找到了一个如何做的示例:http://svn.apache.org/logging/log4net。
添加 EventId
支持有三个部分:
- 包装基础 Log4Net
Logger
实现(Interface + Class)。 - 更新
Microsoft.Extensions.Logging.Log4Net.AspNetCore
中的Log4NetLogger
类以使用新的 Logger 类。
以下是使用的实现:
-
Logger
包装器a.
IEventIDLog
Interfacepublic interface IEventIDLog : ILog { void Log(EventId eventId, LoggingEvent loggingEvent); }
b.
EventIDLogImpl
classpublic class EventIDLogImpl : LogImpl, IEventIDLog { public EventIDLogImpl(log4net.Core.ILogger logger) : base(logger) { /* skip */ } #region Implementation of IEventIDLog public void Log(EventId eventId, LoggingEvent loggingEvent) { // is the EventId empty? if (!(eventId.Id == 0 && string.IsNullOrWhiteSpace(eventId.Name))) loggingEvent.Properties[nameof(EventId)] = eventId; Logger.Log(loggingEvent); } #endregion }
-
更新
Log4NetLogger
类我将仅显示所做的更改——我们更改了实现,现在可以注入缺失的
EventId
引用。public class Log4NetLogger : ILogger { private readonly IEventIDLog eventIdLogger; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!this.IsEnabled(logLevel)) { return; } EnsureValidFormatter(formatter); var candidate = new MessageCandidate<TState>( logLevel, eventId, state, exception, formatter); LoggingEvent loggingEvent = options.LoggingEventFactory.CreateLoggingEvent( in candidate, eventIdLogger.Logger, options, externalScopeProvider); if (loggingEvent == null) return; this.eventIdLogger.Log(eventId, loggingEvent); } }
为 Appender 支持添加依赖注入支持
这有 2 个部分:
- 包装基础
AppenderSkeleton
类并支持 DI。 - 更新
Log4NetProvider
类以准备支持 DI 的AppenderSkeleton
类。
以下是使用的实现:
-
支持 DI 的
ServiceAppenderSkeleton
包装器类。我们定义了一个
internal
显式方法来设置 DI 服务提供程序引用,以及一个protected
方法,该方法可用于从我们的自定义 Appender 中解析任何必需的依赖项。internal interface IAppenderServiceProvider { IServiceProvider ServiceProvider { set; } } public abstract class ServiceAppenderSkeleton : AppenderSkeleton, IServiceAppenderSkeleton, IDisposable { private IServiceProvider _serviceProvider; IServiceProvider IAppenderServiceProvider.ServiceProvider { set => _serviceProvider = value; } protected T ResolveService<T>() where T : class { if (_serviceProvider == null) return default; return _serviceProvider.GetService<T>(); } public void Dispose() => _serviceProvider = null; }
-
更新
Log4NetProvider
类我将仅显示所做的更改,以向实现
IAppenderServiceProvider
接口的 Appender 添加 DI 服务提供程序引用。public class Log4NetProvider : ILoggerProvider, ISupportExternalScope { #region IOC implementation public Log4NetProvider(IServiceProvider serviceCollection) : this(new Log4NetProviderOptions(), serviceCollection) { } public Log4NetProvider(string log4NetConfigFileName, IServiceProvider serviceProvider) : this(new Log4NetProviderOptions(log4NetConfigFileName), serviceProvider) { } public Log4NetProvider(Log4NetProviderOptions options, IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; this.SetOptionsIfValid(options); Assembly loggingAssembly = GetLoggingReferenceAssembly(); this.CreateLoggerRepository(loggingAssembly) .ConfigureLog4NetLibrary(loggingAssembly); } private IServiceProvider serviceProvider; #endregion private Log4NetProvider ConfigureLog4NetLibrary(Assembly assembly) { if (this.options.UseWebOrAppConfig) { XmlConfigurator.Configure(this.Repository); return this; } if (!this.options.ExternalConfigurationSetup) { string fileNamePath = CreateLog4NetFilePath(assembly); if (this.options.Watch) { XmlConfigurator.ConfigureAndWatch( this.Repository, new FileInfo(fileNamePath)); } else { var configXml = ParseLog4NetConfigFile(fileNamePath); if (this.options.PropertyOverrides != null && this.options.PropertyOverrides.Any()) { configXml = UpdateNodesWithOverridingValues( configXml, this.options.PropertyOverrides); } XmlConfigurator.Configure(this.Repository, configXml.DocumentElement); } } this.InjectServices(); return this; } private void InjectServices() { if (this.Repository is null) return; IEnumerable<IAppenderServiceProvider> adapters = this.Repository .GetAppenders() .OfType<IAppenderServiceProvider>(); foreach (IAppenderServiceProvider adapter in adapters) adapter.ServiceProvider = serviceProvider; } }
Logger - DataStoreLoggerAppender 类
public class DataStoreLoggerAppender : AppenderServiceProvider
{
#region Fields
private ILogDataStore? _dataStore;
private DataStoreLoggerConfiguration? _options;
private IServiceProvider? _serviceProvider;
#endregion
#region Methods
protected override void Append(LoggingEvent loggingEvent)
{
if (_serviceProvider is null)
Initialize();
// cast matching Log4Net Loglevel to Microsoft LogLevel type
LogLevel logLevel = loggingEvent.Level.Value switch
{
int.MaxValue => LogLevel.None,
120000 => LogLevel.Debug,
90000 => LogLevel.Critical,
70000 => LogLevel.Error,
60000 => LogLevel.Warning,
20000 => LogLevel.Trace,
_ => LogLevel.Information
};
DataStoreLoggerConfiguration config = _options ??
new DataStoreLoggerConfiguration();
EventId? eventId = (EventId?)loggingEvent.LookupProperty(nameof(EventId));
eventId = eventId is null && config.EventId.Id != 0 ?
config.EventId : eventId;
string message = loggingEvent.RenderedMessage ?? string.Empty;
string exceptionMessage = loggingEvent.GetExceptionString();
_dataStore!.AddEntry(new()
{
Timestamp = DateTime.UtcNow,
LogLevel = logLevel,
EventId = eventId ?? new(),
State = message,
Exception = exceptionMessage,
Color = config.Colors[logLevel],
});
Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message}
- {exceptionMessage ?? "no error"}");
}
private void Initialize()
{
_serviceProvider = ResolveService<IServiceProvider>();
_dataStore = _serviceProvider.GetRequiredService<ILogDataStore>();
_options = _serviceProvider.GetService<DataStoreLoggerConfiguration>();
}
#endregion
}
Public Class DataStoreLoggerAppender : Inherits ServiceAppenderSkeleton
#Region "Fields"
Private _dataStore As ILogDataStore
Private _options As DataStoreLoggerConfiguration
Private _serviceProvider As IServiceProvider
#End Region
#Region "Methods"
Protected Overrides Sub Append(loggingEvent As LoggingEvent)
If _serviceProvider Is Nothing Then
Initialize()
End If
' cast matching Log4Net Loglevel to Microsoft LogLevel type
Dim logLevel As LogLevel
Select Case loggingEvent.Level.Value
Case Integer.MaxValue : logLevel = LogLevel.None
Case 120000 : logLevel = LogLevel.Debug
Case 90000 : logLevel = LogLevel.Critical
Case 70000 : logLevel = LogLevel.Error
Case 60000 : logLevel = LogLevel.Warning
Case 20000 : logLevel = LogLevel.Trace
Case Else : logLevel = LogLevel.Information
End Select
Dim config As DataStoreLoggerConfiguration =
If(_options Is Nothing, New DataStoreLoggerConfiguration, _options)
Dim eventId As EventId = loggingEvent.LookupProperty(NameOf(eventId))
eventId = If(eventId = Nothing AndAlso config.EventId.Id <> 0, _
config.EventId, eventId)
Dim message As String = loggingEvent.RenderedMessage
Dim exceptionMessage = loggingEvent.GetExceptionString()
_dataStore.AddEntry(
New LogModel() With
{
.Timestamp = Date.UtcNow,
.LogLevel = logLevel,
.State = message,
.Exception = exceptionMessage,
.Color = config.Colors(logLevel)
})
Debug.WriteLine($"--- [{logLevel.ToString()(0.3)}] {message}" +
" - {If(String.IsNullOrWhiteSpace(exceptionMessage), _
"no error", exceptionMessage)}")
End Sub
Private Sub Initialize()
_serviceProvider = ResolveService(Of IServiceProvider)()
_dataStore = _serviceProvider.GetRequiredService(Of ILogDataStore)
_options = _serviceProvider.GetService(Of DataStoreLoggerConfiguration)
End Sub
#End Region
End Class
配置自定义 Appender - ServicesExtension 类
public static class ServicesExtension
{
public static ILoggingBuilder AddLog4Net_
(this ILoggingBuilder builder, IConfiguration config)
=> builder
.ClearProviders()
.AddLog4Net(config.GetLog4NetConfiguration());
public static ILoggingBuilder AddLog4Net(this ILoggingBuilder builder,
IConfiguration config, Action<DataStoreLoggerConfiguration> configure)
{
builder
.AddLog4Net(config)
.Services.Configure(configure);
return builder;
}
public static Log4NetProviderOptions? GetLog4NetConfiguration(
this IConfiguration configuration)
=> configuration
.GetSection("Log4NetCore")
.Get<Log4NetProviderOptions>();
}
Public Module ServicesExtension
<extension>
Public Function AddLog4Net(builder As ILoggingBuilder,
config As IConfiguration) As ILoggingBuilder
builder _
.ClearProviders() _
.AddLog4Net(config.GetLog4NetConfiguration())
Return builder
End Function
<extension>
Public Function AddLog4Net(builder As ILoggingBuilder,
config As IConfiguration, _
configure As Action(Of DataStoreLoggerConfiguration))
As ILoggingBuilder
builder.AddLog4Net(config)
builder.Services.Configure(configure)
Return builder
End Function
<extension>
Private Function GetLog4NetConfiguration(configuration As IConfiguration)
As Log4NetProviderOptions
Return configuration _
.GetSection("Log4NetCore") _
.Get(Of Log4NetProviderOptions)
End Function
End Module
注册 Appenders (Loggers)
Log4Net 仅限于使用 XML 配置文件。默认名称是 log4net.config
。可以更改此文件的名称。但是,为了本文的目的,我们将不重点介绍这一点。
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="DebugAppender" type="log4net.Appender.DebugAppender" >
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date
[%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<threshold value="ALL" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date
[%thread] %-5level %logger - %message%newline" />
</layout>
</appender>
<appender name="DataStoreLogger"
type="Log4Net.Appender.LogView.Core.DataStoreLoggerAppender">
<threshold value="ALL" />
</appender>
<root>
<Level value="ALL" />
<appender-ref ref="DebugAppender" />
<appender-ref ref="ConsoleAppender" />
<appender-ref ref="DataStoreLogger" />
</root>
</log4net>
幸运的是,Microsoft.Extensions.Logging.Log4Net.AspNetCore
项目支持覆盖 *log4net.config* 文件中的值。这使我们能够使用 *appsettings*.json 文件为每个启动配置文件支持不同的配置。
这是我们的 *appsettings.Production.json* 文件。
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"System.Net.Http.HttpClient": "Trace"
}
},
"Log4NetCore": {
"Name": "Log4NetLogViewer_Prod",
"LoggerRepository": "LogViewerRepository",
"OverrideCriticalLevelWith": "Critical",
"Watch": false,
"UseWebOrAppConfig": false,
"PropertyOverrides": [
{
"XPath": "/log4net/appender[@name='ConsoleAppender']/layout/conversionPattern",
"Attributes": {
"Value": "%date [%thread] %-5level | %logger | %message%newline"
}
},
{
"XPath": "/log4net/appender[@name='ConsoleAppender']/threshold",
"Attributes": {
"Value": "Warn"
}
},
{
"XPath": "/log4net/appender[@name='DataStoreLogger']/threshold",
"Attributes": {
"Value": "Warn"
}
}
]
}
}
注释
- *log4net.config* 中的默认日志级别适用于所有级别,但是,对于 Production/Release,*appsettings.Production.json* 文件将覆盖为
Warn
,适用于Warning
、Error
和Critical
级别。
依赖注入
ServicesExtension
类和 log4net.config
配置文件连接了 Appenders 的注册,包括我们的自定义 appender,并配置 **Log4Het** 以与 **.NET Logging Framework** 一起工作。现在我们需要告诉 Host 使用 Log4Net Logging。
这是使用默认配置连接依赖注入的示例。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddLog4Net(builder.Configuration);
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddLog4Net(builder.Configuration)
_host = builder.Build()
或者,如果使用自定义配置。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.AddLogViewer();
builder.Logging.AddLog4Net(builder.Configuration, options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.AddLogViewer()
builder.Logging.AddLog4Net(
builder.Configuration,
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
_host = builder.Build()
要创建 Logger,您可以将实例注入到类构造函数中。
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
=> _logger = logger;
#endregion
#region Fields
private readonly ILogger _logger;
#endregion
}
Public Class RandomLoggingService : Inherits BackgroundService
#Region "Constructors"
Public Sub New(logger As ILogger(Of RandomLoggingService))
_logger = logger
End Sub
#End Region
#Region "Fields"
Private _logger As ILogger
#End Region
End Class
或者手动请求实例。
ILogger<class> logger
= _host.Services.GetRequiredService<ILogger<class>>();
Dim logger As ILogger(Of class_name)
= _host.Services.GetRequiredService(Of ILogger(Of class_name))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
手动(不使用依赖注入)
如果不使用依赖注入,仍然可以注册一个或多个 Logger。
我们需要包装用于依赖注入的 ServicesExtension
来使用非 DI 版本的 LogDataStore
类。
public static class ServicesExtension
{
public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder,
IConfiguration config)
{
// We need to use a shared instance of the DataStore to pass to
// the LogViewerControl
builder.Services.AddSingleton(MainControlsDataStore.DataStore);
// call core Log4Net ServiceExtension initializer
builder.AddLog4Net(config);
return builder;
}
public static ILoggingBuilder AddLog4NetNoDI(this ILoggingBuilder builder,
IConfiguration config, Action<DataStoreLoggerConfiguration> configure)
{
builder.AddLog4NetNoDI(config);
builder.Services.Configure(configure);
return builder;
}
}
Public Module ServicesExtension
<extension>
Public Function AddLog4NetNoDI(builder As ILoggingBuilder,
config As IConfiguration) As ILoggingBuilder
' We need to use a shared instance of the DataStore to pass to
' the LogViewerControl
builder.Services.AddSingleton(MainControlsDataStore.DataStore)
' call core Log4Net ServiceExtension initializer
builder.AddLog4Net(config)
Return builder
End Function
<extension>
Public Function AddLog4NetNoDI(builder As ILoggingBuilder,
config As IConfiguration, configure As Action(Of DataStoreLoggerConfiguration))
As ILoggingBuilder
builder.AddLog4NetNoDI(config)
builder.Services.Configure(configure)
Return builder
End Function
End Module
我们还需要一个单例类来保存注册信息以及用于生成 Logger
实例的 Factory
方法。这是本文示例应用程序中使用的 LoggingHelper
类。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>
.Current("Logging:LogLevel", "Default") ?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddLog4NetNoDI(configuration)
// set minimum log level from 'appsettings*.json'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String) _
.Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
builder.AddLog4NetNoDI(configuration)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
或者,如果使用自定义配置。
public static class LoggingHelper
{
#region Constructors
static LoggingHelper()
{
// retrieve the log level from 'appsettings'
string value = AppSettings<string>
.Current("Logging:LogLevel", "Default") ?? "Information";
Enum.TryParse(value, out LogLevel logLevel);
IConfigurationRoot configuration = new ConfigurationBuilder()
.Initialize()
.Build();
// wire up the loggers
Factory = LoggerFactory.Create(builder => builder
// visual debugging tools
.AddLog4NetNoDI(configuration, options =>
{
options.Colors[LogLevel.Trace] = new()
{
Foreground = Color.White,
Background = Color.DarkGray
};
options.Colors[LogLevel.Debug] = new()
{
Foreground = Color.White,
Background = Color.Gray
};
options.Colors[LogLevel.Information] = new()
{
Foreground = Color.White,
Background = Color.DodgerBlue
};
options.Colors[LogLevel.Warning] = new()
{
Foreground = Color.White,
Background = Color.Orchid
};
})
// set minimum log level from 'appsettings*.json'
.SetMinimumLevel(logLevel));
}
#endregion
#region Properties
public static ILoggerFactory Factory { get; }
#endregion
}
Public Module LoggingHelper
#Region "Constructors"
Sub New()
' retrieve the log level from 'appsettings'
Dim value As String = AppSettings(Of String) _
.Current("Logging:LogLevel", "Default")
If String.IsNullOrWhiteSpace(value) Then
value = "Information"
End If
Dim logLevel As LogLevel
If Not [Enum].TryParse(value, logLevel) Then
logLevel = LogLevel.Information
End If
Dim configuration As IConfigurationRoot = New ConfigurationBuilder() _
.Initialize() _
.Build()
' wire up the loggers
Factory = LoggerFactory.Create(
Sub(builder)
builder.AddLog4NetNoDI(
configuration,
Sub(options)
options.Colors(LogLevel.Trace) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DarkGray
}
options.Colors(LogLevel.Debug) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Gray
}
options.Colors(LogLevel.Information) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.DodgerBlue
}
options.Colors(LogLevel.Warning) = New LogEntryColor() With
{
.Foreground = Color.White,
.Background = Color.Orchid
}
End Sub)
' set minimum log level from 'appsettings'
builder.SetMinimumLevel(logLevel)
End Sub)
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property Factory As ILoggerFactory
#End Region
End Module
要创建 Logger,请使用上面 LoggingHelper
类的 Factory
方法。
Logger<class> logger
= new Logger<class>(LoggingHelper.Factory);
Dim logger As Logger(Of class_name)
= New Logger(Of class_name)(LoggingHelper.Factory)
注意
- 创建 Logger 时,类需要被实例化/创建。如果类未实例化,则会抛出错误。
将 Logger 作为构造函数参数创建是可以接受的。例如,以下是可接受的。
RandomLoggingService service
= new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
Dim service As RandomLoggingService
= New RandomLoggingService(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
这是 Logger 实例及其已实例化 Logger 内部的示例屏幕截图。
处理日志条目
我们有 LogDataStore
类,它存储来自所有库和应用程序的所有日志条目,基于从 *appsettings*.json 配置文件中检索到的最小 LogLevel
。
依赖注入
LogDataStore
类被注册为单例。它可以被注入到类中。
public class MyConsumer
{
#region Constructors
public MyConsumer(ILogDataStore dataStore)
=> _dataStore = dataStore;
#endregion
#region Properties
private ILogDataStore? _dataStore;
#endregion
}
Public Class MyConsumer
#Region "Constructors"
Public Sub New(dataStore As ILogDataStore)
_dataStore = dataStore
End Sub
#End Region
#Region "Fields"
Private _dataStore As ILogDataStore
#End Region
End Class
或者手动请求实例。
public class MyConsumer
{
#region Constructors
public MyConsumer(IServiceProvider serviceProvider)
=> _dataStore = serviceProvider.GetRequiredService<LogDataStore>();
#endregion
#region Properties
private ILogDataStore? _dataStore;
#endregion
}
Public Class MyConsumer
#Region "Constructors"
Public Sub New(serviceProvider As IServiceProvider)
_dataStore = serviceProvider.GetRequiredService(Of LogDataStore)
End Sub
#End Region
#Region "Fields"
Private _dataStore As ILogDataStore
#End Region
End Class
我们需要为依赖注入注册 MyConsumer
类来连接所有内容。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddSingleton<LogDataStore>(); // from `ServicesExtension` class above
builder.Services.AddTransient<MyConsumer>();
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.Services.AddSingleton(Of LogDataStore) ' from `ServicesExtension` class above
builder.Services.AddTransient(Of MyConsumer)
_host = builder.Build()
手动(不使用依赖注入)
Data Store 需要保存在一个单例类中,以便它可以被 Logger(生产者)和 Consumer 类共享。
这是将保存共享 Data Store 的 MainControlsDataStore
类。
public static class MainControlsDataStore
{
public static ILogDataStore DataStore { get; } = new();
}
Public Module MainControlsDataStore
Public Property DataStore As ILogDataStore = New LogDataStore()
End Module
我们可以将 Data Store 的实例传递给 Consumer 类以进行 IOC(控制反转),从而允许应用程序/库将来升级到依赖注入或不同的实现。
public class MyConsumer
{
public MyConsumer(ILogDataStore dataStore)
=> _dataStore = dataStore;
private LogDataStore? _dataStore;
}
Public Class MyConsumer
#Region "Constructors"
Public Sub New(dataStore As ILogDataStore)
_dataStore = dataStore
End Sub
#End Region
#Region "Fields"
Private _dataStore As ILogDataStore
#End Region
End Class
要使用 MyConsumer
类,我们会注入 DataStore
。
MyConsumer instance = new MyConsumer(MainControlsDataStore.DataStore);
Dim instance As MyConsumer = new MyConsumer(MainControlsDataStore.DataStore)
监听新条目
当我们实例化 MyConsumer
类并引用 LogDataStore
类时,我们需要手动监听 Entries
属性的 CollectionChanged
事件,或者让数据绑定完成所有工作。
手动处理 CollectionChanged 事件
public class MyConsumer
{
public MyConsumer(LogDataStore dataStore)
{
_dataStore = dataStore;
_dataStore.Entries.CollectionChanged += OnCollectionChanged;
}
private ILogDataStore? _dataStore;
private void OnCollectionChanged
(object? sender, NotifyCollectionChangedEventArgs e)
{
// any new items?
if (e.NewItems?.Count > 0)
{
// process new items
}
// any to remove? ... not required for this purpose.
if (e.OldItems?.Count > 0)
{
// remove items
}
}
}
Public Class MyConsumer
#Region "Constructors"
Public Sub New(ByVal dataStore As LogDataStore)
_dataStore = dataStore
_dataStore.Entries.CollectionChanged += AddressOf OnCollectionChanged
End Sub
#End Region
#Region "Fields"
Private _dataStore As ILogDataStore
#End Region
#Region "Methods"
Private Sub OnCollectionChanged(sender As Object,
e As NotifyCollectionChangedEventArgs)
If e.NewItems?.Count > 0 Then
' process new items
End If
If e.OldItems?.Count > 0 Then
' remove items
End If
End Sub
#End Region
End Class
LogViewerControl 实现
Logger
代码分为两部分:
- 公共代码 -
LogViewer.Core
项目 = 共享代码 - 特定于应用程序类型的控件实现
- WinForms 特有 -
LogViewer.WinForms
项目 = **WinForm** 包装代码(针对 Common Code) - Wpf 特有 -
LogViewer.Wpf
项目 = **Wpf** 包装代码(针对 Common Code) - Avalonia 特有 -
LogViewer.Avalonia
项目 = **Avalonia** 包装代码(针对 Common Code)
- WinForms 特有 -
这样做的原因是我们需要将数据传回 UI 线程。所有应用程序类型的执行此操作的方法略有不同。包含了一个 DispatcherHelper
类,用于 **Wpf** 和 **WinForms**。**Avalonia** 不需要相同的,它们有一个易于使用的实现。下面,您可以看到实现中的差异。
DispatcherHelper 类
Logger
框架利用独立于 UI 线程的线程来保持性能。消费日志条目并在 UI 上显示它们需要将数据传送到 UI 线程。这方面的抽象将由 DispatcherHelper
类处理。DispatcherHelper
类的 Execute
方法接受一个委托,并识别它是否在 UI 线程上,如果需要,它会在调用委托之前切换。
对 **Wpf** 和 **WinForms** 的使用非常简单。
DispatcherHelper.Execute(() => delegate_method());
'DispatcherHelper
Execute(Sub() delegate_method())
或者您可以内联 delegate_method()
。
DispatcherHelper.Execute(() =>
{
// do work here
});
'DispatcherHelper
Execute(Sub()
' do work here
End Sub)
在 **Avalonia** 中的使用非常相似。
await Dispatcher.UIThread.InvokeAsync(() => delegate_method());
Await Dispatcher.UIThread.InvokeAsync(Sub() delegate_method())
或者您可以内联 delegate_method()
。
await Dispatcher.UIThread.InvokeAsync(() =>
{
// do work here
});
Await Dispatcher.UIThread.InvokeAsync(
Sub()
' do work here
End Sub)
WinForms 实现
public static class DispatcherHelper
{
public static void Execute(Action action)
{
// no cross-thread concerns
if (Application.OpenForms.Count == 0)
{
action.Invoke();
return;
}
try
{
if (Application.OpenForms[0]!.InvokeRequired)
// Marshall to Main Thread
Application.OpenForms[0]!.Invoke(action);
else
// We are already on the Main Thread
action.Invoke();
}
catch (Exception)
{
// ignore as might be thrown on shutting down
}
}
}
Public Module DispatcherHelper
Public Sub Execute(action As Action)
' no cross-tread concerns
If Application.OpenForms.Count = 0 Then
action.Invoke()
Return
End If
Try
If Application.OpenForms(0).InvokeRequired Then
' Marshall to Main Thread
Application.OpenForms(0).Invoke(action)
Else
' no cross-tread concerns
action.Invoke()
End If
Catch ex As Exception
' ignore as might be thrown on shutting down
End Try
End Sub
End Module
WPF 实现
public static class DispatcherHelper
{
public static void Execute(Action action)
{
if (Application.Current is null || Application.Current.Dispatcher is null)
// We are already on the Main Thread
return;
// Marshall to Main Thread
Application.Current.Dispatcher.BeginInvoke
( DispatcherPriority.Background, action);
}
}
Public Module DispatcherHelper
Public Sub Execute(action As Action)
If Application.Current Is Nothing OrElse _
Application.Current.Dispatcher Is Nothing Then
Return
End If
' Marshall to Main Thread
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, action)
End Sub
End Module
公共代码 - LogViewer.Core 项目
以上各节已涵盖 DataStoreLogger
、DataStoreLoggerProvider
、DataStoreLoggerConfiguration
、LogDataStore
、LogModel
和 LogEntryColor
类。对于 **WPF**,我们将在“WPF LogViwerControl 实现”部分介绍 LogViewerControlViewModel
类和 ILogDataStoreImpl
接口。
LoggerExtensions 类
包含两个方法:
Emit
方法 -Log
方法的高性能包装器。TestPattern
方法 - 一个辅助方法,用于查看所有LogLevel
类型的输出格式(仅用于调试目的)。
public static class LoggerExtensions
{
public static void Emit(this ILogger logger, EventId eventId,
LogLevel logLevel, string message, Exception? exception = null,
params object?[] args)
{
if (logger is null)
return;
//if (!logger.IsEnabled(logLevel))
// return;
switch (logLevel)
{
case LogLevel.Trace:
logger.LogTrace(eventId, message, args);
break;
case LogLevel.Debug:
logger.LogDebug(eventId, message, args);
break;
case LogLevel.Information:
logger.LogInformation(eventId, message, args);
break;
case LogLevel.Warning:
logger.LogWarning(eventId, exception, message, args);
break;
case LogLevel.Error:
logger.LogError(eventId, exception, message, args);
break;
case LogLevel.Critical:
logger.LogCritical(eventId, exception, message, args);
break;
}
}
public static void TestPattern(this ILogger logger, EventId eventId)
{
Exception exception = new Exception("Test Error Message");
logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern");
logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern");
logger.Emit(eventId, LogLevel.Information, "Information Test Pattern");
logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern");
logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception);
logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception);
}
}
Public Module LoggerExtensions
' NOTE Optional And ParamArray are not allowed in same method call,
' so used overload instead
<Extension>
Sub Emit(logger As ILogger, eventId As EventId,
logLevel As LogLevel, message As String, ParamArray args As Object())
logger.Emit(eventId, logLevel, message, Nothing, args)
End Sub
<Extension>
Sub Emit(logger As ILogger, eventId As EventId,
logLevel As LogLevel, message As String, [exception] As Exception,
ParamArray args As Object())
If logger Is Nothing Then
Return
End If
If Not logger.IsEnabled(logLevel) Then
Return
End If
Select Case logLevel
Case LogLevel.Trace
logger.LogTrace(eventId, message, args)
Case LogLevel.Debug
logger.LogDebug(eventId, message, args)
Case LogLevel.Information
logger.LogInformation(eventId, message, args)
Case LogLevel.Warning
logger.LogWarning(eventId, message, args)
Case LogLevel.[Error]
logger.LogError(eventId, [exception], message, args)
Case LogLevel.Critical
logger.LogCritical(eventId, message, args)
End Select
End Sub
<Extension>
Sub TestPattern(logger As ILogger, Optional eventId As EventId = Nothing)
Dim exception As Exception = New Exception("Test Error Message")
logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern")
logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern")
logger.Emit(eventId, LogLevel.Information, "Information Test Pattern")
logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern")
logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception)
logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception)
End Sub
End Module
ViewModel: LogViewerControlViewModel 类
对于 **WinForms**、**WPF** 和 **Avalonia** 的依赖注入实现,一个通用的 LogViewerControlViewModel
类用于引用单例 LogDataStore
实例,以便手动(**WinForms**)或通过数据绑定(**WPF**)在 LogViewControl
控件中进行监视。
public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl
{
#region Constructor
public LogViewerControlViewModel(ILogDataStore dataStore)
{
DataStore = dataStore;
}
#endregion
#region Properties
public ILogDataStore DataStore { get; set; }
#endregion
}
Public Class LogViewerControlViewModel
Inherits ViewModel
Implements ILogDataStoreImpl
#Region "Constructors"
Public Sub New(store As ILogDataStore)
DataStore = store
End Sub
#End Region
#Region "Properties"
Public ReadOnly Property DataStore As ILogDataStore _
Implements ILogDataStoreImpl.DataStore
#End Region
End Class
WinForms - LogViewerControl
现在我们可以创建控件本身了。对于 **WinForms**,将查看代码后置。如果您想查看 UserControl
的设计,请下载并检查设计器代码。
代码后置
public partial class LogViewerControl : UserControl
{
#region Constructors
// supports DI and non-DI usage
public LogViewerControl()
{
InitializeComponent();
// Stop the flickering!
ListView.SetDoubleBuffered();
Disposed += OnDispose;
}
public LogViewerControl(LogViewerControlViewModel viewModel) : this()
=> RegisterLogDataStore(viewModel.DataStore);
#endregion
#region Fields
private ILogDataStore? _dataStore;
private static readonly SemaphoreSlim _semaphore = new(initialCount: 1);
#endregion
#region Methods
public void RegisterLogDataStore(ILogDataStore dataStore)
{
_dataStore = dataStore;
// As we are manually handling the DataBinding, we need to add existing
// log entries
AddListViewItems(_dataStore.Entries);
// Simple way to DataBind the ObservableCollection to the ListView is to
// listen to the CollectionChanged event
_dataStore.Entries.CollectionChanged += OnCollectionChanged;
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// any new items?
if (e.NewItems?.Count > 0)
{
AddListViewItems(e.NewItems.Cast<LogModel>());
ExclusiveDispatcher(() =>
{
// auto-scroll if required
if (CanAutoScroll.Checked)
ListView.Items[^1].EnsureVisible();
});
}
// any to remove? ... not required for this purpose.
if (e.OldItems?.Count > 0)
{
// remove from ListView.Items
}
}
private void AddListViewItems(IEnumerable<LogModel> logEntries)
{
ExclusiveDispatcher(() =>
{
foreach (LogModel item in logEntries)
{
ListViewItem lvi = new ListViewItem
{
Font = new(ListView.Font, FontStyle.Regular),
Text = item.Timestamp.ToString("G"),
ForeColor = item.Color!.Foreground,
BackColor = item.Color.Background
};
lvi.SubItems.Add(item.LogLevel.ToString());
lvi.SubItems.Add(item.EventId.ToString());
lvi.SubItems.Add(item.State?.ToString() ?? string.Empty);
lvi.SubItems.Add(item.Exception ?? string.Empty);
ListView.Items.Add(lvi);
}
});
}
private void ExclusiveDispatcher(Action action)
{
// ensure only one operation at time from multiple threads
_semaphore.Wait();
// delegate to UI thread
DispatcherHelper.Execute(action.Invoke);
_semaphore.Release();
}
// cleanup time ...
private void OnDispose(object? sender, EventArgs e)
{
Disposed -= OnDispose;
if (_dataStore is null)
return;
_dataStore.Entries.CollectionChanged -= OnCollectionChanged;
}
#endregion
}
Public Class LogViewerControl
#Region "Constructors"
' supports DI and non-DI usage
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Stop the flickering!
ListView.SetDoubleBuffered()
AddHandler Disposed, AddressOf OnDispose
End Sub
Public Sub New(viewModel As LogViewerControlViewModel)
Me.New()
RegisterLogDataStore(viewModel.DataStore)
End Sub
#End Region
#Region "Fields"
Private _dataStore As ILogDataStore
Private Shared ReadOnly _semaphore As SemaphoreSlim =
New SemaphoreSlim(initialCount:=1)
#End Region
#Region "Methods"
Public Sub RegisterLogDataStore(datastore As ILogDataStore)
_dataStore = datastore
' As we are manually handling the DataBinding, we need to add existing log entries
AddListViewItems(_dataStore.Entries)
' Simple way to DataBind the ObservableCollection to the ListView Is to listen
' to the CollectionChanged event
AddHandler _dataStore.Entries.CollectionChanged, AddressOf OnCollectionChanged
End Sub
Private Sub OnCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
' any new items?
If e.NewItems IsNot Nothing AndAlso e.NewItems.Count > 0 Then
AddListViewItems(e.NewItems.Cast(Of LogModel))
ExclusiveDispatcher(
Sub()
' auto-scroll if required
If CanAutoScroll.Checked Then
ListView.Items(ListView.Items.Count - 1).EnsureVisible()
End If
End Sub)
End If
' any to remove? ... not required for this purpose.
If e.OldItems IsNot Nothing AndAlso e.OldItems.Count > 0 Then
' remove from ListView.Items
End If
End Sub
Private Sub AddListViewItems(logEntries As IEnumerable(Of LogModel))
ExclusiveDispatcher(
Sub()
For Each item As LogModel In logEntries
Dim lvi As ListViewItem = New ListViewItem With
{
.Font = New Font(ListView.Font, FontStyle.Regular),
.Text = item.Timestamp.ToString("G"),
.ForeColor = item.Color.Foreground,
.BackColor = item.Color.Background
}
lvi.SubItems.Add(item.LogLevel.ToString())
lvi.SubItems.Add(item.EventId.ToString())
lvi.SubItems.Add(If(item.State Is Nothing, String.Empty, item.State.ToString()))
lvi.SubItems.Add(If(item.Exception Is Nothing, String.Empty,
item.Exception.ToString()))
ListView.Items.Add(lvi)
Next
End Sub)
End Sub
Private Sub ExclusiveDispatcher(action As Action)
' ensure only one operation at time from multiple threads
_semaphore.Wait()
' delegate to UI thread
'DispatcherHelper.
Execute(Sub() action.Invoke())
_semaphore.Release()
End Sub
' cleanup time ...
Private Sub OnDispose(sender As Object, e As EventArgs)
RemoveHandler Disposed, AddressOf OnDispose
If _dataStore Is Nothing Then
Return
End If
RemoveHandler _dataStore.Entries.CollectionChanged, AddressOf OnCollectionChanged
End Sub
#End Region
End Class
LogViewerControl
包含两个控件:
ListView
控件 - 日志条目的主要显示。CheckBox
控件 - 切换ListView
控件的自动滚动。
代码仅引用 LogDataStore
实例,并监听 Entries
集合的变化。当添加项时,将创建一个 ListViewItem
,对其进行格式化,然后添加到 ListView
控件中。
它还监听 LogViewerControl
被处置时的情况,并取消引用所有事件以避免内存泄漏。
这是一个 **默认** 着色实际运行的 GIF。
WPF - LogViewerControl
我们将使用数据绑定来管理新日志条目添加时的事件处理。
后台代码
public partial class LogViewerControl
{
public LogViewerControl() => InitializeComponent();
private void OnLayoutUpdated(object? sender, EventArgs e)
{
if (!CanAutoScroll.IsChecked == true)
return;
// design time
if (DataContext is null)
return;
// Okay, we can now get the item and scroll into view
LogModel? item = (DataContext as ILogDataStoreImpl)
?.DataStore.Entries.LastOrDefault();
if (item is null)
return;
ListView.ScrollIntoView(item);
}
}
Public Class LogViewerControl
Public Sub New()
' This call is required by the designer.
InitializeComponent()
End Sub
Private Sub OnLayoutUpdated(sender As Object, e As EventArgs)
If Not CanAutoScroll.IsChecked Then
Return
End If
' design time
If DataContext Is Nothing Then
Return
End If
Dim store As ILogDataStoreImpl = DirectCast(DataContext, ILogDataStoreImpl)
' Okay, we can now get the item and scroll into view
Dim item As LogModel = store.DataStore.Entries.LastOrDefault()
If item Is Nothing Then
Return
End If
ListView.ScrollIntoView(item)
End Sub
End Class
我们需要支持:
- 带 MVVM 的依赖注入。
- 无依赖注入和 MVVM。
- 无依赖注入,并在代码后置中使用手动数据绑定。
对于 MVVM,LogDataStore
将位于 Model
或 ViewModel
中。最后一个选项可能将 LogDataStore
作为 Window
或 UserControl
的属性暴露。控件需要访问 LogDataStore
以用于这两种情况。LogViewControl
需要一个指向该属性的通用接口。
public interface ILogDataStoreImpl
{
public LogDataStore DataStore { get; }
}
Public Interface ILogDataStoreImpl
ReadOnly Property DataStore As ILogDataStore
End Interface
用户界面
XAML 侧重于 ListView
控件中的数据绑定。
<ListView x:Name="ListView"
ItemsSource="{Binding DataStore.Entries}"
LayoutUpdated="OnLayoutUpdated">
<ListView.Resources>
<!-- trimmed -->
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="BorderBrush" Value="Silver"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Foreground" Value="{Binding Color.Foreground,
Converter={StaticResource ColorConverter}}" />
<Setter Property="Background" Value="{Binding Color.Background,
Converter={StaticResource ColorConverter}}" />
<!-- trimmed -->
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn Header="Time" Width="140">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Timestamp}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Level" Width="80">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding LogLevel}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Event Id" Width="120">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding EventId}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="State" Width="300">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding State}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Exception" Width="300">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Exception}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
注意:下载解决方案以查看 UI 的完整实现。
由于 DataStoreLogger
被 **WinForms** 和 **WPF** 项目类型使用,因此 System.Drawing.Color
类在 DataStoreLoggerConfiguration
类中使用。因此,对于 **WPF**,我们需要将 Color
类型类 System.Windows.Media.Color
转换为并返回 SolidColorBrush
。
public class ChangeColorTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
SysDrawColor sysDrawColor = (SysDrawColor)value;
return new SolidColorBrush(Color.FromArgb(
sysDrawColor.A,
sysDrawColor.R,
sysDrawColor.G,
sysDrawColor.B));
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
=> throw new NotImplementedException();
}
Public Class ChangeColorTypeConverter : Implements IValueConverter
Public Function Convert(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.Convert
Dim sysDrawColor As SysDrawColor = DirectCast(value, SysDrawColor)
Return New SolidColorBrush(Color.FromArgb(
sysDrawColor.A,
sysDrawColor.R,
sysDrawColor.G,
sysDrawColor.B))
End Function
Public Function ConvertBack(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException
End Function
End Class
这是一个 **自定义** 着色实际运行的 GIF。
Avalonia - LogViewerControl
我们将使用数据绑定来管理新日志条目添加时的事件处理。
对于 **Avalonia**,实现有所不同,因为控件不相同。这里,我们使用 DataGrid
,而 WPF 使用 ListView
。对于自动滚动,与 **WPF** 相比存在细微差别。下面,您可以看到随着项目的添加,我们如何处理滚动到视图,这与 **WPF** 不同。
后台代码
public partial class LogViewerControl : UserControl
{
public LogViewerControl()
=> InitializeComponent();
private ILogDataStoreImpl? vm;
private LogModel? item;
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (DataContext is null)
return;
vm = (ILogDataStoreImpl)DataContext;
vm.DataStore.Entries.CollectionChanged += OnCollectionChanged;
}
private void OnCollectionChanged
(object? sender, NotifyCollectionChangedEventArgs e)
=> item = MyDataGrid.Items.Cast<LogModel>().LastOrDefault();
private void OnLayoutUpdated(object? sender, EventArgs e)
{
if (CanAutoScroll.IsChecked != true || item is null)
return;
MyDataGrid.ScrollIntoView(item, null);
item = null;
}
private void OnDetachedFromLogicalTree(object? sender,
LogicalTreeAttachmentEventArgs e)
{
if (vm is null) return;
vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged;
}
}
Partial Public Class LogViewerControl : Inherits UserControl
Private _vm As ILogDataStoreImpl
Private _model As LogModel
Private MyDataGrid As DataGrid
Private CanAutoScroll As CheckBox
Sub New()
InitializeComponent()
End Sub
' Auto-wiring does not work for VB, so do it manually
' Wires up the controls and optionally loads XAML markup and attaches
' dev tools (if Avalonia.Diagnostics package is referenced)
Private Sub InitializeComponent(Optional loadXaml As Boolean = True)
If loadXaml Then
AvaloniaXamlLoader.Load(Me)
End If
MyDataGrid = FindNameScope().Find("MyDataGrid")
CanAutoScroll = FindNameScope().Find("CanAutoScroll")
End Sub
Private Shadows Sub OnDataContextChanged(sender As Object, e As EventArgs)
If DataContext Is Nothing Then
Return
End If
_vm = DirectCast(DataContext, ILogDataStoreImpl)
AddHandler _vm.DataStore.Entries.CollectionChanged,
AddressOf OnCollectionChanged
End Sub
Private Sub OnCollectionChanged(sender As Object,
e As NotifyCollectionChangedEventArgs)
_model = MyDataGrid.Items.Cast(Of LogModel).LastOrDefault()
End Sub
Private Sub OnLayoutUpdated(sender As Object, e As EventArgs)
If CanAutoScroll.IsChecked <> True OrElse _model Is Nothing Then
Return
End If
MyDataGrid.ScrollIntoView(_model, Nothing)
_model = Nothing
End Sub
Private Shadows Sub OnDetachedFromLogicalTree(sender As Object,
e As LogicalTreeAttachmentEventArgs)
If _vm Is Nothing Then
Return
End If
RemoveHandler _vm.DataStore.Entries.CollectionChanged,
AddressOf OnCollectionChanged
End Sub
End Class
我们需要支持:
- 带 MVVM 的依赖注入。
- 无依赖注入和 MVVM。
- 无依赖注入,并在代码后置中使用手动数据绑定。
对于 MVVM,LogDataStore
将位于 Model
或 ViewModel
中。最后一个选项可能将 LogDataStore
作为 Window 或 UserControl 的属性暴露。控件需要访问 LogDataStore
以用于这两种情况。LogViewControl
需要一个指向该属性的通用接口。
public interface ILogDataStoreImpl
{
public LogDataStore DataStore { get; }
}
Public Interface ILogDataStoreImpl
ReadOnly Property DataStore As ILogDataStore
End Interface
用户界面
XAML 侧重于 ListView
控件中的数据绑定。
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.Resources>
<converters:ChangeColorTypeConverter x:Key="ColorConverter" />
<converters:EventIdConverter x:Key="EventIdConverter"/>
<SolidColorBrush x:Key="ColorBlack">Black</SolidColorBrush>
<SolidColorBrush x:Key="ColorTransparent">Transparent</SolidColorBrush>
</Grid.Resources>
<Grid.Styles>
<Style Selector="DataGridRow">
<Setter Property="Padding" Value="0" />
<Setter Property="Foreground" Value="{Binding Color.Foreground,
Converter={StaticResource ColorConverter},
ConverterParameter={StaticResource ColorBlack}}" />
<Setter Property="Background" Value="{Binding Color.Background,
Converter={StaticResource ColorConverter},
ConverterParameter={StaticResource ColorTransparent}}" />
</Style>
<Style Selector="DataGridCell.size">
<Setter Property="FontSize" Value="11" />
<Setter Property="Padding" Value="0" />
</Style>
</Grid.Styles>
<DataGrid x:Name="MyDataGrid"
Items="{Binding DataStore.Entries}" AutoGenerateColumns="False"
CanUserSortColumns="False"
LayoutUpdated="OnLayoutUpdated">
<DataGrid.Columns>
<DataGridTextColumn CellStyleClasses="size"
Header="Time" Width="150"
Binding="{Binding Timestamp}"/>
<DataGridTextColumn CellStyleClasses="size"
Header="Level" Width="90"
Binding="{Binding LogLevel}" />
<DataGridTextColumn CellStyleClasses="size"
Header="Event Id" Width="120"
Binding="{Binding EventId,
Converter={StaticResource EventIdConverter}}" />
<DataGridTextColumn CellStyleClasses="size"
Header="State" Width="300"
Binding="{Binding State}" />
<DataGridTextColumn CellStyleClasses="size"
Header="Exception" Width="300"
Binding="{Binding Exception}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
注意:下载解决方案以查看 UI 的完整实现。
由于 DataStoreLogger
被 **WinForms** 和 **WPF** 项目类型使用,因此 System.Drawing.Color
类在 DataStoreLoggerConfiguration
类中使用。因此,对于 **WPF**,我们需要将 Color
类型类 System.Windows.Media.Color
转换为并返回 SolidColorBrush
。
public class ChangeColorTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
SysDrawColor sysDrawColor = (SysDrawColor)value;
return new SolidColorBrush(Color.FromArgb(
sysDrawColor.A,
sysDrawColor.R,
sysDrawColor.G,
sysDrawColor.B));
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
Public Class ChangeColorTypeConverter : Implements IValueConverter
Public Function Convert(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.Convert
Dim sysDrawColor As SysDrawColor = DirectCast(value, SysDrawColor)
Return New SolidColorBrush(Color.FromArgb(
sysDrawColor.A,
sysDrawColor.R,
sysDrawColor.G,
sysDrawColor.B))
End Function
Public Function ConvertBack(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.ConvertBack
Throw New NotImplementedException
End Function
End Class
与 **WPF** 不同,我们需要从 EventId
类中提取字符串值,因为 **Avalonia** 数据绑定到 DataGrid
控件不使用该类的 ToString()
方法。
public class EventIdConverter : IValueConverter
{
public object Convert(object? value, Type targetType,
object? parameter, CultureInfo culture)
{
if (value is null)
return "0";
EventId eventId = (EventId)value;
return eventId.ToString();
}
// If not implemented, an error is thrown
public object ConvertBack(object? value, Type targetType,
object? parameter, CultureInfo culture)
=> new EventId(0, value?.ToString() ?? string.Empty);
}
Public Class EventIdConverter : Implements IValueConverter
Public Function Convert(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.Convert
If value Is Nothing Then
Return "0"
End If
Dim eventId As EventId = DirectCast(value, EventId)
Return eventId.ToString()
End Function
Public Function ConvertBack(value As Object, targetType As Type,
parameter As Object, culture As CultureInfo)
As Object Implements IValueConverter.ConvertBack
Return New EventId(0, If(value Is Nothing, String.Empty, value.ToString()))
End Function
End Class
这是一个 **自定义** 着色实际运行的 GIF。
使用 LogViewControl
我们已经创建了自定义 Logger,我们有了一个通用的 Data Store 来共享所有日志条目,并创建了一个LogViewerControl,现在我们可以使用了。
WinForms - 依赖注入
注册 - ServicesExtension 类
LogViewerControl
和 LogViewerControlViewModel
的注册被抽象到 ServicesExtension
类中的一个扩展方法。
public static class ServicesExtension
{
public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder)
{
builder.Services.AddSingleton<ILogDataStore, Logging.LogDataStore>();
builder.Services.AddSingleton<LogViewerControlViewModel>();
builder.Services.AddTransient<LogViewerControl>();
return builder;
}
}
Public Module ServicesExtension
<Extension>
Public Function AddLogViewer(builder As HostApplicationBuilder) _
As HostApplicationBuilder
builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
builder.Services.AddSingleton(Of LogViewerControlViewModel)
builder.Services.AddTransient(Of LogViewerControl)
Return builder
End Function
End Module
注释
LogViewerControlViewModel
类被注册为单例,用于DataStoreLogger
所需的共享LogDataStore
实例,以便与LogViewerControl
共享日志条目。- 每次实例化
LogViewerControl
时,将在宿主LogViewerControl
控件中手动连接共享的LogViewerControlViewModel
实例。
MainForm 代码后置
MainForm
Designer 有一个名为 HostPanel
的 Panel
控件,用于托管 LogViewerControl
。下面,我们可以看到 LogViewerControl
被注入到 MainForm
中,并被添加到 HostPanel
中。
public partial class MainForm : Form
{
#region Constructors
public MainForm(MainControlsDataStore controlsDataStore)
{
InitializeComponent();
// wire up the control
HostPanel.AddControl(controlsDataStore.LogViewer);
}
#endregion
}
Public Class MainForm
#Region "Constructors"
Sub New(controlsDataStore As MainControlsDataStore)
' This call is required by the designer.
InitializeComponent()
' wire up the control
HostPanel.AddControl(controlsDataStore.LogViewer)
End Sub
#End Region
End Class
AddControl
是一个封装了执行此任务代码的扩展方法。
public static class ControlsExtension
{
public static void AddControl(this Panel panel, Control control)
{
panel.Controls.Add(control);
control.Dock = DockStyle.Fill;
control.BringToFront();
}
}
Public Module ControlsExtension
<Extension>
Public Sub AddControl(panel As Panel, control As Control)
panel.Controls.Add(control)
control.Dock = DockStyle.Fill
control.BringToFront()
End Sub
End Module
注册 - Bootstrapper 类 (C#)
我们不能在 static
类中使用依赖注入,在这种情况下,**WinForms** 应用程序中的 Program
类。所以我们添加一个 Bootstrapper
类并指向一个实例。
internal static class Program
{
#region Bootstrap
[STAThread]
static void Main() => _ = new Bootstrapper();
#endregion
}
然后,在 Bootstrapper
类中,我们可以连接依赖项。
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.AddDefaultDataStoreLogger();
builder.Services
.AddSingleton<MainControlsDataStore>()
.AddSingleton<MainForm>();
_host = builder.Build();
用法
注册后,我们可以从 Bootstrapper
类中显示 MainForm
。
// set and show
Application.Run(_host.Services.GetRequiredService<MainForm>());
注册 - ApplicationEvents 类
**VB.NET** 以不同于 **C#** 的方式连接 WinForms 应用程序。
Partial Friend Class MyApplication
Protected Overrides Function OnStartup( _
eventArgs As ApplicationServices.StartupEventArgs) As Boolean
InitializeDI()
Return MyBase.OnStartup(eventArgs)
End Function
Private Sub InitializeDI()
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.Logging.AddDefaultDataStoreLogger()
Dim services As IServiceCollection = builder.Services
services _
.AddSingleton(Of MainControlsDataStore) _
.AddSingleton(Of MainForm)
_host = builder.Build()
End Sub
End Class
WinForms - 手动(不使用依赖注入)
对于手动(无依赖注入),我们将 LogViewControl
控件直接添加到 Form 中,然后手动将 LogDataStore
实例注册到 LogViewControl
控件。
MainForm 代码后置
public partial class MainForm : Form
{
public Form1()
{
InitializeComponent();
// Initialize service and pass in the Logger
RandomLoggingService service =
new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
// Start generating log entries
_ = service.StartAsync(CancellationToken.None);
// manually wire up the logging to the view ...
// the control will show backlog entries...
LogViewerControl.RegisterLogDataStore(MainControlsDataStore.DataStore);
}
}
Public Class MyMainForm
Sub New()
' This call is required by the designer.
InitializeComponent()
' Initialize service and pass in the Logger
Dim service As RandomLoggingService =
New RandomLoggingService
(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
Dim task As Task = service.StartAsync(CancellationToken.None)
' manually wire up the logging to the view ...
' the control will show backlog entries...
LogViewerControl.RegisterLogDataStore(MainControlsDataStore.DataStore)
End Sub
End Class
WPF - 依赖注入
与 **WinForms** 实现有很大的重叠。
注册 - ServicesExtension 类
LogViewerControl
和 LogViewerControlViewModel
的注册被抽象到 ServicesExtension
类中的一个扩展方法。DataContext
的设置也在实例化时通过依赖注入完成。
public static class ServicesExtension
{
public static HostApplicationBuilder AddLogViewer(this HostApplicationBuilder builder)
{
builder.Services.AddSingleton<ILogDataStore, Logging.LogDataStore>();
builder.Services.AddSingleton<LogViewerControlViewModel>();
builder.Services.AddTransient(service => new LogViewerControl
{
DataContext = service.GetRequiredService<LogViewerControlViewModel>()
});
return builder;
}
}
Public Module ServicesExtension
<Extension>
Public Function AddLogViewer(builder As HostApplicationBuilder, _
Optional config As Action(Of DataStoreLoggerConfiguration) = Nothing) _
As HostApplicationBuilder
builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
builder.Services.AddSingleton(Of LogViewerControlViewModel)
builder.Services.AddTransient(
Function(service) New LogViewerControl() With
{
.DataContext = service.GetRequiredService(Of LogViewerControlViewModel)
})
Return builder
End Function
End Module
注释
LogViewerControlViewModel
类被注册为单例,用于DataStoreLogger
所需的共享LogDataStore
实例,以便与LogViewerControl
共享日志条目。- 每次实例化
LogViewerControl
时,DataContext
将自动设置为共享的LogViewerControlViewModel
实例。
MainWindow - LogViewerControl Host
托管 UserControl
有很多不同的方法。我使用的方法是 ContentControl
。
<Window x:Class="WpfLoggingDI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:control="clr-namespace:LogViewer.Wpf;assembly=LogViewer.Wpf"
xmlns:viewModels="clr-namespace:LogViewer.Core.ViewModels;assembly=LogViewer.Core"
Title="C# WPF MVVM | LogViewer Control Example - Dot Net 7.0"
WindowStartupLocation="CenterScreen" Height="634" Width="600">
<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:LogViewerControlViewModel}">
<control:LogViewerControl />
</DataTemplate>
</Window.Resources>
<ContentControl Grid.Row="1" Content="{Binding LogViewer}" />
</Window>
我们注册 MainWindow
以便依赖注入通过 MainViewModel
类注入 LogViewerControl
。MainViewModel
将暴露 LogViewerControlViewModel
,使用模板的数据绑定将初始化 LogViewControl
。
MainViewModel 类
public class MainViewModel : ViewModel
{
#region Constructor
public MainViewModel(LogViewerControlViewModel logViewer)
{
LogViewer = logViewer;
}
#endregion
#region Properties
public LogViewerControlViewModel LogViewer { get; }
#endregion
}
Public Class MainViewModel : Inherits Viewmodel
#Region "Constructor"
Public Sub New(logViewer As LogViewerControlViewModel)
Me.LogViewer = logViewer
End Sub
#End Region
#Region "Properties"
Public Property LogViewer As LogViewerControlViewModel
#End Region
End Class
注册 - App (C#) / Application (VB) 类
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.AddDataStoreLogger();
builder.Services.
.AddSingleton<MainViewModel>()
.AddSingleton<MainWindow>(service => new MainWindow
{
DataContext = service.GetRequiredService<MainViewModel>()
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.Logging.AddDefaultDataStoreLogger()
Dim services As IServiceCollection = builder.Services
services _
.AddSingleton(Of MainViewModel) _
.AddSingleton(Of MainWindow)(
Function(service) New MainWindow() With
{
.DataContext = service.GetRequiredService(Of MainViewModel)
})
_host = builder.Build()
用法
注册后,我们可以从 App
类中显示 MainWindow
。
MainWindow = _host.Services.GetRequiredService<MainWindow>();
MainWindow.Show();
MainWindow = _host.Services.GetRequiredService(Of MainWindow)()
MainWindow.Show()
WPF - 手动(不使用依赖注入)
对于手动(无依赖注入),我们将 LogViewControl
控件直接添加到 Window 中,将 LogDataStore
实例的引用手动存储为 Window 上的 Property,然后将 LogViewControl
控件的 DataContext
设置为 Window,并让数据绑定连接 LogViewControl
控件。
MainWindow XAML - LogViewerControl Host
<Window x:Class="WpfLoggingNoDI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:control="clr-namespace:LogViewer.Wpf;assembly=LogViewer.Wpf"
Title="C# WINFORMS MINIMAL | LogViewer Control Example - Dot Net 7.0"
WindowStartupLocation="CenterScreen" Height="634" Width="600">
<control:LogViewerControl x:Name="LogViewerControl" />
</Window>
MainWindow 代码后置
public partial class MainWindow : ILogDataStoreImpl
{
public MainWindow()
{
InitializeComponent();
// Initialize service and pass in the Logger
RandomLoggingService service =
new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
// Start generating log entries
_ = service.StartAsync(CancellationToken.None);
// manually wire up the logging to the view ...
// the control will show backlog entries...
DataStore = MainControlsDataStore.DataStore;
// we can't bind the controls' DataContext to a static object, so assign
// the DataStore to the Window and pass a reference to the Window itself
LogViewerControl.DataContext = this;
}
// Passed to the LogViewerControl via the DataContext property as ILogDataStoreImpl
public ILogDataStore DataStore { get; init; }
}
Class MainWindow : Implements ILogDataStoreImpl
Sub New()
' This call is required by the designer.
InitializeComponent()
' Initialize service and pass in the Logger
Dim service As RandomLoggingService =
New RandomLoggingService(New Logger_
(Of RandomLoggingService)(LoggingHelper.Factory))
' Start generating log entries
Dim task As Task = service.StartAsync(CancellationToken.None)
' manually wire up the logging to the view ...
' the control will show backlog entries...
DataStore = MainControlsDataStore.DataStore
' we can't bind the controls' DataContext to a static object, so assign
' the DataStore to the Window and pass a reference to the Window itself
LogViewerControl.DataContext = Me
End Sub
Public Property DataStore As ILogDataStore Implements ILogDataStoreImpl.DataStore
End Class
Avalonia - 依赖注入
与 **WPF** 实现有很大的重叠,因此如果您熟悉 **WPF**,那么这应该会让您感觉很熟悉。
注册 - ServicesExtension 类
LogViewerControlViewModel
的注册被抽象到 ServicesExtension
类中的一个扩展方法。DataContext
的设置也在实例化时通过依赖注入完成。
public static class ServicesExtension
{
public static HostApplicationBuilder AddLogViewer
(this HostApplicationBuilder builder)
{
builder.Services.AddSingleton<ILogDataStore, LogDataStore>();
builder.Services.AddSingleton<LogViewerControlViewModel>();
return builder;
}
}
Public Module ServicesExtension
<Extension>
Public Function AddLogViewer(builder As HostApplicationBuilder,
Optional config As Action(Of DataStoreLoggerConfiguration)
= Nothing) As HostApplicationBuilder
builder.Services.AddSingleton(Of ILogDataStore, Logging.LogDataStore)
builder.Services.AddSingleton(Of LogViewerControlViewModel)
Return builder
End Function
End Module
注释
LogViewerControlViewModel
类被注册为单例,用于DataStoreLogger
所需的共享LogDataStore
实例,以便与LogViewerControl
共享日志条目。- 每次实例化
LogViewerControl
时,DataContext
将手动设置为共享的LogViewerControlViewModel
实例。
MainWindow - LogViewerControl Host
托管 UserControl
有很多不同的方法。我使用的方法是 ContentControl
。
<Window x:Class="AvaloniaLoggingDI.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window"
xmlns:control="clr-namespace:LogViewer.Avalonia;assembly=LogViewer.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Title="C# AVALONIA | LogViewer Control Example - Dot Net 7.0"
Icon="/Assets/avalonia-logo.ico"
WindowStartupLocation="CenterScreen" Height="634" Width="600">
<control:LogViewerControl DataContext="{Binding LogViewer}" />
</Window>
MainViewModel 类
public class MainViewModel : ViewModelBase
{
#region Constructor
public MainViewModel(LogViewerControlViewModel logViewer)
{
LogViewer = logViewer;
}
#endregion
#region Properties
public LogViewerControlViewModel LogViewer { get; }
#endregion
}
Public Class MainViewModel : Inherits ViewModelBase
#Region "Constructor"
Public Sub New(logViewer As LogViewerControlViewModel)
Me.LogViewer = logViewer
End Sub
#End Region
#Region "Properties"
Public Property LogViewer As LogViewerControlViewModel
#End Region
End Class
注册 - App 类
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.AddDefaultDataStoreLogger();
builder.Services.
.AddSingleton<MainViewModel>()
.AddSingleton<MainViewModel>()
.AddSingleton<MainWindow>(service => new MainWindow
{
DataContext = service.GetRequiredService<MainViewModel>()
});
_host = builder.Build();
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
builder.Logging.AddDefaultDataStoreLogger()
Dim services As IServiceCollection = builder.Services
services _
.AddSingleton(Of MainViewModel) _
.AddSingleton(Of MainWindow)(
Function(service) New MainWindow() With
{
.DataContext = service.GetService(Of MainViewModel)
})
_host = builder.Build()
用法
注册后,我们可以从 App
类中显示 MainWindow
。
desktop.MainWindow = _host.Services.GetRequiredService<MainWindow>();
desktop.MainWindow = _host.Services.GetRequiredService(Of MainWindow)
Avalonia - 手动(不使用依赖注入)
对于手动(无依赖注入),我们将 LogViewControl
控件直接添加到 Window 中,将 LogDataStore
实例的引用手动存储为 Window 上的 Property,然后将 LogViewControl
控件的 DataContext
设置为 Window,并让数据绑定连接 LogViewControl
控件。
MainWindow XAML - LogViewerControl Host
<Window x:Class="AvaloniaLoggingNoDI.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window"
xmlns:control="clr-namespace:LogViewer.Avalonia;assembly=LogViewer.Avalonia"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="C# AVALONIA MINIMAL | LogViewer Control Example - Dot Net 7.0"
Icon="/Assets/avalonia-logo.ico"
WindowStartupLocation="CenterScreen" Height="634" Width="600">
<control:LogViewerControl x:Name="LogViewerControl" />
</Window>
MainWindow 代码后置
public partial class MainWindow : ILogDataStoreImpl
{
public MainWindow()
{
InitializeComponent();
// bare minimum to get the service running and wire up logging
// Initialize service and pass in the Logger
RandomLoggingService service =
new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
// Start generating log entries
_ = service.StartAsync(CancellationToken.None);
// manually wire up the logging to the view ...
// the control will show backlog entries...
DataStore = MainControlsDataStore.DataStore;
// we can't bind the controls' DataContext to a static object, so assign
// the DataStore to the Window and pass a reference to the Window itself
LogViewerControl.DataContext = this;
}
// Passed to the LogViewerControl via the DataContext property as ILogDataStoreImpl
public LogDataStore DataStore { get; init; }
}
Partial Public Class MainWindow : Inherits Window : Implements ILogDataStoreImpl
Private LogViewerControl As LogViewerControl
Sub New()
' This call is required by the designer.
InitializeComponent()
' Initialize service and pass in the Logger
Dim service As RandomLoggingService =
New RandomLoggingService_
(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
' Start generating log entries
Dim task As Task = service.StartAsync(CancellationToken.None)
' manually wire up the logging to the view ...
' the control will show backlog entries...
DataStore = MainControlsDataStore.DataStore
' we can't bind the controls' DataContext to a static object, so assign
' the DataStore to the Window and pass a reference to the Window itself
LogViewerControl.DataContext = Me
End Sub
' Auto-wiring does not work for VB, so do it manually
' Wires up the controls and optionally loads XAML markup and attaches
' dev tools (if Avalonia.Diagnostics package is referenced)
Private Sub InitializeComponent(Optional loadXaml As Boolean = True)
If loadXaml Then
AvaloniaXamlLoader.Load(Me)
End If
LogViewerControl = FindNameScope().Find("LogViewerControl")
End Sub
Public Property DataStore As ILogDataStore Implements ILogDataStoreImpl.DataStore
End Class
生成示例日志消息
最后要做的是生成 Log
消息以模拟实时应用程序。为此,我将使用一个 BackgroundService。BackgroundService
服务类用于为 **ASP.NET** 后台任务或 Windows 服务创建长时间运行的任务。我们也可以在桌面应用程序中使用它,但是,与 **ASP.NET** 不同,它需要手动激活和关闭。
我们将利用 **.NET Framework** HostedServices
。HostedServices
可以管理我们应用程序中的一个或多个后台任务。
后台服务 - RandomLoggingService 类
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
=> _logger = logger;
#endregion
#region Fields
#region Injected
private readonly ILogger _logger;
#endregion
// ChatGPT generated lists
private readonly List<string> _messages = new()
{
"Bringing your virtual world to life!",
// trimmed
};
readonly List<string> _eventNames = new()
{
"OnButtonClicked",
// trimmed
};
readonly List<string> _errorMessages = new()
{
"Error: Could not connect to the server. Please check your internet connection.",
// trimmed
};
private readonly Random _random = new();
private static readonly EventId EventId =
new(id: 0x1A4, name: "RandomLoggingService");
#endregion
#region BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.Emit(EventId, LogLevel.Information, "Started");
while (!stoppingToken.IsCancellationRequested)
{
// wait for a pre-determined interval
await Task.Delay(1000, stoppingToken).ConfigureAwait(false);
if (stoppingToken.IsCancellationRequested)
return;
// heartbeat logging
GenerateLogEntry();
}
_logger.Emit(EventId, LogLevel.Information, "Stopped");
}
public Task StartAsync()
=> StartAsync(CancellationToken.None);
public override async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Yield();
_logger.Emit(EventId, LogLevel.Information, "Starting");
await base.StartAsync(cancellationToken).ConfigureAwait(false);
}
public Task StopAsync()
=> StopAsync(CancellationToken.None);
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.Emit(EventId, LogLevel.Information, "Stopping");
await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
#endregion
#region Methods
private void GenerateLogEntry()
{
LogLevel level = _random.Next(0, 100) switch
{
< 50 => LogLevel.Information,
< 65 => LogLevel.Debug,
< 75 => LogLevel.Trace,
< 85 => LogLevel.Warning,
< 95 => LogLevel.Error,
_ => LogLevel.Critical
};
if (level < LogLevel.Error)
{
_logger.Emit(GenerateEventId(), level, GetMessage());
return;
}
_logger.Emit(GenerateEventId(), level, GetMessage(),
new Exception(_errorMessages[_random.Next(0, _errorMessages.Count)]));
}
private EventId GenerateEventId()
{
int index = _random.Next(0, _eventNames.Count);
return new EventId(id: 0x1A4 + index, name: _eventNames[index]);
}
private string GetMessage()
=> _messages[_random.Next(0, _messages.Count)];
#endregion
}
Public Class RandomLoggingService : Inherits BackgroundService
#Region "Constructors"
Public Sub New(logger As ILogger(Of RandomLoggingService))
_logger = logger
End Sub
#End Region
#Region "Fields"
#Region "Injects"
Private _logger As ILogger
#End Region
' ChatGPT generated lists
Private ReadOnly _messages As List(Of String) = New List(Of String) From {
"Bringing your virtual world to life!",
' trimmed
}
Private ReadOnly _eventNames As List(Of String) = New List(Of String)() From {
"OnButtonClicked",
' trimmed
}
Private ReadOnly _errorMessages As List(Of String) = New List(Of String)() From {
"Error: Could not connect to the server. Please check your internet connection.",
' trimmed
}
Private ReadOnly _random As Random = New Random()
Private Shared ReadOnly EventId As EventId
= New EventId(id:=&H1A4, name:="RandomLoggingService")
#End Region
#Region "BackgroundService"
Protected Overrides Async Function ExecuteAsync(
stoppingToken As CancellationToken) As Task
_logger.Emit(EventId, LogLevel.Information, "Started")
While Not stoppingToken.IsCancellationRequested
' wait for a pre-determined interval
Await Task.Delay(1000, stoppingToken).ConfigureAwait(False)
' heartbeat logging
GenerateLogEntry()
End While
_logger.Emit(EventId, LogLevel.Information, "Stopped")
End Function
Public Overrides Async Function StartAsync(
cancellationToken As CancellationToken) As Task
Await Task.Yield()
_logger.Emit(EventId, LogLevel.Information, "Starting")
Await MyBase.StartAsync(cancellationToken).ConfigureAwait(False)
End Function
Public Overrides Async Function StopAsync(
cancellationToken As CancellationToken) As Task
_logger.Emit(EventId, LogLevel.Information, "Stopping")
Await MyBase.StopAsync(cancellationToken).ConfigureAwait(False)
End Function
#End Region
#Region "Methods"
Private Sub GenerateLogEntry()
Dim level As LogLevel
Select Case _random.Next(0, 100)
Case < 50 : level = LogLevel.Information
Case < 65 : level = LogLevel.Debug
Case < 75 : level = LogLevel.Trace
Case < 85 : level = LogLevel.Warning
Case < 95 : level = LogLevel.Error
Case Else : level = LogLevel.Critical
End Select
If level < LogLevel.Error Then
_logger.Emit(GenerateEventId(), level, GetMessage())
Return
End If
_logger.Emit(GenerateEventId(), level, GetMessage(),
New Exception(_errorMessages(_random.Next(0, _errorMessages.Count))))
End Sub
Private Function GenerateEventId() As EventId
Dim index As Integer = _random.[Next](0, _eventNames.Count)
Return New EventId(id:=&H1A4 + index, name:=_eventNames(index))
End Function
Private Function GetMessage() As String
Return _messages(_random.[Next](0, _messages.Count))
End Function
#End Region
End Class
依赖注入
使用后台服务是一个两步过程:
- 我们需要设置类的范围并注册服务。
- 手动启动管理所有注册的后台服务的托管服务。
注册
public static class ServicesExtension
{
public static HostApplicationBuilder AddRandomBackgroundService(
this HostApplicationBuilder builder)
{
builder.Services.AddSingleton<RandomLoggingService>();
builder.Services.AddHostedService(service
=> service.GetRequiredService<RandomLoggingService>());
return builder;
}
}
Public Module ServicesExtension
<Extension>
Public Function AddRandomBackgroundService(builder As HostApplicationBuilder)
As HostApplicationBuilder
builder.Services.AddSingleton(Of RandomLoggingService)
builder.Services.AddHostedService(
Function(service) service.GetRequiredService(Of RandomLoggingService))
Return builder
End Function
End Module
用法
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
// Register the Random Logging Service
builder.AddRandomBackgroundService();
_host = builder.Build();
// startup one or more registered background services
_ = _host.StartAsync(_cancellationTokenSource.Token);
Dim builder As HostApplicationBuilder = Host.CreateApplicationBuilder()
' Random Logging Service
builder.AddRandomBackgroundService()
_host = builder.Build()
' startup background services
Dim task As Task = _host.StartAsync(_cancellationTokenSource.Token)
手动(不使用依赖注入)
// Initialize service and pass in the Logger
RandomLoggingService service =
new(new Logger<RandomLoggingService>(LoggingHelper.Factory));
// Start generating log entries
_ = service.StartAsync(_cancellationTokenSource.Token);
' Initialize service and pass in the Logger
Dim service As RandomLoggingService = New RandomLoggingService_
(New Logger(Of RandomLoggingService)(LoggingHelper.Factory))
' startup background services
Dim task As Task = _host.StartAsync(_cancellationTokenSource.Token)
LoggerMessageAttribute (仅限 C#)
在 .Net 6.0 中,支持通过 LoggerMessageAttribute 进行编译时源生成的、高性能的日志记录 API。
Microsoft 提供了涵盖使用情况的文档,称为 Compile-time logging source generation。列出的日志记录约束必须遵守:
- 日志记录方法必须是部分方法并返回
void
。 - 日志记录方法名称不得以下划线开头。
- 日志记录方法的参数名称不得以下划线开头。
- 日志记录方法可能未在嵌套类型中定义。
- 日志记录方法不能是泛型。
- 如果日志记录方法是
static
,则需要ILogger
实例作为参数。
其他未列出的约束包括:
- 需要 Event Id,它是
static
参数。 - 可选的 Event Name 是
static
参数。 - 异常必须包含在消息中,而不是单独的字段。
即将发布的 .NET 8.0(截至本文撰写时)为可传递的构造函数参数增加了更多灵活性,但 static
字段仍然存在。您可以在此处阅读更多内容:Expanding LoggerMessageAttribute Constructor Overloads for Enhanced Functionality。
考虑到以上约束,我们现在可以更新代码。
- 每个应用程序项目都需要一个专用的
Logging
方法,并带有LoggerMessageAttribute
装饰器。 - 每个 Event Name 都需要自己的专用
Logging
方法。
专用应用程序日志方法
public static partial class ApplicationLog
{
private const string AppName = "WpfLoggingAttrDI";
[LoggerMessage (EventId = 0, EventName = AppName, Message = "{msg}")]
public static partial void Emit(ILogger logger, LogLevel level, string msg);
public static void Emit(ILogger logger, LogLevel level,
string msg, Exception exception)
=> Emit(logger, level, $"{msg} - {exception}");
}
调用时,我们只需使用:
ApplicationLog.Emit(logger, logLevel, message);
如果存在异常,则:
ApplicationLog.Emit(logger, logLevel, message, exception);
专用 RandomServiceLog 方法
由于我们有多个 Event Name,每个 Event Name 都需要自己的专用 Logging
方法。下面,我设置了一个 Lookup
表来简化调用正确的方法并共享 Event Name。
public static partial class RandomServiceLog
{
public static Dictionary<string, Action<ILogger, LogLevel, string>> Events = new()
{
["OnButtonClicked"] = LogOnButtonClicked,
["OnMenuItemSelected"] = LogOnMenuItemSelected,
["OnWindowResized"] = LogOnWindowResized,
["OnDataLoaded"] = LogOnDataLoaded,
["OnFormSubmitted"] = LogOnFormSubmitted,
["OnTabChanged"] = LogOnTabChanged,
["OnItemSelected"] = LogOnItemSelected,
["OnValidationFailed"] = LogOnValidationFailed,
["OnNotificationReceived"] = LogOnNotificationReceived,
["OnApplicationStarted"] = LogOnApplicationStarted,
["OnUserLoggedIn"] = LogOnUserLoggedIn,
["OnUploadStarted"] = LogOnUploadStarted,
["OnDownloadCompleted"] = LogOnDownloadCompleted,
["OnProgressUpdated"] = LogOnProgressUpdated,
["OnNetworkErrorOccurred"] = LogOnNetworkErrorOccurred,
["OnPaymentSuccessful"] = LogOnPaymentSuccessful,
["OnProfileUpdated"] = LogOnProfileUpdated,
["OnSearchCompleted"] = LogOnSearchCompleted,
["OnFilterChanged"] = LogOnFilterChanged,
["OnLanguageChanged"] = LogOnLanguageChanged
};
public static void Emit(ILogger logger, EventId eventId,
LogLevel level, string message, Exception? exception = null)
=> Events[eventId.Name!].Invoke(logger, level, exception is null ?
message : $"{message} - {exception}");
[LoggerMessage (EventId = 101, EventName = "OnButtonClicked", Message = "{msg}")]
private static partial void LogOnButtonClicked(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 102,
EventName = "OnMenuItemSelected", Message = "{msg}")]
private static partial void LogOnMenuItemSelected(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 103, EventName = "OnWindowResized", Message = "{msg}")]
private static partial void LogOnWindowResized(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 104, EventName = "OnDataLoaded", Message = "{msg}")]
private static partial void LogOnDataLoaded(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 105, EventName = "OnFormSubmitted", Message = "{msg}")]
private static partial void LogOnFormSubmitted(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 106, EventName = "OnTabChanged", Message = "{msg}")]
private static partial void LogOnTabChanged(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 107, EventName = "OnItemSelected", Message = "{msg}")]
private static partial void LogOnItemSelected(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 108, EventName = "OnValidationFailed",
Message = "{msg}")]
private static partial void LogOnValidationFailed(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 109, EventName = "OnNotificationReceived",
Message = "{msg}")]
private static partial void LogOnNotificationReceived(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 110, EventName = "OnApplicationStarted",
Message = "{msg}")]
private static partial void LogOnApplicationStarted(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 111, EventName = "OnUserLoggedIn", Message = "{msg}")]
private static partial void LogOnUserLoggedIn(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 112, EventName = "OnUploadStarted", Message = "{msg}")]
private static partial void LogOnUploadStarted(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 113, EventName = "OnDownloadCompleted",
Message = "{msg}")]
private static partial void LogOnDownloadCompleted(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 114, EventName = "OnProgressUpdated",
Message = "{msg}")]
private static partial void LogOnProgressUpdated(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 115, EventName = "OnNetworkErrorOccurred",
Message = "{msg}")]
private static partial void LogOnNetworkErrorOccurred(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 116, EventName = "OnPaymentSuccessful",
Message = "{msg}")]
private static partial void LogOnPaymentSuccessful(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 117, EventName = "OnProfileUpdated",
Message = "{msg}")]
private static partial void LogOnProfileUpdated(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 118, EventName = "OnSearchCompleted",
Message = "{msg}")]
private static partial void LogOnSearchCompleted(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 119, EventName = "OnFilterChanged",
Message = "{msg}")]
private static partial void LogOnFilterChanged(ILogger logger,
LogLevel level, string msg);
[LoggerMessage (EventId = 120, EventName = "OnLanguageChanged",
Message = "{msg}")]
private static partial void LogOnLanguageChanged(ILogger logger,
LogLevel level, string msg);
}
注意:上面,每个唯一的 Event Name 都有一个唯一的 Event Id。这不是强制性的,但强烈建议。
调用时,我们只需使用:
RandomServiceLog.Emit("Event_Name", logger, LogLevel.Information, "message goes here");
更新 RandomLoggingService 类
现在我们可以更新 RandomLoggingService
类了。
public class RandomLoggingService : BackgroundService
{
#region Constructors
public RandomLoggingService(ILogger<RandomLoggingService> logger)
{
_logger = logger;
_eventNames = RandomServiceLog.Events.Keys.ToList();
}
#endregion
#region Fields
#region Injected
private readonly ILogger _logger;
#endregion
// ChatGPT generated lists
private readonly List<string> _messages = new()
{
"Bringing your virtual world to life!",
"Preparing a new world of adventure for you.",
"Calculating the ideal balance of work and play.",
"Generating endless possibilities for you to explore.",
"Crafting the perfect balance of life and love.",
"Assembling a world of endless exploration.",
"Bringing your imagination to life one pixel at a time.",
"Creating a world of endless creativity and inspiration.",
"Designing the ultimate dream home for you to live in.",
"Preparing for the ultimate life simulation experience.",
"Loading up your personalized world of dreams and aspirations.",
"Building a new neighborhood full of excitement and adventure.",
"Creating a world full of surprise and wonder.",
"Generating the ultimate adventure for you to embark on.",
"Assembling a community full of life and energy.",
"Crafting the perfect balance of laughter and joy.",
"Bringing your digital world to life with endless possibilities.",
"Calculating the perfect formula for happiness and success.",
"Generating a world of endless imagination and creativity.",
"Designing a world that's truly one-of-a-kind for you."
};
private readonly IReadOnlyList<string> _eventNames;
private readonly List<string> _errorMessages = new()
{
"Error: Could not connect to the server. Please check your internet connection.",
"Warning: Your computer's operating system is not compatible with this software.",
"Error: Insufficient memory. Please close other programs and try again.",
"Warning: Your graphics card drivers may be outdated.
Please update them before playing.",
"Error: The installation file is corrupt. Please download a new copy.",
"Warning: Your computer may be running too hot.
Please check the temperature and cooling system.",
"Error: The required DirectX version is not installed on your computer.",
"Warning: Your sound card may not be supported.
Please check the system requirements.",
"Error: The installation directory is full.
Please free up space and try again.",
"Warning: Your computer's power supply may not be sufficient.
Please check the requirements.",
"Error: The installation process was interrupted.
Please restart the setup.",
"Warning: Your antivirus software may interfere with the game.
Please add it to the exception list.",
"Error: The required Microsoft library is not installed.",
"Warning: Your input devices may not be compatible.
Please check the system requirements.",
"Error: The installation process failed. Please contact support for assistance.",
"Warning: Your network speed may cause lag and disconnections.",
"Error: The setup file is not compatible with your operating system.",
"Warning: Your computer's resolution may cause display issues.",
"Error: The required Microsoft .NET Framework is not installed on your computer.",
"Warning: Your keyboard layout may cause input errors. Please check the settings."
};
private readonly Random _random = new();
private static readonly EventId EventId = new(id: 0x1A4, name: "RandomLoggingService");
#endregion
#region BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ApplicationLog.Emit(_logger, LogLevel.Information, "Started");
while (!stoppingToken.IsCancellationRequested)
{
// wait for a pre-determined interval
await Task.Delay(1000, stoppingToken).ConfigureAwait(false);
if (stoppingToken.IsCancellationRequested)
return;
// heartbeat logging
GenerateLogEntry();
}
ApplicationLog.Emit(_logger, LogLevel.Information, "Stopped");
}
public Task StartAsync()
=> StartAsync(CancellationToken.None);
public override async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Yield();
ApplicationLog.Emit(_logger, LogLevel.Information, "Starting");
await base.StartAsync(cancellationToken).ConfigureAwait(false);
}
public Task StopAsync()
=> StopAsync(CancellationToken.None);
public override async Task StopAsync(CancellationToken cancellationToken)
{
ApplicationLog.Emit(_logger, LogLevel.Information, "Stopping");
await base.StopAsync(cancellationToken).ConfigureAwait(false);
}
#endregion
#region Methods
private void GenerateLogEntry()
{
LogLevel level = _random.Next(0, 100) switch
{
< 50 => LogLevel.Information,
< 65 => LogLevel.Debug,
< 75 => LogLevel.Trace,
< 85 => LogLevel.Warning,
< 95 => LogLevel.Error,
_ => LogLevel.Critical
};
if (level < LogLevel.Error)
{
RandomServiceLog.Emit(_logger, GenerateEventId(),
level, message: GetMessage());
return;
}
RandomServiceLog.Emit(_logger, GenerateEventId(), level, message: GetMessage(),
new Exception(_errorMessages[_random.Next(0, _errorMessages.Count)]));
}
private EventId GenerateEventId()
{
int index = _random.Next(0, _eventNames.Count);
return new EventId(id: 0x1A4 + index, name: _eventNames[index]);
}
private string GetMessage()
=> _messages[_random.Next(0, _messages.Count)];
#endregion
}
要查看更新后的 RandomLoggingService
的实际效果,请下载代码并在 _MSlogger/Attribute_ 解决方案文件夹中的 WpfLoggingAttrDI
项目中运行。
摘要
我们涵盖了日志记录的工作原理;如何为 **WinForms** **WPF** 和 **Avalonia** 应用程序类型创建、注册和使用自定义 Logger & Provider,同时支持 **C#** 和 **VB**。我们查看了 .NET 内部代码以处理 Logger 和 Provider。我们为 **WinForms** **WPF** 和 **Avalonia** 应用程序类型创建了自定义控件,同时支持 **C#** 和 **VB**,以使用 Microsoft 的默认 Logger 和第三方 SeriLog 结构化 Logger 来消费日志。我们还涵盖了如何在依赖注入和手动连接中使用自定义 Logger 和自定义控件。最后,我们创建了 **.NET Framework** 后台服务来模拟生成日志条目的应用程序。
虽然本文篇幅较长且内容详实,但创建自定义 Logger 和消费生成的内容并不复杂,无论应用程序类型如何,以及应用程序是如何连接的,无论是手动的还是通过依赖注入。
所有源代码,包括 **C#** 和 **VB**,都可以在本文顶部的链接中找到。要在您自己的项目中进行使用,请复制应用程序类型所需的所有库,添加对 LogViewer
控件项目 + Logger 项目类型的引用,然后按照使用指南进行操作。
如果您有任何问题,请在下方留言,我很乐意为您解答。
参考文献
文档、文章等
-
.NET (Core) 7.0 Framework
- .NET App Settings Demystified (C# & VB) | CodeProject
- Logging in .NET | Microsoft Learn
- Implement a custom logging provider in .NET | Microsoft Learn
- Dependency injection in .NET | Microsoft Learn
- Model-View-ViewModel (MVVM) | Microsoft Learn
- Data binding overview (Windows Forms.NET) | Microsoft Learn
- Data binding overview (WPF .NET) | Microsoft Learn
- BackgroundService | Microsoft Learn
- Background tasks with hosted services in ASP.NET Core | Microsoft Learn
- Announcing .NET 6 — The Fastest .NET Yet > Microsoft.Extensions.Logging
- Compile-time logging source generation | Microsoft Learn
- What's new in .NET 8 Preview 6 > Expanding LoggerMessageAttribute Constructor Overloads for Enhanced Functionality | Github
-
Avalonia UI
-
Serilog
-
NLog
- Getting started with .NET Core 2 Console application | NLog
- Getting started with ASP.NET Core 6 | NLog
- How to write a custom target | NLog
- Register your custom component | NLog
- NLog configuration with appsettings.json | NLog
- NLog properties with Microsoft Extension Logging | NLog
- NLog.Extensions.Logging changes capture of EventId | NLog
-
Log4Net
Nuget 包
-
.NET (Core) 7.0 Framework
- Microsoft.Extensions.Configuration
- Microsoft.Extensions.Configuration.EnvironmentVariables
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Hosting
- Microsoft.Extensions.Logging
- Microsoft.Extensions.Logging.Abstractions
- Microsoft.Extensions.Options.ConfigurationExtensions
- CommunityToolkit.Mvvm 8.1.0 (用于 Avalonia)
- XamlNameReferenceGenerator 1.6.1 (用于 Avalonia)
-
Avalonia
-
Serilog
-
NLog
-
Log4Net
历史
- 2023年3月23日 - v1.00 - 初始发布
- 2023年3月28日 - v1.10 - 添加了对 NLOG 日志平台的支持 + **WinForms**、**WPF** 和 **Avalonia** 示例 DI 和非 DI 应用程序(x6);修复了
LogViewer.Winforms
项目中可能偶尔出现的“索引超出范围”异常。 - 2023年3月29日 - v1.20 = 添加了对 Apache Log4Net 日志服务 + **WinForms**、**WPF** 和 **Avalonia** 示例 DI 和非 DI 应用程序(x6)的支持;各种代码清理和优化。
- 2023年4月20日 - v1.20a - 使用 Microsoft 的文件浏览器“Compress to Zip”重新压缩项目。
- 2023年9月12日 - v1.30 - 添加了 LoggerMessageAttribute (仅限 C#) 部分。