mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
Merge pull request #127 from GeWuYou/feat/async-log-appender-error-handler
feat(logging): 添加异步日志输出器的错误处理回调功能
This commit is contained in:
commit
63b1d71a0e
@ -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,56 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Append_WhenInnerAppenderThrowsOperationCanceledException_ShouldNotReportError()
|
||||||
|
{
|
||||||
|
var reportedExceptions = new List<Exception>();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助测试类
|
// 辅助测试类
|
||||||
@ -228,4 +280,20 @@ public class AsyncLogAppenderTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CancellationAppender : ILogAppender
|
||||||
|
{
|
||||||
|
public void Append(LogEntry entry)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Simulated cancellation");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Flush()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -16,3 +16,4 @@ global using System.Collections.Generic;
|
|||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
|
global using System.Threading.Channels;
|
||||||
@ -1,17 +1,26 @@
|
|||||||
using System.Threading.Channels;
|
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
namespace GFramework.Core.Logging.Appenders;
|
namespace GFramework.Core.Logging.Appenders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
|
/// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。
|
||||||
/// </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 +30,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 +155,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 +170,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 +193,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 +202,37 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
|||||||
{
|
{
|
||||||
_innerAppender.Flush();
|
_innerAppender.Flush();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ReportProcessingError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
|
||||||
|
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">后台处理中捕获到的异常。</param>
|
||||||
|
private void ReportProcessingError(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception is OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_processingErrorHandler is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_processingErrorHandler(exception);
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// 忽略刷新错误
|
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user