Route optional interfaces through OptionalInterfaceForwardingSink for restricted sinks#2234
Conversation
|
Thanks for checking this out. Since It's a little awkward, but this works in my copy: var sink = logEventSink;
if (levelSwitch != null)
{
if (restrictedToMinimumLevel != LevelAlias.Minimum)
SelfLog.WriteLine("Sink {0} was configured with both a level switch and minimum level '{1}'; the minimum level will be ignored and the switch level used", sink, restrictedToMinimumLevel);
sink = new RestrictedSink(logEventSink, levelSwitch);
if (!OptionalInterfaceForwardingSink.SupportsAll(sink) && OptionalInterfaceForwardingSink.SupportsAny(logEventSink))
{
sink = new OptionalInterfaceForwardingSink(sink, logEventSink);
}
}
else if (restrictedToMinimumLevel > LevelAlias.Minimum)
{
sink = new RestrictedSink(logEventSink, new(restrictedToMinimumLevel));
if (!OptionalInterfaceForwardingSink.SupportsAll(sink) && OptionalInterfaceForwardingSink.SupportsAny(logEventSink))
{
sink = new OptionalInterfaceForwardingSink(sink, logEventSink);
}
}In the long run, thie idea is that for any new added interfaces, an update to How's that look to you? |
When LoggerSinkConfiguration.Sink wraps a sink with a non-default RestrictedToMinimumLevel, the resulting RestrictedSink did not implement ISetLoggingFailureListener, so failure listeners installed by FailureListenerSink (used by WriteTo.FallbackChain and WriteTo.Fallible) never reached the inner sink. For async/batched sinks that surface failures only through the listener, this caused batches to be silently dropped via SelfLog instead of routed to the fallback chain. Mirror the forwarding pattern already used by OptionalInterfaceForwardingSink so SetFailureListener is propagated to the inner sink when supported.
6210f76 to
7db6514
Compare
…aceForwardingSink Strip RestrictedSink to ILogEventSink and route IDisposable, IAsyncDisposable, and ISetLoggingFailureListener through OptionalInterfaceForwardingSink so future optional interfaces only need updates in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
That looks great. I copied the code 1:1. At the moment |
|
@nblumhardt is it okay like this? |
…ound Serilog 4.4.0 (serilog/serilog#2234) forwards optional sink interfaces — including ISetLoggingFailureListener — through an OptionalInterfaceForwardingSink for restricted sinks. That closes the gap where setting RestrictedToMinimumLevel on a sink wrapped by WriteTo.FallbackChain(...) silently dropped failed batches instead of routing them to the fallback. With the minimum Serilog now at 4.4.0, the documentation workaround is obsolete: - README: replace the "do not set RestrictedToMinimumLevel" gotcha with a note that it composes correctly on 4.4.0+; add a migration bullet for the raised minimum; bump the 9.0.0 compatibility row to Serilog 4.4.x. - CHANGELOG: add the raised-minimum breaking-change bullet under 9.0.0. - FallbackChainOutageTests: convert to a [Theory] and add the RestrictedToMinimumLevel case, pinning the upstream fix — the fallback still receives post-outage events through the RestrictedSink. Also updates the remaining dev/runtime package versions to latest. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…log 4.4.0 (#333) * Replace in-sink failure sink with WriteTo.FallbackChain composition The in-sink failure sink and EmitEventFailureHandling enum are removed. Publish failures now always propagate so Serilog's BatchingSink listener observes them, which is what enables WriteTo.FallbackChain(...) and WriteTo.Fallible(...) from Serilog core to compose with the RabbitMQ sink. Diagnostics are written to SelfLog on every failure with no opt-in flag. Public surface removed: - EmitEventFailureHandling (enum) - RabbitMQSinkConfiguration.EmitEventFailure - WriteTo.RabbitMQ(..., failureSinkConfiguration) overloads - emitEventFailure / failureSinkConfiguration flat parameters Migration guidance and a code/JSON example for FallbackChain are added to README under "Migrating to 9.0.0", including a callout for the Serilog RestrictedSink listener-forwarding gap that silently breaks FallbackChain when RestrictedToMinimumLevel is set on the wrapped sink config. A new integration test (FallbackChainOutageTests) uses Testcontainers.RabbitMq to drive a real broker outage and assert the fallback file receives the post-outage events. A LoadTestSample is bundled for manual smoke-testing the wire-up under load. * Expose RetryTimeLimit, add IAsyncDisposable, simplify batched-sink registration - RabbitMQSinkConfiguration.RetryTimeLimit (default 10m, matching Serilog's BatchingOptions default) wires through to BatchingSink so callers can tune how long a failed batch is retried before it is handed to the registered ILoggingFailureListener (the next link in WriteTo.FallbackChain). Exposed on the public flat overload as retryTimeLimit; default substitution lives in RegisterSink alongside BatchPostingLimit / BufferingTimeLimit. - RabbitMQSink now implements IAsyncDisposable on net8.0 / net10.0 (gated by FEATURE_ASYNCDISPOSABLE, mirroring Serilog core). DisposeAsync awaits the client directly without the AsyncHelpers.RunSync bridge; both Dispose paths share _disposedValue and are mutually idempotent. netstandard2.0 keeps IDisposable only. - RegisterSink now calls Sink(IBatchedLogEventSink, BatchingOptions, LogEventLevel) directly instead of materialising the BatchingSink via CreateSink and wrapping it again. Same RestrictedSink -> BatchingSink -> RabbitMQSink chain, fewer steps; private-only simplification. CHANGELOG and README updated. Approved API surface regenerated. * Bump packages incl. Serilog 4.4.0; retire fixed RestrictedSink workaround Serilog 4.4.0 (serilog/serilog#2234) forwards optional sink interfaces — including ISetLoggingFailureListener — through an OptionalInterfaceForwardingSink for restricted sinks. That closes the gap where setting RestrictedToMinimumLevel on a sink wrapped by WriteTo.FallbackChain(...) silently dropped failed batches instead of routing them to the fallback. With the minimum Serilog now at 4.4.0, the documentation workaround is obsolete: - README: replace the "do not set RestrictedToMinimumLevel" gotcha with a note that it composes correctly on 4.4.0+; add a migration bullet for the raised minimum; bump the 9.0.0 compatibility row to Serilog 4.4.x. - CHANGELOG: add the raised-minimum breaking-change bullet under 9.0.0. - FallbackChainOutageTests: convert to a [Theory] and add the RestrictedToMinimumLevel case, pinning the upstream fix — the fallback still receives post-outage events through the RestrictedSink. Also updates the remaining dev/runtime package versions to latest. * Fix RetryTimeLimit=Zero being clobbered to the 10-minute default The documented "TimeSpan.Zero disables retries → immediate fallback" contract was unreachable. RegisterSink used `RetryTimeLimit == default` as its "unset" sentinel, and default(TimeSpan) is TimeSpan.Zero — so every Zero (whether omitted or set explicitly to disable retries) was overwritten with the 10-minute default. Both public paths were affected, and two tests locked in the wrong behaviour. Fix distinguishes "unset" from "explicit Zero": - The flat overload's `retryTimeLimit` parameter is now `TimeSpan?` (default null); null resolves to the library default before the config is built, any value — including TimeSpan.Zero — is honored. - Removed the RetryTimeLimit default-substitution block from RegisterSink. The property already initializes to DEFAULT_RETRY_TIME_LIMIT at construction, so the delegate and direct-config paths keep the 10-minute default while a caller-set Zero now survives to BatchingOptions.RetryTimeLimit. Tests: dropped the RetryTimeLimit=Zero assertion from the batching-defaults test (Zero is no longer "unset"), added a regression test asserting Zero survives, and refreshed the stale RegisterSink comments. Public API snapshot regenerated (retryTimeLimit is now TimeSpan?). CHANGELOG and README updated. * Clear CodeQL alerts in test/sample code - cs/dispose-not-called-on-throw (warning, the PR-gating alert): the two cross-path dispose idempotency tests called DisposeAsync/Dispose in sequence without guaranteeing the second ran if the first threw. Wrapped each in try/finally so the sink is always disposed; assertions unchanged. - cs/path-combine (note): wrapped the fallback file name in Path.GetFileName so the second Path.Combine segment is provably relative. The two cs/catch-of-all-exceptions notes in LoadTestSample are intentional (a load-test harness must survive a mid-run broker restart) and are left as-is. * Potential fix for pull request finding 'CodeQL / Call to 'System.IO.Path.Combine' may silently drop its earlier arguments' * Potential fix for pull request finding 'CodeQL / Missed 'using' opportunity' * Potential fix for pull request finding 'CodeQL / Generic catch clause' * Potential fix for pull request finding 'CodeQL / Generic catch clause' * Remove unused System.IO using in LoadTestSample The CodeQL catch-narrowing autofixes left `using System.IO;` unused, which IDE0005 fails under TreatWarningsAsErrors (the buildcheck/format CI step). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Closes #2231.
Summary
Serilog.Core.Sinks.RestrictedSink— the wrapper applied byLoggerSinkConfiguration.Sink(sink, restrictedToMinimumLevel)wheneverrestrictedToMinimumLevelis anything other thanLevelAlias.Minimum— hid the inner sink's optional interfaces (IDisposable,IAsyncDisposable,ISetLoggingFailureListener). The most visible consequence: the failure listener installed byFailureListenerSink(used internally byWriteTo.FallbackChain(...)andWriteTo.Fallible(...), introduced in #2108) never reached the inner sink.For synchronous sinks the gap is masked:
FailureListenerSink.Emit(...)catches exceptions and notifies the listener directly. For async/batched sinks that surface failures only throughILoggingFailureListener(notably anything built onBatchingSink), the listener stayed unset and failed batches were silently dropped viaSelfLoginstead of being routed to the next stage of the fallback chain.Per maintainer feedback, the fix routes optional interfaces through the existing
OptionalInterfaceForwardingSink(the canonical mechanism, already used in the chained-sink path) rather than re-implementing forwarding inside each wrapper. Future optional interfaces will only need updates in one place.Changes
RestrictedSinkis now a plainILogEventSink— itsDispose/DisposeAsync/SetFailureListenershims are removed.LoggerSinkConfiguration.Sink(...)wraps the resultingRestrictedSinkinOptionalInterfaceForwardingSinkwhenever the inner sink supports any optional interface, mirroring the existing chained-sink wiring.RestrictedToMinimumLevelAllowsFailureListenerPropagation— pins the wiring throughFallbackChain+restrictedToMinimumLevel.DefaultLevelAllowsFailureListenerPropagation— pins the un-wrapped baseline.ListenerTrackingSinkhelper for the integration tests.Test plan
dotnet test test/Serilog.Tests --filter "FullyQualifiedName~FailureListenerTests"— 4/4 passing onnet8.0,net9.0,net10.0Serilog.Testssuite — 815/815 passingSerilog.ApprovalTestspasses (no public-API drift;RestrictedSinkis internal)windows-latestcovers thenet48/net462legsDiscovered via
Wiring
Serilog.Sinks.RabbitMQagainst the newWriteTo.FallbackChain/WriteTo.Fallibleextensions from #2108. SettingRestrictedToMinimumLevel = Informationon the primary sink silently broke the whole fallback story, with no compile-time or runtime indication.