按照软件设计文档开始重构代码01
This commit is contained in:
184
软件设计文档/原始文档/09-日志记录与聚合过滤设计.md
Normal file
184
软件设计文档/原始文档/09-日志记录与聚合过滤设计.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 软件开发文档 - 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容器配置和主窗口显示
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user