From 49609d3821c977d571f9692a862492113d1bc537 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:54:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(logging):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 AsyncLogAppender 构造函数中添加 processingErrorHandler 参数用于处理后台异常 - 实现 ReportProcessingError 方法安全上报后台处理异常而不影响处理循环 - 更新文档注释说明异常处理机制和错误回调用途 - 修改测试用例验证异常处理回调功能的正确性 - 确保错误观察者异常不会终止日志处理线程 - 移除直接写入控制台错误输出的逻辑改为统一回调处理 --- .../Logging/AsyncLogAppenderTests.cs | 35 ++++++++++- .../Logging/Appenders/AsyncLogAppender.cs | 58 ++++++++++++++++--- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs index 65dbc49..cc22646 100644 --- a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs +++ b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs @@ -1,6 +1,5 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging.Appenders; -using NUnit.Framework; namespace GFramework.Core.Tests.Logging; @@ -152,8 +151,12 @@ public class AsyncLogAppenderTests [Test] public void Append_WhenInnerAppenderThrows_ShouldNotCrash() { + var reportedExceptions = new List(); var innerAppender = new ThrowingAppender(); - using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); + using var asyncAppender = new AsyncLogAppender( + innerAppender, + bufferSize: 1000, + processingErrorHandler: reportedExceptions.Add); // 即使内部 Appender 抛出异常,也不应该影响调用线程 Assert.DoesNotThrow(() => @@ -165,7 +168,33 @@ public class AsyncLogAppenderTests } }); - Thread.Sleep(100); // 等待后台处理 + asyncAppender.Flush(); + + Assert.That(reportedExceptions, Has.Count.EqualTo(10)); + Assert.That(reportedExceptions, Has.All.TypeOf()); + Assert.That(reportedExceptions.Select(static exception => exception.Message), + Has.All.EqualTo("Test exception")); + } + + [Test] + public void Append_WhenProcessingErrorHandlerThrows_ShouldStillNotCrash() + { + var innerAppender = new ThrowingAppender(); + using var asyncAppender = new AsyncLogAppender( + innerAppender, + bufferSize: 1000, + processingErrorHandler: static _ => throw new InvalidOperationException("Observer failure")); + + Assert.DoesNotThrow(() => + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + }); + + Assert.That(asyncAppender.Flush(), Is.True); } // 辅助测试类 diff --git a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs index ee67b32..9d52b9b 100644 --- a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs +++ b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs @@ -4,14 +4,24 @@ using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Appenders; /// -/// 异步日志输出器,使用 Channel 实现非阻塞日志写入 +/// 异步日志输出器,使用 将调用线程与慢速日志目标解耦。 /// -public sealed class AsyncLogAppender : ILogAppender, IDisposable +/// +/// +/// 该输出器在后台线程中顺序消费日志条目,因此调用方不会因为文件 IO 或其他慢速输出目标而阻塞。 +/// +/// +/// 内部输出器抛出的异常不会重新抛回调用线程;如需观察后台处理失败,请在构造函数中提供 +/// processingErrorHandler 回调。 +/// +/// +public sealed class AsyncLogAppender : ILogAppender { private readonly Channel _channel; private readonly CancellationTokenSource _cts; private readonly SemaphoreSlim _flushSemaphore = new(0, 1); private readonly ILogAppender _innerAppender; + private readonly Action? _processingErrorHandler; private readonly Task _processingTask; private bool _disposed; private volatile bool _flushRequested; @@ -21,9 +31,17 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable /// /// 内部日志输出器 /// 缓冲区大小(默认 10000) - public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000) + /// + /// 后台处理日志时的错误回调。 + /// 默认值为 ,表示吞掉内部异常以避免污染宿主标准错误输出。 + /// + public AsyncLogAppender( + ILogAppender innerAppender, + int bufferSize = 10000, + Action? processingErrorHandler = null) { _innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender)); + _processingErrorHandler = processingErrorHandler; if (bufferSize <= 0) throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize)); @@ -138,7 +156,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable } /// - /// 后台处理日志的异步方法 + /// 后台处理日志的异步方法。 + /// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。 /// private async Task ProcessLogsAsync(CancellationToken cancellationToken) { @@ -152,8 +171,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable } catch (Exception ex) { - // 记录内部错误到控制台(避免递归) - await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}"); + // 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。 + ReportProcessingError(ex); } // 检查是否有刷新请求且通道已空 @@ -175,7 +194,7 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable } catch (Exception ex) { - await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}"); + ReportProcessingError(ex); } finally { @@ -184,10 +203,31 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable { _innerAppender.Flush(); } - catch + catch (Exception ex) { - // 忽略刷新错误 + ReportProcessingError(ex); } } } + + /// + /// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。 + /// + /// 后台处理中捕获到的异常。 + private void ReportProcessingError(Exception exception) + { + if (_processingErrorHandler is null) + { + return; + } + + try + { + _processingErrorHandler(exception); + } + catch + { + // 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。 + } + } } \ No newline at end of file From d94d8deb2945b4c3da0e3894fd0d45f272afd70b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:59:51 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(logging):=20=E4=BF=AE=E5=A4=8D=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=97=A5=E5=BF=97=E8=BF=BD=E5=8A=A0=E5=99=A8=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=93=8D=E4=BD=9C=E5=8F=96=E6=B6=88=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加对 OperationCanceledException 的特殊处理,避免将其报告为后台处理错误 - 在 ReportProcessingError 方法中检查并过滤掉操作取消异常 - 添加单元测试验证当内部追加器抛出 OperationCanceledException 时不报告错误 - 创建 CancellationAppender 测试辅助类来模拟取消异常场景 - 确保取消相关的异常不会触发错误处理逻辑 --- .../Logging/AsyncLogAppenderTests.cs | 40 ++++++++++++++++++- .../Logging/Appenders/AsyncLogAppender.cs | 7 +++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs index cc22646..bd5576a 100644 --- a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs +++ b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs @@ -1,4 +1,3 @@ -using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging.Appenders; namespace GFramework.Core.Tests.Logging; @@ -197,6 +196,29 @@ public class AsyncLogAppenderTests Assert.That(asyncAppender.Flush(), Is.True); } + [Test] + public void Append_WhenInnerAppenderThrowsOperationCanceledException_ShouldNotReportError() + { + var reportedExceptions = new List(); + var innerAppender = new CancellationAppender(); + using var asyncAppender = new AsyncLogAppender( + innerAppender, + bufferSize: 1000, + processingErrorHandler: reportedExceptions.Add); + + Assert.DoesNotThrow(() => + { + for (int i = 0; i < 10; i++) + { + var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null); + asyncAppender.Append(entry); + } + }); + + Assert.That(asyncAppender.Flush(), Is.True); + Assert.That(reportedExceptions, Is.Empty); + } + // 辅助测试类 private class TestAppender : ILogAppender { @@ -257,4 +279,20 @@ public class AsyncLogAppenderTests { } } + + private class CancellationAppender : ILogAppender + { + public void Append(LogEntry entry) + { + throw new OperationCanceledException("Simulated cancellation"); + } + + public void Flush() + { + } + + public void Dispose() + { + } + } } \ No newline at end of file diff --git a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs index 9d52b9b..556c7d3 100644 --- a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs +++ b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs @@ -1,4 +1,3 @@ -using System.Threading.Channels; using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Appenders; @@ -212,10 +211,16 @@ public sealed class AsyncLogAppender : ILogAppender /// /// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。 + /// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。 /// /// 后台处理中捕获到的异常。 private void ReportProcessingError(Exception exception) { + if (exception is OperationCanceledException) + { + return; + } + if (_processingErrorHandler is null) { return; From cdc49c319a9619af5f9a382948f23f27f3e52466 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:04:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(logging):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=99=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Channel 实现异步日志处理机制 - 解耦调用线程与慢速日志目标 - 添加全局 Channels 命名空间引用 - 完善日志组件的异步处理能力 --- GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs | 1 + GFramework.Core/GlobalUsings.cs | 3 ++- GFramework.Core/Logging/Appenders/AsyncLogAppender.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs index bd5576a..ae1537c 100644 --- a/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs +++ b/GFramework.Core.Tests/Logging/AsyncLogAppenderTests.cs @@ -1,3 +1,4 @@ +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging.Appenders; namespace GFramework.Core.Tests.Logging; diff --git a/GFramework.Core/GlobalUsings.cs b/GFramework.Core/GlobalUsings.cs index 4d27181..8add267 100644 --- a/GFramework.Core/GlobalUsings.cs +++ b/GFramework.Core/GlobalUsings.cs @@ -15,4 +15,5 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using System.Threading.Channels; \ No newline at end of file diff --git a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs index 556c7d3..af70014 100644 --- a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs +++ b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs @@ -3,7 +3,7 @@ using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Appenders; /// -/// 异步日志输出器,使用 将调用线程与慢速日志目标解耦。 +/// 异步日志输出器,使用 将调用线程与慢速日志目标解耦。 /// /// ///