Files
DMS/软件设计文档/原始文档/09-日志记录与聚合过滤设计.md

5.7 KiB
Raw Permalink Blame History

软件开发文档 - 09. 日志记录与聚合过滤设计

本文档详细阐述了一套基于NLog框架的、带有智能聚合过滤功能的日志系统设计方案旨在提供详细、高效且能避免“日志风暴”的日志记录能力。

1. 设计目标

  • 信息全面:日志需记录时间、级别、消息、异常、调用点(文件、方法、行号)等详细上下文。
  • 持久化存储:将日志信息存储到数据库中,便于长期查询和分析。
  • 高频聚合过滤当同一条日志在短时间内如30秒被高频触发时系统应能自动聚合只记录首次日志和最终的触发总次数以防止日志泛滥并保留关键信息。
  • 易于使用:通过简单的服务接口,让业务代码能方便地记录日志。

2. 数据库实体设计 (DMS.Infrastructure)

我们创建一个 DbLog 实体来定义日志在数据库中的存储结构。

// 文件: 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。

// 文件: 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 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

为了方便业务代码调用,可以封装一个简单的服务接口。

// 文件: 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的全局诊断上下文。

// 文件: DMS.WPF/App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
    // 在程序启动的最开始就设置好连接字符串
    GlobalDiagnosticsContext.Set("connectionString", "your_db_connection_string_here");

    // ... DI容器配置和主窗口显示
}