From f039fab10d9774ffd149a8175dc674992dd7feb1 Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Sun, 6 Jul 2025 20:15:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=97=A5=E5=BF=97=E8=8A=82?= =?UTF-8?q?=E6=B5=81=E5=8A=9F=E8=83=BD=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=90=8C?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E6=97=A5=E5=BF=97=E8=A2=AB=E5=A4=9A=E6=AC=A1?= =?UTF-8?q?=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Helper/NlogHelper.cs | 206 ++++++++++++++++++++++++++------ Services/S7BackgroundService.cs | 6 +- 2 files changed, 173 insertions(+), 39 deletions(-) diff --git a/Helper/NlogHelper.cs b/Helper/NlogHelper.cs index 66dbf14..2341b38 100644 --- a/Helper/NlogHelper.cs +++ b/Helper/NlogHelper.cs @@ -1,10 +1,15 @@ -using System.Runtime.CompilerServices; + +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; using NLog; namespace PMSWPF.Helper; /// /// NLog 日志帮助类,提供简化的日志记录方法,并自动捕获调用者信息。 +/// 新增了日志节流功能,以防止在短时间内产生大量重复的日志(日志风暴)。 /// public static class NlogHelper { @@ -14,85 +19,214 @@ public static class NlogHelper private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// - /// 记录一个错误级别的日志。 + /// 内部类,用于存储节流日志的状态信息。 + /// + private class ThrottledLogInfo + { + /// + /// 日志在节流时间窗口内的调用次数。 + /// 使用 int 类型,并通过 Interlocked.Increment 进行原子性递增,确保线程安全。 + /// + public int Count; + + /// + /// 用于在节流时间结束后执行操作的计时器。 + /// + public Timer Timer; + + /// + /// 调用日志方法的源文件完整路径。 + /// + public string CallerFilePath; + + /// + /// 调用日志方法的成员或属性名称。 + /// + public string CallerMember; + + /// + /// 调用日志方法的行号。 + /// + public int CallerLineNumber; + + /// + /// 日志级别 (e.g., Info, Error)。 + /// + public LogLevel Level; + + /// + /// 关联的异常对象(如果有)。 + /// + public Exception Exception; + } + + /// + /// 线程安全的字典,用于存储正在被节流的日志。 + /// 键 (string) 是根据日志消息和调用位置生成的唯一标识。 + /// 值 (ThrottledLogInfo) 是该日志的节流状态信息。 + /// + private static readonly ConcurrentDictionary ThrottledLogs = new ConcurrentDictionary(); + + /// + /// 定义节流的时间窗口(单位:秒)。 + /// + private const int ThrottleTimeSeconds = 10; + + /// + /// 内部核心日志记录方法,包含了节流逻辑。 + /// + /// 日志消息内容。 + /// NLog 的日志级别。 + /// 可选的异常对象。 + /// 是否启用节流。 + /// 调用此方法的源文件完整路径。 + /// 调用此方法的成员或属性名称。 + /// 调用此方法的行号。 + private static void LogInternal(string msg, LogLevel level, Exception exception, bool throttle, string callerFilePath, string callerMember, int callerLineNumber) + { + // 如果不启用节流,则直接记录日志并返回。 + if (!throttle) + { + LogWithContext(msg, level, exception, callerFilePath, callerMember, callerLineNumber); + return; + } + + // 使用消息内容和调用位置生成唯一键,以区分不同的日志来源。 + var key = $"{callerFilePath}:{callerLineNumber}:{msg}"; + + // 使用 AddOrUpdate 实现原子操作,确保线程安全。 + // 它会尝试添加一个新的节流日志条目,如果键已存在,则更新现有条目。 + ThrottledLogs.AddOrUpdate( + key, + // --- 添加逻辑 (addValueFactory):当日志第一次被节流时执行 --- + _ => + { + // 1. 首次出现,立即记录一次原始日志。 + LogWithContext(msg, level, exception, callerFilePath, callerMember, callerLineNumber); + + // 2. 创建一个新的节流信息对象。 + var newThrottledLog = new ThrottledLogInfo + { + Count = 1, + CallerFilePath = callerFilePath, + CallerMember = callerMember, + CallerLineNumber = callerLineNumber, + Level = level, + Exception = exception + }; + + // 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} 次。"; + LogWithContext(summaryMsg, finishedLog.Level, finishedLog.Exception, finishedLog.CallerFilePath, finishedLog.CallerMember, finishedLog.CallerLineNumber); + } + } + }, null, ThrottleTimeSeconds * 1000, Timeout.Infinite); // 设置30秒后触发,且不重复。 + + return newThrottledLog; + }, + // --- 更新逻辑 (updateValueFactory):当日志在节流窗口内再次被调用时执行 --- + (_, existingLog) => + { + // 只需将调用次数加一。使用 Interlocked.Increment 保证原子操作,避免多线程下的竞态条件。 + Interlocked.Increment(ref existingLog.Count); + return existingLog; + }); + } + + /// + /// 将日志信息包装在 NLog 的 MappedDiagnosticsLogicalContext 中进行记录。 + /// 这允许在 nlog.config 配置文件中使用 ${mdlc:item=...} 来获取调用者信息。 + /// + private static void LogWithContext(string msg, LogLevel level, Exception exception, string callerFilePath, string callerMember, int callerLineNumber) + { + using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) + using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) + using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) + { + if (exception != null) + { + Logger.Log(level, exception, msg); + } + else + { + Logger.Log(level, msg); + } + } + } + + /// + /// 记录一个错误级别的日志,支持节流。 /// /// 日志消息内容。 /// 可选:要记录的异常对象。 + /// 是否启用日志节流。如果为 true,则在30秒内对来自同一位置的相同日志消息进行节流处理。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void Error(string msg, Exception exception = null, + public static void Error(string msg, Exception exception = null, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - // 使用 using 语句确保 MappedDiagnosticsLogicalContext 在作用域结束时被清理。 - // 这对于异步方法尤其重要,因为上下文会随着异步操作的流转而传递。 - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - Logger.Error(exception, msg); - } + LogInternal(msg, LogLevel.Error, exception, throttle, callerFilePath, callerMember, callerLineNumber); } /// - /// 记录一个信息级别的日志。 + /// 记录一个信息级别的日志,支持节流。 /// /// 日志消息内容。 + /// 是否启用日志节流。如果为 true,则在30秒内对来自同一位置的相同日志消息进行节流处理。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void Info(string msg, + public static void Info(string msg, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - Logger.Info(msg); - } + LogInternal(msg, LogLevel.Info, null, throttle, callerFilePath, callerMember, callerLineNumber); } /// - /// 记录一个警告级别的日志。 + /// 记录一个警告级别的日志,支持节流。 /// /// 日志消息内容。 + /// 是否启用日志节流。如果为 true,则在30秒内对来自同一位置的相同日志消息进行节流处理。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void Warn(string msg, + public static void Warn(string msg, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - Logger.Warn(msg); - } + LogInternal(msg, LogLevel.Warn, null, throttle, callerFilePath, callerMember, callerLineNumber); } /// - /// 记录一个跟踪级别的日志。 + /// 记录一个跟踪级别的日志,支持节流。 /// /// 日志消息内容。 + /// 是否启用日志节流。如果为 true,则在30秒内对来自同一位置的相同日志消息进行节流处理。 /// 自动捕获:调用此方法的源文件完整路径。 /// 自动捕获:调用此方法的成员或属性名称。 /// 自动捕获:调用此方法的行号。 - public static void Trace(string msg, + public static void Trace(string msg, bool throttle = false, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMember = "", [CallerLineNumber] int callerLineNumber = 0) { - using (MappedDiagnosticsLogicalContext.SetScoped("CallerFilePath", callerFilePath)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerLineNumber", callerLineNumber)) - using (MappedDiagnosticsLogicalContext.SetScoped("CallerMember", callerMember)) - { - Logger.Trace(msg); - } + LogInternal(msg, LogLevel.Trace, null, throttle, callerFilePath, callerMember, callerLineNumber); } -} \ No newline at end of file +} diff --git a/Services/S7BackgroundService.cs b/Services/S7BackgroundService.cs index 1b074ae..108a39d 100644 --- a/Services/S7BackgroundService.cs +++ b/Services/S7BackgroundService.cs @@ -137,10 +137,10 @@ namespace PMSWPF.Services PollS7Devices(stoppingToken); // 执行S7设备轮询 stopwatch.Stop(); // 停止计时器 // _logger.LogDebug($"结束轮询变量,当前时间:{DateTime.Now}"); - NlogHelper.Info($"读取变量数:{readCount}个,跳过变量数:{TGCount}总耗时:{stopwatch.ElapsedMilliseconds}ms"); + NlogHelper.Info($"读取变量数:{readCount}个,跳过变量数:{TGCount}",throttle:true); // 短暂休眠以防止CPU占用过高,并控制轮询频率。 - Thread.Sleep(1000); + Thread.Sleep(100); } NlogHelper.Info("S7轮询线程已停止。"); @@ -199,7 +199,7 @@ namespace PMSWPF.Services if (_s7Devices == null || !_s7Devices.Any()) { NlogHelper.Info( - "未找到活跃的S7设备进行轮询。等待5秒后重试。"); + "未找到活跃的S7设备进行轮询。等待5秒后重试。",throttle:true); try { // 使用CancellationToken来使等待可取消。