diff --git a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj
index 1393c9d..7a40f61 100644
--- a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj
+++ b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj
@@ -25,7 +25,7 @@
-
+
diff --git a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj
index 5aad612..9c46043 100644
--- a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj
+++ b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj
@@ -35,8 +35,8 @@
-
-
+
+
diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj
index f2d0491..43a9261 100644
--- a/EonaCat.LogStack/EonaCat.LogStack.csproj
+++ b/EonaCat.LogStack/EonaCat.LogStack.csproj
@@ -66,18 +66,18 @@ It features a rich fluent API for routing log events to dozens of destinations f
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
-
+
+
+
-
+
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs
index 92837c0..9b4ef45 100644
--- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs
@@ -15,24 +15,65 @@ namespace EonaCat.LogStack.Flows
{
private readonly IFlow _primary;
private readonly IFlow _secondary;
+ private readonly TimeSpan _recoveryCheckInterval;
+ private readonly int _failureThreshold;
- public FailoverFlow(IFlow primary, IFlow secondary)
+ private IFlow _activeFlow;
+ private long _consecutiveFailures;
+ private DateTime _lastRecoveryCheck;
+ private CancellationTokenSource _recoveryCancellation;
+ private Task _recoveryTask;
+ private readonly object _syncLock = new object();
+
+ public FailoverFlow(
+ IFlow primary,
+ IFlow secondary,
+ TimeSpan? recoveryCheckInterval = null,
+ int failureThreshold = 5)
: base($"Failover({primary.Name})", primary.MinimumLevel)
{
_primary = primary ?? throw new ArgumentNullException(nameof(primary));
_secondary = secondary ?? throw new ArgumentNullException(nameof(secondary));
+ _recoveryCheckInterval = recoveryCheckInterval ?? TimeSpan.FromSeconds(10);
+ _failureThreshold = failureThreshold;
+ _activeFlow = _primary;
+ _consecutiveFailures = 0;
+ _lastRecoveryCheck = DateTime.UtcNow;
+ _recoveryCancellation = new CancellationTokenSource();
+ _recoveryTask = StartRecoveryDetectionAsync();
}
public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
- var result = await _primary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
+ var result = await _activeFlow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Success)
{
+ // Reset failure counter on success
+ Interlocked.Exchange(ref _consecutiveFailures, 0);
return result;
}
- return await _secondary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
+ // Only failover to secondary if using primary
+ if (_activeFlow == _primary)
+ {
+ var failures = Interlocked.Increment(ref _consecutiveFailures);
+
+ if (failures >= _failureThreshold)
+ {
+ lock (_syncLock)
+ {
+ // Double-check inside lock
+ if (_activeFlow == _primary && Interlocked.Read(ref _consecutiveFailures) >= _failureThreshold)
+ {
+ _activeFlow = _secondary;
+ Interlocked.Exchange(ref _consecutiveFailures, 0);
+ }
+ }
+ }
+ }
+
+ return result;
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
@@ -43,8 +84,80 @@ namespace EonaCat.LogStack.Flows
public override async ValueTask DisposeAsync()
{
+ _recoveryCancellation?.Cancel();
+
+ if (_recoveryTask != null)
+ {
+ try
+ {
+ await _recoveryTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when cancelling the recovery task
+ }
+ }
+
+ _recoveryCancellation?.Dispose();
await _primary.DisposeAsync();
await _secondary.DisposeAsync();
}
+
+ private async Task StartRecoveryDetectionAsync()
+ {
+ try
+ {
+ while (!_recoveryCancellation.Token.IsCancellationRequested)
+ {
+ await Task.Delay(_recoveryCheckInterval, _recoveryCancellation.Token).ConfigureAwait(false);
+
+ // Only check recovery if we've switched to secondary
+ if (_activeFlow == _secondary)
+ {
+ await CheckPrimaryRecoveryAsync().ConfigureAwait(false);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when shutting down
+ }
+ }
+
+ private async Task CheckPrimaryRecoveryAsync()
+ {
+ try
+ {
+ // Try to write a minimal test log to check if primary is healthy
+ var testEvent = new LogEvent
+ {
+ Level = LogLevel.Trace,
+ Message = new ReadOnlyMemory("recovery_check".ToCharArray()),
+ Timestamp = DateTime.UtcNow.Ticks,
+ Category = "RecoveryDetection",
+ ThreadId = Thread.CurrentThread.ManagedThreadId
+ };
+
+ var result = await _primary.BlastAsync(testEvent, _recoveryCancellation.Token).ConfigureAwait(false);
+
+ if (result == WriteResult.Success)
+ {
+ lock (_syncLock)
+ {
+ if (_activeFlow == _secondary)
+ {
+ _activeFlow = _primary;
+ Interlocked.Exchange(ref _consecutiveFailures, 0);
+ _lastRecoveryCheck = DateTime.UtcNow;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Silently catch exceptions during recovery check
+ // The primary is still unhealthy
+ }
+ }
}
}
diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs
index d0d6d4f..51b3c4a 100644
--- a/EonaCat.LogStack/LogBuilder.cs
+++ b/EonaCat.LogStack/LogBuilder.cs
@@ -433,7 +433,7 @@ public sealed class LogBuilder
///
/// Adds a Failover flow that switches to a secondary flow if the primary fails.
///
- public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow)
+ public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5)
{
if (primaryFlow == null)
{
@@ -445,7 +445,7 @@ public sealed class LogBuilder
throw new ArgumentNullException(nameof(secondaryFlow));
}
- _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow));
+ _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold));
return this;
}
diff --git a/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj
index 95cedb5..68e0551 100644
--- a/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj
+++ b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj
@@ -7,11 +7,11 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+