# 软件开发文档 - 09. 日志记录与聚合过滤设计 本文档详细阐述了一套基于NLog框架的、带有智能聚合过滤功能的日志系统设计方案,旨在提供详细、高效且能避免“日志风暴”的日志记录能力。 ## 1. 设计目标 * **信息全面**:日志需记录时间、级别、消息、异常、调用点(文件、方法、行号)等详细上下文。 * **持久化存储**:将日志信息存储到数据库中,便于长期查询和分析。 * **高频聚合过滤**:当同一条日志在短时间内(如30秒)被高频触发时,系统应能自动聚合,只记录首次日志和最终的触发总次数,以防止日志泛滥并保留关键信息。 * **易于使用**:通过简单的服务接口,让业务代码能方便地记录日志。 ## 2. 数据库实体设计 (`DMS.Infrastructure`) 我们创建一个 `DbLog` 实体来定义日志在数据库中的存储结构。 ```csharp // 文件: DMS.Infrastructure/Entities/DbLog.cs using SqlSugar; using System; namespace DMS.Infrastructure.Entities; [SugarTable("Logs")] public class DbLog { [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] public long Id { get; set; } public DateTime Logged { get; set; } public string Level { get; set; } [SugarColumn(Length = -1)] public string Message { get; set; } [SugarColumn(IsNullable = true, Length = -1)] public string Exception { get; set; } public string CallSite { get; set; } public string MethodName { get; set; } public int AggregatedCount { get; set; } = 1; } ``` ## 3. NLog 自定义Target (`DMS.Infrastructure`) 这是实现聚合过滤功能的核心。我们创建一个继承自 `NLog.Targets.TargetWithLayout` 的自定义Target。 ```csharp // 文件: DMS.Infrastructure/Logging/ThrottlingDatabaseTarget.cs using NLog; using NLog.Targets; using System.Collections.Concurrent; namespace DMS.Infrastructure.Logging; // 内部类,用于存储日志缓存信息 file class LogCacheEntry { public LogEventInfo FirstLogEvent { get; set; } public int Count { get; set; } public Timer Timer { get; set; } } [Target("ThrottlingDatabase")] public class ThrottlingDatabaseTarget : TargetWithLayout { private readonly ConcurrentDictionary _throttleCache = new(); private readonly TimeSpan _throttleTime = TimeSpan.FromSeconds(30); public string ConnectionString { get; set; } protected override void Write(LogEventInfo logEvent) { string logKey = $"{logEvent.Level}|{logEvent.Message}|{logEvent.CallerFilePath}|{logEvent.CallerLineNumber}"; if (_throttleCache.TryGetValue(logKey, out var entry)) { Interlocked.Increment(ref entry.Count); } else { var newEntry = new LogCacheEntry { FirstLogEvent = logEvent, Count = 1 }; newEntry.Timer = new Timer(_ => FlushEntry(logKey), null, _throttleTime, Timeout.InfiniteTime); if (_throttleCache.TryAdd(logKey, newEntry)) { WriteToDatabase(logEvent, 1); } else if (_throttleCache.TryGetValue(logKey, out var existingEntry)) { Interlocked.Increment(ref existingEntry.Count); } } } private void FlushEntry(string logKey) { if (_throttleCache.TryRemove(logKey, out var entry)) { entry.Timer?.Dispose(); if (entry.Count > 1) { var aggMsg = $"[聚合日志] 此消息在过去30秒内共出现 {entry.Count} 次。首次消息: {entry.FirstLogEvent.FormattedMessage}"; var aggEvent = new LogEventInfo(entry.FirstLogEvent.Level, entry.FirstLogEvent.LoggerName, aggMsg); // 此处应复制其他上下文属性到 aggEvent WriteToDatabase(aggEvent, entry.Count); } } } private void WriteToDatabase(LogEventInfo logEvent, int count) { // 使用 ConnectionString 创建 SqlSugarClient 实例 // 将 logEvent 映射到 DbLog 实体并插入数据库 // var dbLog = new DbLog { ..., AggregatedCount = count }; } } ``` ## 4. NLog 配置 (`nlog.config`) 在WPF项目中添加 `nlog.config` 文件,并设置为“如果较新则复制”。 ```xml ``` ## 5. 封装与初始化 ### 5.1. `ILoggerService` 为了方便业务代码调用,可以封装一个简单的服务接口。 ```csharp // 文件: DMS.Application/Interfaces/ILoggerService.cs public interface ILoggerService { void Info(string message); void Warn(string message); void Error(Exception ex, string message = null); } // 文件: DMS.Infrastructure/Logging/NLogService.cs public class NLogService : ILoggerService { private static readonly ILogger _logger = LogManager.GetCurrentClassLogger(); // ... 实现接口方法 } ``` ### 5.2. 初始化 在应用程序启动时,必须将数据库连接字符串提供给NLog的全局诊断上下文。 ```csharp // 文件: DMS.WPF/App.xaml.cs protected override void OnStartup(StartupEventArgs e) { // 在程序启动的最开始就设置好连接字符串 GlobalDiagnosticsContext.Set("connectionString", "your_db_connection_string_here"); // ... DI容器配置和主窗口显示 } ```