diff --git a/GFramework.Core.Tests/logging/SamplingFilterTests.cs b/GFramework.Core.Tests/logging/SamplingFilterTests.cs new file mode 100644 index 0000000..64370c3 --- /dev/null +++ b/GFramework.Core.Tests/logging/SamplingFilterTests.cs @@ -0,0 +1,133 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.filters; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +[TestFixture] +public class SamplingFilterTests +{ + [Test] + public void SamplingFilter_Should_Sample_Logs_By_Rate() + { + var filter = new SamplingFilter(sampleRate: 3, timeWindow: TimeSpan.FromMinutes(1)); + var entry = new LogEntry( + DateTime.UtcNow, + LogLevel.Info, + "TestLogger", + "Test message", + null, + null + ); + + // 前 3 条:第 1 条通过,第 2、3 条被过滤 + Assert.That(filter.ShouldLog(entry), Is.True); // 1st + Assert.That(filter.ShouldLog(entry), Is.False); // 2nd + Assert.That(filter.ShouldLog(entry), Is.False); // 3rd + + // 第 4 条通过(新周期) + Assert.That(filter.ShouldLog(entry), Is.True); // 4th + Assert.That(filter.ShouldLog(entry), Is.False); // 5th + Assert.That(filter.ShouldLog(entry), Is.False); // 6th + } + + [Test] + public void SamplingFilter_Should_Reset_After_Time_Window() + { + var filter = new SamplingFilter(sampleRate: 2, timeWindow: TimeSpan.FromMilliseconds(100)); + var entry = new LogEntry( + DateTime.UtcNow, + LogLevel.Info, + "TestLogger", + "Test message", + null, + null + ); + + // 第一个窗口 + Assert.That(filter.ShouldLog(entry), Is.True); // 1st + Assert.That(filter.ShouldLog(entry), Is.False); // 2nd + + // 等待时间窗口过期 + Thread.Sleep(150); + + // 新窗口应该重置计数 + Assert.That(filter.ShouldLog(entry), Is.True); // 1st in new window + Assert.That(filter.ShouldLog(entry), Is.False); // 2nd in new window + } + + [Test] + public void SamplingFilter_Should_Maintain_Separate_State_Per_Logger() + { + var filter = new SamplingFilter(sampleRate: 2, timeWindow: TimeSpan.FromMinutes(1)); + + var entry1 = new LogEntry(DateTime.UtcNow, LogLevel.Info, "Logger1", "Message", null, null); + var entry2 = new LogEntry(DateTime.UtcNow, LogLevel.Info, "Logger2", "Message", null, null); + + // Logger1 的第一条 + Assert.That(filter.ShouldLog(entry1), Is.True); + + // Logger2 的第一条(独立计数) + Assert.That(filter.ShouldLog(entry2), Is.True); + + // Logger1 的第二条 + Assert.That(filter.ShouldLog(entry1), Is.False); + + // Logger2 的第二条 + Assert.That(filter.ShouldLog(entry2), Is.False); + } + + [Test] + public void SamplingFilter_Should_Throw_When_SampleRate_Is_Invalid() + { + Assert.Throws(() => + { + new SamplingFilter(sampleRate: 0, timeWindow: TimeSpan.FromMinutes(1)); + }); + + Assert.Throws(() => + { + new SamplingFilter(sampleRate: -1, timeWindow: TimeSpan.FromMinutes(1)); + }); + } + + [Test] + public void SamplingFilter_Should_Throw_When_TimeWindow_Is_Invalid() + { + Assert.Throws(() => { new SamplingFilter(sampleRate: 2, timeWindow: TimeSpan.Zero); }); + + Assert.Throws(() => + { + new SamplingFilter(sampleRate: 2, timeWindow: TimeSpan.FromSeconds(-1)); + }); + } + + [Test] + public void SamplingFilter_Should_Be_Thread_Safe() + { + var filter = new SamplingFilter(sampleRate: 10, timeWindow: TimeSpan.FromMinutes(1)); + var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Message", null, null); + + var passedCount = 0; + var tasks = new List(); + + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(() => + { + for (int j = 0; j < 100; j++) + { + if (filter.ShouldLog(entry)) + { + Interlocked.Increment(ref passedCount); + } + } + })); + } + + Task.WaitAll(tasks.ToArray()); + + // 1000 条日志,采样率 10,应该通过约 100 条 + Assert.That(passedCount, Is.InRange(90, 110)); + } +} \ No newline at end of file diff --git a/GFramework.Core.Tests/logging/StatisticsAppenderTests.cs b/GFramework.Core.Tests/logging/StatisticsAppenderTests.cs new file mode 100644 index 0000000..902f8cd --- /dev/null +++ b/GFramework.Core.Tests/logging/StatisticsAppenderTests.cs @@ -0,0 +1,186 @@ +using GFramework.Core.Abstractions.logging; +using GFramework.Core.logging.appenders; +using NUnit.Framework; + +namespace GFramework.Core.Tests.logging; + +[TestFixture] +public class StatisticsAppenderTests +{ + [SetUp] + public void SetUp() + { + _appender = new StatisticsAppender(); + } + + [TearDown] + public void TearDown() + { + } + + private StatisticsAppender _appender = null!; + + [Test] + public void StatisticsAppender_Should_Count_Total_Logs() + { + var entry = CreateLogEntry(LogLevel.Info, "TestLogger", "Message"); + + _appender.Append(entry); + _appender.Append(entry); + _appender.Append(entry); + + Assert.That(_appender.TotalCount, Is.EqualTo(3)); + } + + [Test] + public void StatisticsAppender_Should_Count_Errors() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info")); + _appender.Append(CreateLogEntry(LogLevel.Error, "Logger", "Error")); + _appender.Append(CreateLogEntry(LogLevel.Fatal, "Logger", "Fatal")); + _appender.Append(CreateLogEntry(LogLevel.Warning, "Logger", "Warning")); + + Assert.That(_appender.ErrorCount, Is.EqualTo(2)); // Error + Fatal + Assert.That(_appender.TotalCount, Is.EqualTo(4)); + } + + [Test] + public void StatisticsAppender_Should_Calculate_Error_Rate() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info")); + _appender.Append(CreateLogEntry(LogLevel.Error, "Logger", "Error")); + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info")); + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info")); + + // 1 error out of 4 total = 0.25 + Assert.That(_appender.ErrorRate, Is.EqualTo(0.25).Within(0.001)); + } + + [Test] + public void StatisticsAppender_Should_Count_By_Level() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info1")); + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Info2")); + _appender.Append(CreateLogEntry(LogLevel.Error, "Logger", "Error")); + _appender.Append(CreateLogEntry(LogLevel.Warning, "Logger", "Warning")); + + Assert.That(_appender.GetCountByLevel(LogLevel.Info), Is.EqualTo(2)); + Assert.That(_appender.GetCountByLevel(LogLevel.Error), Is.EqualTo(1)); + Assert.That(_appender.GetCountByLevel(LogLevel.Warning), Is.EqualTo(1)); + Assert.That(_appender.GetCountByLevel(LogLevel.Debug), Is.EqualTo(0)); + } + + [Test] + public void StatisticsAppender_Should_Count_By_Logger() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger1", "Message")); + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger1", "Message")); + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger2", "Message")); + + Assert.That(_appender.GetCountByLogger("Logger1"), Is.EqualTo(2)); + Assert.That(_appender.GetCountByLogger("Logger2"), Is.EqualTo(1)); + Assert.That(_appender.GetCountByLogger("Logger3"), Is.EqualTo(0)); + } + + [Test] + public void StatisticsAppender_Should_Track_Uptime() + { + var startTime = _appender.StartTime; + Thread.Sleep(100); + + Assert.That(_appender.Uptime, Is.GreaterThanOrEqualTo(TimeSpan.FromMilliseconds(100))); + Assert.That(_appender.StartTime, Is.EqualTo(startTime)); + } + + [Test] + public void StatisticsAppender_Should_Reset_Statistics() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger", "Message")); + _appender.Append(CreateLogEntry(LogLevel.Error, "Logger", "Error")); + + Assert.That(_appender.TotalCount, Is.EqualTo(2)); + Assert.That(_appender.ErrorCount, Is.EqualTo(1)); + + var oldStartTime = _appender.StartTime; + Thread.Sleep(10); + _appender.Reset(); + + Assert.That(_appender.TotalCount, Is.EqualTo(0)); + Assert.That(_appender.ErrorCount, Is.EqualTo(0)); + Assert.That(_appender.GetLevelCounts().Count, Is.EqualTo(0)); + Assert.That(_appender.GetLoggerCounts().Count, Is.EqualTo(0)); + Assert.That(_appender.StartTime, Is.GreaterThan(oldStartTime)); + } + + [Test] + public void StatisticsAppender_Should_Generate_Report() + { + _appender.Append(CreateLogEntry(LogLevel.Info, "Logger1", "Info")); + _appender.Append(CreateLogEntry(LogLevel.Error, "Logger2", "Error")); + _appender.Append(CreateLogEntry(LogLevel.Warning, "Logger1", "Warning")); + + var report = _appender.GenerateReport(); + + Assert.That(report, Does.Contain("总日志数: 3")); + Assert.That(report, Does.Contain("错误日志数: 1")); + Assert.That(report, Does.Contain("Info")); + Assert.That(report, Does.Contain("Error")); + Assert.That(report, Does.Contain("Warning")); + Assert.That(report, Does.Contain("Logger1")); + Assert.That(report, Does.Contain("Logger2")); + } + + [Test] + public void StatisticsAppender_Should_Return_Empty_Collections_When_No_Data() + { + Assert.That(_appender.TotalCount, Is.EqualTo(0)); + Assert.That(_appender.ErrorCount, Is.EqualTo(0)); + Assert.That(_appender.ErrorRate, Is.EqualTo(0)); + Assert.That(_appender.GetLevelCounts().Count, Is.EqualTo(0)); + Assert.That(_appender.GetLoggerCounts().Count, Is.EqualTo(0)); + } + + [Test] + public void StatisticsAppender_Should_Be_Thread_Safe() + { + var tasks = new List(); + + for (int i = 0; i < 10; i++) + { + var loggerId = i; + tasks.Add(Task.Run(() => + { + for (int j = 0; j < 100; j++) + { + var level = j % 2 == 0 ? LogLevel.Info : LogLevel.Error; + _appender.Append(CreateLogEntry(level, $"Logger{loggerId}", "Message")); + } + })); + } + + Task.WaitAll(tasks.ToArray()); + + Assert.That(_appender.TotalCount, Is.EqualTo(1000)); + Assert.That(_appender.ErrorCount, Is.EqualTo(500)); + Assert.That(_appender.GetCountByLevel(LogLevel.Info), Is.EqualTo(500)); + Assert.That(_appender.GetCountByLevel(LogLevel.Error), Is.EqualTo(500)); + } + + [Test] + public void Flush_Should_Not_Throw() + { + Assert.DoesNotThrow(() => _appender.Flush()); + } + + private static LogEntry CreateLogEntry(LogLevel level, string loggerName, string message) + { + return new LogEntry( + DateTime.UtcNow, + level, + loggerName, + message, + null, + null + ); + } +} \ No newline at end of file