mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(logging): 添加异步日志输出器的错误处理回调功能
- 在 AsyncLogAppender 构造函数中添加 processingErrorHandler 参数用于处理后台异常 - 实现 ReportProcessingError 方法安全上报后台处理异常而不影响处理循环 - 更新文档注释说明异常处理机制和错误回调用途 - 修改测试用例验证异常处理回调功能的正确性 - 确保错误观察者异常不会终止日志处理线程 - 移除直接写入控制台错误输出的逻辑改为统一回调处理
This commit is contained in:
parent
003fe42ad8
commit
49609d3821
@ -1,6 +1,5 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging.Appenders;
|
using GFramework.Core.Logging.Appenders;
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Logging;
|
namespace GFramework.Core.Tests.Logging;
|
||||||
|
|
||||||
@ -152,8 +151,12 @@ public class AsyncLogAppenderTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
|
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
|
||||||
{
|
{
|
||||||
|
var reportedExceptions = new List<Exception>();
|
||||||
var innerAppender = new ThrowingAppender();
|
var innerAppender = new ThrowingAppender();
|
||||||
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
|
using var asyncAppender = new AsyncLogAppender(
|
||||||
|
innerAppender,
|
||||||
|
bufferSize: 1000,
|
||||||
|
processingErrorHandler: reportedExceptions.Add);
|
||||||
|
|
||||||
// 即使内部 Appender 抛出异常,也不应该影响调用线程
|
// 即使内部 Appender 抛出异常,也不应该影响调用线程
|
||||||
Assert.DoesNotThrow(() =>
|
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<InvalidOperationException>());
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助测试类
|
// 辅助测试类
|
||||||
|
|||||||
@ -4,14 +4,24 @@ using GFramework.Core.Abstractions.Logging;
|
|||||||
namespace GFramework.Core.Logging.Appenders;
|
namespace GFramework.Core.Logging.Appenders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
|
/// 异步日志输出器,使用 <see cref="Channel{T}" /> 将调用线程与慢速日志目标解耦。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// 该输出器在后台线程中顺序消费日志条目,因此调用方不会因为文件 IO 或其他慢速输出目标而阻塞。
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// 内部输出器抛出的异常不会重新抛回调用线程;如需观察后台处理失败,请在构造函数中提供
|
||||||
|
/// <c>processingErrorHandler</c> 回调。
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class AsyncLogAppender : ILogAppender
|
||||||
{
|
{
|
||||||
private readonly Channel<LogEntry> _channel;
|
private readonly Channel<LogEntry> _channel;
|
||||||
private readonly CancellationTokenSource _cts;
|
private readonly CancellationTokenSource _cts;
|
||||||
private readonly SemaphoreSlim _flushSemaphore = new(0, 1);
|
private readonly SemaphoreSlim _flushSemaphore = new(0, 1);
|
||||||
private readonly ILogAppender _innerAppender;
|
private readonly ILogAppender _innerAppender;
|
||||||
|
private readonly Action<Exception>? _processingErrorHandler;
|
||||||
private readonly Task _processingTask;
|
private readonly Task _processingTask;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private volatile bool _flushRequested;
|
private volatile bool _flushRequested;
|
||||||
@ -21,9 +31,17 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="innerAppender">内部日志输出器</param>
|
/// <param name="innerAppender">内部日志输出器</param>
|
||||||
/// <param name="bufferSize">缓冲区大小(默认 10000)</param>
|
/// <param name="bufferSize">缓冲区大小(默认 10000)</param>
|
||||||
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
|
/// <param name="processingErrorHandler">
|
||||||
|
/// 后台处理日志时的错误回调。
|
||||||
|
/// 默认值为 <see langword="null" />,表示吞掉内部异常以避免污染宿主标准错误输出。
|
||||||
|
/// </param>
|
||||||
|
public AsyncLogAppender(
|
||||||
|
ILogAppender innerAppender,
|
||||||
|
int bufferSize = 10000,
|
||||||
|
Action<Exception>? processingErrorHandler = null)
|
||||||
{
|
{
|
||||||
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
|
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
|
||||||
|
_processingErrorHandler = processingErrorHandler;
|
||||||
|
|
||||||
if (bufferSize <= 0)
|
if (bufferSize <= 0)
|
||||||
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
|
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
|
||||||
@ -138,7 +156,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 后台处理日志的异步方法
|
/// 后台处理日志的异步方法。
|
||||||
|
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
|
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@ -152,8 +171,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 记录内部错误到控制台(避免递归)
|
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
|
||||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
|
ReportProcessingError(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有刷新请求且通道已空
|
// 检查是否有刷新请求且通道已空
|
||||||
@ -175,7 +194,7 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
|
ReportProcessingError(ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -184,10 +203,31 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
{
|
{
|
||||||
_innerAppender.Flush();
|
_innerAppender.Flush();
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 忽略刷新错误
|
ReportProcessingError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">后台处理中捕获到的异常。</param>
|
||||||
|
private void ReportProcessingError(Exception exception)
|
||||||
|
{
|
||||||
|
if (_processingErrorHandler is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_processingErrorHandler(exception);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user