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] =?UTF-8?q?feat(logging):=20=E6=B7=BB=E5=8A=A0=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=9B=9E=E8=B0=83=E5=8A=9F?= =?UTF-8?q?=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