185 lines
5.7 KiB
Markdown
185 lines
5.7 KiB
Markdown
# 软件开发文档 - 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容器配置和主窗口显示
|
||
}
|
||
```
|