From 653a1d87490c050aec2024dc2d93215d9d835a2d Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Thu, 4 Sep 2025 14:46:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Nlog=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DMS.Core/DMS.Core.csproj | 9 +- DMS.WPF/App.xaml | 2 +- DMS.WPF/App.xaml.cs | 44 ++-- .../Configurations/nlog.config | 4 +- DMS.WPF/DMS.WPF.csproj | 9 + DMS.WPF/Logging/NLogLogger.cs | 210 ++++++++++++++++++ DMS.WPF/Logging/NLogLoggerFactory.cs | 38 ++++ DMS.WPF/ViewModels/SplashViewModel.cs | 8 +- 软件设计文档/07-日志服务使用说明.md | 113 ++++++++++ 9 files changed, 399 insertions(+), 38 deletions(-) rename {DMS.Core => DMS.WPF}/Configurations/nlog.config (97%) create mode 100644 DMS.WPF/Logging/NLogLogger.cs create mode 100644 DMS.WPF/Logging/NLogLoggerFactory.cs create mode 100644 软件设计文档/07-日志服务使用说明.md diff --git a/DMS.Core/DMS.Core.csproj b/DMS.Core/DMS.Core.csproj index e0c93f7..09472c2 100644 --- a/DMS.Core/DMS.Core.csproj +++ b/DMS.Core/DMS.Core.csproj @@ -6,16 +6,9 @@ enable - - - - - Always - - - + diff --git a/DMS.WPF/App.xaml b/DMS.WPF/App.xaml index 5da72cb..1c33f54 100644 --- a/DMS.WPF/App.xaml +++ b/DMS.WPF/App.xaml @@ -1,5 +1,5 @@  /// Interaction logic for App.xaml @@ -65,10 +64,6 @@ public partial class App : System.Windows.Application try { - // var databaseInitializer = Host.Services.GetRequiredService(); - // databaseInitializer.InitializeDataBase(); - // await databaseInitializer.InitializeMenu(); - // Settings = AppSettings.Load(); Host.Services.GetRequiredService(); // 初始化数据处理链 @@ -110,17 +105,14 @@ public partial class App : System.Windows.Application private void ConfigureServices(IServiceCollection services) { - // services.AddTransient(); + // 注册NLogLogger作为Microsoft.Extensions.Logging.ILogger的实现 + services.AddSingleton(); + services.AddSingleton(); // - // - // services.AddSingleton(); - // services.AddSingleton(); - // //services.AddSingleton(); services.AddSingleton(); // services.AddHostedService(); // services.AddHostedService(); // services.AddHostedService(); - // // --- 核心配置 --- services.AddAutoMapper(cfg => @@ -204,7 +196,7 @@ public partial class App : System.Windows.Application private void ConfigureLogging(ILoggingBuilder loggingBuilder) { - LogManager.Setup().LoadConfigurationFromFile("Config/nlog.config"); + LogManager.Setup().LoadConfigurationFromFile("Configurations/nlog.config"); loggingBuilder.ClearProviders(); loggingBuilder.SetMinimumLevel(LogLevel.Trace); // loggingBuilder.AddNLog(); diff --git a/DMS.Core/Configurations/nlog.config b/DMS.WPF/Configurations/nlog.config similarity index 97% rename from DMS.Core/Configurations/nlog.config rename to DMS.WPF/Configurations/nlog.config index 2360386..27ed46f 100644 --- a/DMS.Core/Configurations/nlog.config +++ b/DMS.WPF/Configurations/nlog.config @@ -26,9 +26,9 @@ + connectionString="server=127.0.0.1;port=3306;user=root;password=Pgw15221236646; database=dms_test; "> - INSERT INTO nlog ( + INSERT INTO dbnlog ( LogTime, Level, ThreadID,ThreadName,Callsite,CallsiteLineNumber,Message, Logger, Exception, CallerFilePath, CallerLineNumber,CallerMember ) VALUES ( diff --git a/DMS.WPF/DMS.WPF.csproj b/DMS.WPF/DMS.WPF.csproj index 5f00591..82a64df 100644 --- a/DMS.WPF/DMS.WPF.csproj +++ b/DMS.WPF/DMS.WPF.csproj @@ -154,6 +154,8 @@ + + @@ -168,4 +170,11 @@ + + + + Always + + + diff --git a/DMS.WPF/Logging/NLogLogger.cs b/DMS.WPF/Logging/NLogLogger.cs new file mode 100644 index 0000000..41d6b74 --- /dev/null +++ b/DMS.WPF/Logging/NLogLogger.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using Microsoft.Extensions.Logging; +using NLog; +using ILogger = Microsoft.Extensions.Logging.ILogger; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using NLogLevel = NLog.LogLevel; + +namespace DMS.WPF.Logging; + +/// +/// NLog日志服务实现,直接使用NLog记录日志,并实现Microsoft.Extensions.Logging.ILogger接口 +/// +public class NLogLogger : ILogger +{ + /// + /// 获取当前类的 NLog 日志实例。 + /// + private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + /// + /// 日志记录器名称 + /// + private readonly string _name; + + public NLogLogger() + { + _name = nameof(NLogLogger); + } + + public NLogLogger(string name) + { + _name = name; + } + + /// + /// 内部类,用于存储节流日志的状态信息。 + /// + private class ThrottledLogInfo + { + /// + /// 日志在节流时间窗口内的调用次数。 + /// 使用 int 类型,并通过 Interlocked.Increment 进行原子性递增,确保线程安全。 + /// + public int Count; + + /// + /// 用于在节流时间结束后执行操作的计时器。 + /// + public Timer Timer; + } + + /// + /// 线程安全的字典,用于存储正在被节流的日志。 + /// 键 (string) 是根据日志消息生成的唯一标识。 + /// 值 (ThrottledLogInfo) 是该日志的节流状态信息。 + /// + private static readonly ConcurrentDictionary ThrottledLogs = new ConcurrentDictionary(); + + /// + /// 定义节流的时间窗口(单位:秒)。 + /// + private const int ThrottleTimeSeconds = 30; + + /// + /// 内部核心日志记录方法,包含了节流逻辑。 + /// + /// 日志消息内容。 + /// NLog 的日志级别。 + /// 可选的异常对象。 + /// 是否启用节流。 + private void LogInternal(string msg, NLogLevel level, Exception exception, bool throttle) + { + // 如果不启用节流,则直接记录日志并返回。 + if (!throttle) + { + Logger.Log(level, exception, msg); + return; + } + + // 使用消息内容生成唯一键,以区分不同的日志来源。 + var key = msg; + + // 使用 AddOrUpdate 实现原子操作,确保线程安全。 + // 它会尝试添加一个新的节流日志条目,如果键已存在,则更新现有条目。 + ThrottledLogs.AddOrUpdate( + key, + // --- 添加逻辑 (addValueFactory):当日志第一次被节流时执行 --- + _ => + { + // 1. 首次出现,立即记录一次原始日志。 + Logger.Log(level, exception, msg); + + // 2. 创建一个新的节流信息对象。 + var newThrottledLog = new ThrottledLogInfo + { + Count = 1 + }; + + // 3. 创建并启动一个一次性计时器。 + newThrottledLog.Timer = new Timer(s => + { + // --- 计时器回调:在指定时间(例如30秒)后触发 --- + // 尝试从字典中移除当前日志条目。 + if (ThrottledLogs.TryRemove(key, out var finishedLog)) + { + // 释放计时器资源。 + finishedLog.Timer.Dispose(); + // 如果在节流期间有后续调用(Count > 1),则记录一条摘要日志。 + if (finishedLog.Count > 1) + { + var summaryMsg = $"日志 '{msg}' 在过去 {ThrottleTimeSeconds} 秒内被调用 {finishedLog.Count} 次。"; + Logger.Log(level, summaryMsg); + } + } + }, null, ThrottleTimeSeconds * 1000, Timeout.Infinite); // 设置30秒后触发,且不重复。 + + return newThrottledLog; + }, + // --- 更新逻辑 (updateValueFactory):当日志在节流窗口内再次被调用时执行 --- + (_, existingLog) => + { + // 只需将调用次数加一。使用 Interlocked.Increment 保证原子操作,避免多线程下的竞态条件。 + Interlocked.Increment(ref existingLog.Count); + return existingLog; + }); + } + + public IDisposable BeginScope(TState state) + { + return null; // NLog不直接支持作用域 + } + + public bool IsEnabled(LogLevel logLevel) + { + return Logger.IsEnabled(ToNLogLevel(logLevel)); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + var nlogLevel = ToNLogLevel(logLevel); + + switch (logLevel) + { + case LogLevel.Trace: + LogInternal(message, nlogLevel, exception, false); + break; + case LogLevel.Debug: + LogInternal(message, nlogLevel, exception, false); + break; + case LogLevel.Information: + LogInternal(message, nlogLevel, exception, false); + break; + case LogLevel.Warning: + LogInternal(message, nlogLevel, exception, false); + break; + case LogLevel.Error: + LogInternal(message, nlogLevel, exception, false); + break; + case LogLevel.Critical: + LogInternal($"[Critical] {message}", nlogLevel, exception, false); + break; + } + } + + private NLogLevel ToNLogLevel(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => NLogLevel.Trace, + LogLevel.Debug => NLogLevel.Debug, + LogLevel.Information => NLogLevel.Info, + LogLevel.Warning => NLogLevel.Warn, + LogLevel.Error => NLogLevel.Error, + LogLevel.Critical => NLogLevel.Fatal, + _ => NLogLevel.Debug + }; + } + + // 保持原有的自定义方法以提供向后兼容性 + public void LogError(string message, Exception exception = null) + { + LogInternal(message, NLogLevel.Error, exception, true); + } + + public void LogInfo(string message) + { + LogInternal(message, NLogLevel.Info, null, true); + } + + public void LogWarning(string message) + { + LogInternal(message, NLogLevel.Warn, null, true); + } + + public void LogDebug(string message) + { + LogInternal(message, NLogLevel.Debug, null, true); + } + + public void LogTrace(string message) + { + LogInternal(message, NLogLevel.Trace, null, true); + } +} \ No newline at end of file diff --git a/DMS.WPF/Logging/NLogLoggerFactory.cs b/DMS.WPF/Logging/NLogLoggerFactory.cs new file mode 100644 index 0000000..ab0cc3c --- /dev/null +++ b/DMS.WPF/Logging/NLogLoggerFactory.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; + +namespace DMS.WPF.Logging; + +/// +/// NLog ILoggerFactory实现,用于创建命名的NLogLogger实例 +/// 这个工厂类允许通过类别名称创建不同的Logger实例, +/// 从而可以区分不同组件或模块的日志输出 +/// +public class NLogLoggerFactory : ILoggerFactory +{ + /// + /// 添加日志提供程序(NLog不使用此机制,保留为空实现) + /// + /// 日志提供程序 + public void AddProvider(ILoggerProvider provider) + { + // NLog不使用providers机制,所以这里留空 + } + + /// + /// 创建指定类别的Logger实例 + /// + /// 日志类别名称(通常是类的全名) + /// ILogger实例 + public ILogger CreateLogger(string categoryName) + { + return new NLogLogger(categoryName); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + // 清理资源(如果需要) + } +} \ No newline at end of file diff --git a/DMS.WPF/ViewModels/SplashViewModel.cs b/DMS.WPF/ViewModels/SplashViewModel.cs index 42c7e6b..5ff9bd6 100644 --- a/DMS.WPF/ViewModels/SplashViewModel.cs +++ b/DMS.WPF/ViewModels/SplashViewModel.cs @@ -7,8 +7,10 @@ using DMS.WPF.Services; using System; using System.Threading.Tasks; using DMS.Application.Services; +using DMS.WPF.Helper; using DMS.WPF.Views; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace DMS.WPF.ViewModels; @@ -17,6 +19,7 @@ namespace DMS.WPF.ViewModels; /// public partial class SplashViewModel : ObservableObject { + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly IInitializeService _initializeService; private readonly IDataCenterService _dataCenterService; @@ -25,9 +28,10 @@ public partial class SplashViewModel : ObservableObject [ObservableProperty] private string _loadingMessage = "正在加载..."; - public SplashViewModel(IServiceProvider serviceProvider, IInitializeService initializeService, + public SplashViewModel( ILogger logger,IServiceProvider serviceProvider, IInitializeService initializeService, IDataCenterService dataCenterService, DataServices dataServices) { + _logger = logger; _serviceProvider = serviceProvider; _initializeService = initializeService; this._dataCenterService = dataCenterService; @@ -41,6 +45,8 @@ public partial class SplashViewModel : ObservableObject { try { + + _logger.LogInformation("正在初始化数据库..."); LoadingMessage = "正在初始化数据库..."; _initializeService.InitializeTables(); _initializeService.InitializeMenus(); diff --git a/软件设计文档/07-日志服务使用说明.md b/软件设计文档/07-日志服务使用说明.md new file mode 100644 index 0000000..66de951 --- /dev/null +++ b/软件设计文档/07-日志服务使用说明.md @@ -0,0 +1,113 @@ +# DMS日志服务使用说明 + +## 概述 + +DMS项目现在支持Microsoft.Extensions.Logging.ILogger接口的日志服务: +1. NLogLogger类直接实现Microsoft.Extensions.Logging.ILogger接口(位于DMS.WPF项目中) +2. 提供ILoggerFactory用于创建命名的Logger实例(位于DMS.WPF项目中) + +## 如何注册日志服务 + +在`Startup.cs`或程序的依赖注入配置中,使用以下扩展方法注册日志服务: + +```csharp +using DMS.Application; + +// 在ConfigureServices方法中 +services.AddDmsServices(); +``` + +这将注册以下服务: +- `Microsoft.Extensions.Logging.ILogger` 接口及其实现 `NLogLogger`(单例,位于DMS.WPF.Logging命名空间) +- `Microsoft.Extensions.Logging.ILoggerFactory` 接口及其实现 `NLogLoggerFactory`(单例,位于DMS.WPF.Logging命名空间) + +## 如何使用日志服务 + +### 1. 使用构造函数注入ILogger(推荐) + +```csharp +using Microsoft.Extensions.Logging; + +public class MyService +{ + private readonly ILogger _logger; + + public MyService(ILogger logger) + { + _logger = logger; + } + + public void DoSomething() + { + _logger.LogInformation("正在执行操作..."); + + try + { + // 业务逻辑 + } + catch (Exception ex) + { + _logger.LogError(ex, "操作失败"); + } + } +} +``` + +### 2. 使用ILoggerFactory创建命名的Logger + +```csharp +using Microsoft.Extensions.Logging; + +public class MyService +{ + private readonly ILogger _logger; + + public MyService(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("MyService"); + } + + public void DoSomething() + { + _logger.LogInformation("正在执行操作..."); + + try + { + // 业务逻辑 + } + catch (Exception ex) + { + _logger.LogError(ex, "操作失败"); + } + } +} +``` + +## NLogLoggerFactory的作用 + +`NLogLoggerFactory`是`ILoggerFactory`接口的实现,主要作用是: + +1. **创建命名的Logger实例**:通过`CreateLogger(string categoryName)`方法,可以为不同的类或模块创建具有特定名称的Logger实例 +2. **支持依赖注入**:作为服务注册后,其他类可以通过构造函数注入`ILoggerFactory`来创建Logger实例 +3. **符合标准**:实现Microsoft.Extensions.Logging标准,确保与其他.NET日志组件兼容 + +## 日志级别映射 + +Microsoft.Extensions.Logging与NLog之间的日志级别映射如下: + +| Microsoft.Extensions.Logging | NLog | +|-----------------------------|----------| +| Trace | Trace | +| Debug | Debug | +| Information | Info | +| Warning | Warn | +| Error | Error | +| Critical | Fatal | + +## 节流功能 + +NLogLogger提供了节流功能,以防止在短时间内产生大量重复的日志(日志风暴)。默认情况下,所有日志都会启用节流,节流时间窗口为30秒。 + +## 配置 + +NLog的配置文件位于 `DMS.Core\Configurations\nlog.config`,可以根据需要进行修改。 \ No newline at end of file