# 软件开发文档 - 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<string, LogCacheEntry> _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
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true">

  <extensions>
    <add assembly="DMS.Infrastructure"/>
  </extensions>

  <targets>
    <target name="db" xsi:type="ThrottlingDatabase"
            connectionString="${gdc:item=connectionString}" />
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="db" />
  </rules>
</nlog>
```

## 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容器配置和主窗口显示
}
```
