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来使等待可取消。