From c5ed053f2cd05920c107f658c8328e7cb2c6890e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:11:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(logging):=20=E6=B7=BB=E5=8A=A0=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=87=87=E6=A0=B7=E8=BF=87=E6=BB=A4=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E8=BF=BD=E5=8A=A0=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 SamplingFilter 类用于限制高频日志输出,支持线程安全的采样控制 - 添加 StatisticsAppender 类用于收集日志指标,包括总数量、错误率、级别分布等统计信息 - 提供时间窗口内的采样机制,可配置采样率和时间窗口参数 - 实现完整的统计报告生成功能,支持按级别和日志记录器分类展示 - 添加线程安全的数据结构确保并发环境下的数据一致性 - 提供统计重置和数据查询接口便于监控和调试 --- .../logging/appenders/StatisticsAppender.cs | 163 ++++++++++++++++++ .../logging/filters/SamplingFilter.cs | 74 ++++++++ 2 files changed, 237 insertions(+) create mode 100644 GFramework.Core/logging/appenders/StatisticsAppender.cs create mode 100644 GFramework.Core/logging/filters/SamplingFilter.cs diff --git a/GFramework.Core/logging/appenders/StatisticsAppender.cs b/GFramework.Core/logging/appenders/StatisticsAppender.cs new file mode 100644 index 0000000..664c497 --- /dev/null +++ b/GFramework.Core/logging/appenders/StatisticsAppender.cs @@ -0,0 +1,163 @@ +using System.Collections.Concurrent; +using System.Text; +using GFramework.Core.Abstractions.logging; + +namespace GFramework.Core.logging.appenders; + +/// +/// 日志统计 Appender,用于收集日志指标 +/// 线程安全:所有方法都是线程安全的 +/// +public sealed class StatisticsAppender : ILogAppender +{ + private readonly ConcurrentDictionary _levelCounts = new(); + private readonly ConcurrentDictionary _loggerCounts = new(); + private long _errorCount; + private DateTime _startTime = DateTime.UtcNow; + private long _totalCount; + + /// + /// 获取总日志数量 + /// + public long TotalCount => Interlocked.Read(ref _totalCount); + + /// + /// 获取错误日志数量(Error + Fatal) + /// + public long ErrorCount => Interlocked.Read(ref _errorCount); + + /// + /// 获取统计开始时间 + /// + public DateTime StartTime => _startTime; + + /// + /// 获取运行时长 + /// + public TimeSpan Uptime => DateTime.UtcNow - _startTime; + + /// + /// 获取错误率(错误数 / 总数) + /// + public double ErrorRate + { + get + { + var total = TotalCount; + return total == 0 ? 0 : (double)ErrorCount / total; + } + } + + /// + /// 追加日志条目 + /// + public void Append(LogEntry entry) + { + // 增加总计数 + Interlocked.Increment(ref _totalCount); + + // 增加级别计数 + _levelCounts.AddOrUpdate(entry.Level, 1, (_, count) => count + 1); + + // 增加日志记录器计数 + _loggerCounts.AddOrUpdate(entry.LoggerName, 1, (_, count) => count + 1); + + // 如果是错误级别,增加错误计数 + if (entry.Level >= LogLevel.Error) + { + Interlocked.Increment(ref _errorCount); + } + } + + /// + /// 刷新缓冲区(此 Appender 无需刷新) + /// + public void Flush() + { + // 统计 Appender 不需要刷新 + } + + /// + /// 获取指定级别的日志数量 + /// + public long GetCountByLevel(LogLevel level) + { + return _levelCounts.TryGetValue(level, out var count) ? count : 0; + } + + /// + /// 获取指定日志记录器的日志数量 + /// + public long GetCountByLogger(string loggerName) + { + return _loggerCounts.TryGetValue(loggerName, out var count) ? count : 0; + } + + /// + /// 获取所有级别的日志数量 + /// + public IReadOnlyDictionary GetLevelCounts() + { + return new Dictionary(_levelCounts); + } + + /// + /// 获取所有日志记录器的日志数量 + /// + public IReadOnlyDictionary GetLoggerCounts() + { + return new Dictionary(_loggerCounts); + } + + /// + /// 重置所有统计数据 + /// + public void Reset() + { + Interlocked.Exchange(ref _totalCount, 0); + Interlocked.Exchange(ref _errorCount, 0); + _levelCounts.Clear(); + _loggerCounts.Clear(); + _startTime = DateTime.UtcNow; + } + + /// + /// 生成统计报告 + /// + public string GenerateReport() + { + var report = new StringBuilder(); + report.AppendLine("=== 日志统计报告 ==="); + report.AppendLine($"统计时间: {_startTime:yyyy-MM-dd HH:mm:ss} - {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"); + report.AppendLine($"运行时长: {Uptime}"); + report.AppendLine($"总日志数: {TotalCount}"); + report.AppendLine($"错误日志数: {ErrorCount}"); + report.AppendLine($"错误率: {ErrorRate:P2}"); + report.AppendLine(); + + report.AppendLine("按级别统计:"); + foreach (var level in Enum.GetValues()) + { + var count = GetCountByLevel(level); + if (count > 0) + { + var percentage = (double)count / TotalCount; + report.AppendLine($" {level}: {count} ({percentage:P2})"); + } + } + + report.AppendLine(); + report.AppendLine("按日志记录器统计 (Top 10):"); + var topLoggers = _loggerCounts + .OrderByDescending(kvp => kvp.Value) + .Take(10); + + foreach (var (logger, count) in topLoggers) + { + var percentage = (double)count / TotalCount; + report.AppendLine($" {logger}: {count} ({percentage:P2})"); + } + + return report.ToString(); + } +} \ No newline at end of file diff --git a/GFramework.Core/logging/filters/SamplingFilter.cs b/GFramework.Core/logging/filters/SamplingFilter.cs new file mode 100644 index 0000000..23da89e --- /dev/null +++ b/GFramework.Core/logging/filters/SamplingFilter.cs @@ -0,0 +1,74 @@ +using System.Collections.Concurrent; +using GFramework.Core.Abstractions.logging; + +namespace GFramework.Core.logging.filters; + +/// +/// 日志采样过滤器,用于限制高频日志的输出 +/// 线程安全:所有方法都是线程安全的 +/// +public sealed class SamplingFilter : ILogFilter +{ + private readonly int _sampleRate; + private readonly ConcurrentDictionary _samplingStates = new(); + private readonly TimeSpan _timeWindow; + + /// + /// 创建日志采样过滤器 + /// + /// 采样率(每 N 条日志保留 1 条) + /// 时间窗口(在此时间内应用采样) + public SamplingFilter(int sampleRate, TimeSpan timeWindow) + { + if (sampleRate <= 0) + throw new ArgumentException("Sample rate must be greater than 0", nameof(sampleRate)); + + if (timeWindow <= TimeSpan.Zero) + throw new ArgumentException("Time window must be greater than zero", nameof(timeWindow)); + + _sampleRate = sampleRate; + _timeWindow = timeWindow; + } + + /// + /// 判断是否应该记录该日志条目 + /// + public bool ShouldLog(LogEntry entry) + { + // 为每个日志记录器维护独立的采样状态 + var key = entry.LoggerName; + var state = _samplingStates.GetOrAdd(key, _ => new SamplingState()); + + return state.ShouldLog(_sampleRate, _timeWindow); + } + + /// + /// 采样状态 + /// + private sealed class SamplingState + { + private readonly object _lock = new(); + private long _count; + private DateTime _windowStart = DateTime.UtcNow; + + public bool ShouldLog(int sampleRate, TimeSpan timeWindow) + { + lock (_lock) + { + var now = DateTime.UtcNow; + + // 检查是否需要重置时间窗口 + if (now - _windowStart >= timeWindow) + { + _windowStart = now; + _count = 0; + } + + _count++; + + // 每 N 条保留 1 条 + return _count % sampleRate == 1; + } + } + } +} \ No newline at end of file