diff --git a/Directory.Version.props b/Directory.Version.props
index e0c7503..84b79be 100644
--- a/Directory.Version.props
+++ b/Directory.Version.props
@@ -1,6 +1,6 @@
- 10.0.0
+ 10.0.1
diff --git a/README.md b/README.md
index 68cc3ee..681dd39 100644
--- a/README.md
+++ b/README.md
@@ -313,6 +313,8 @@ Some Serilog packages require a reference to a logger configuration object. The
},
```
+The same nested pattern applies to wrapping sinks such as `Conditional` (from _[Serilog.Expressions](https://github.com/serilog/serilog-expressions)_), `FallbackChain`, and `Fallible` — see the [sample `appsettings.json`](sample/Sample/appsettings.json) for working examples.
+
### Destructuring
Destructuring means extracting pieces of information from an object and create properties with values; Serilog offers the `@` [structure-capturing operator](https://github.com/serilog/serilog/wiki/Structured-Data#preserving-object-structure). In case there is a need to customize the way log events are serialized (e.g., hide property values or replace them with something else), one can define several destructuring policies, like this:
diff --git a/sample/Sample/AlwaysFailingSink.cs b/sample/Sample/AlwaysFailingSink.cs
new file mode 100644
index 0000000..5ee7bdb
--- /dev/null
+++ b/sample/Sample/AlwaysFailingSink.cs
@@ -0,0 +1,13 @@
+using Serilog.Core;
+using Serilog.Events;
+
+namespace Sample;
+
+public class AlwaysFailingSink : ILogEventSink
+{
+ public void Emit(LogEvent logEvent)
+ {
+ throw new InvalidOperationException(
+ "AlwaysFailingSink always throws so the sample can demonstrate fallback/failure-listener behavior.");
+ }
+}
diff --git a/sample/Sample/SampleFailureListener.cs b/sample/Sample/SampleFailureListener.cs
new file mode 100644
index 0000000..0f3df8c
--- /dev/null
+++ b/sample/Sample/SampleFailureListener.cs
@@ -0,0 +1,22 @@
+using Serilog.Core;
+using Serilog.Debugging;
+using Serilog.Events;
+
+namespace Sample;
+
+public class SampleFailureListener : ILoggingFailureListener
+{
+ public void OnLoggingFailed(
+ object sender,
+ LoggingFailureKind kind,
+ string message,
+ IReadOnlyCollection? events,
+ Exception? exception)
+ {
+ var exceptionDetail = exception is null
+ ? string.Empty
+ : $" — {exception.GetType().Name}: {exception.Message}";
+ SelfLog.WriteLine(
+ $"[SampleFailureListener] {kind} failure from {sender.GetType().Name}: {message} ({events?.Count ?? 0} events){exceptionDetail}");
+ }
+}
diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json
index 98aa712..e0ab488 100644
--- a/sample/Sample/appsettings.json
+++ b/sample/Sample/appsettings.json
@@ -4,7 +4,7 @@
"LevelSwitches": { "controlSwitch": "Verbose" },
"FilterSwitches": { "$filterSwitch": "Application = 'Sample'" },
"MinimumLevel": {
- "Default": "Debug",
+ "ControlledBy": "$controlSwitch",
"Override": {
"Microsoft": "Warning",
"MyApp.Something.Tricky": "Verbose"
@@ -37,7 +37,10 @@
"Name": "File",
"Args": {
"path": "%TEMP%/Logs/serilog-configuration-sample.txt",
- "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}/{ThreadName}) {Message}{NewLine}{Exception}"
+ "formatter": {
+ "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions",
+ "template": "{@t:o} [{@l:u3}] ({Application}/{MachineName}/{ThreadId}/{ThreadName}) {@m}\n{@x}"
+ }
}
}
]
@@ -46,7 +49,7 @@
"WriteTo:ConditionalSink": {
"Name": "Conditional",
"Args": {
- "expression": "@Level in ['Error', 'Fatal']",
+ "expression": "@l in ['Error', 'Fatal']",
"configureSink": [
{
"Name": "File",
@@ -63,6 +66,43 @@
]
}
},
+ "WriteTo:FallbackChain": {
+ "Name": "FallbackChain",
+ "Args": {
+ "configureSink": [
+ {
+ "Name": "Sink",
+ "Args": {
+ "sink": { "type": "Sample.AlwaysFailingSink, Sample" }
+ }
+ }
+ ],
+ "configureFallback": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[fallback {Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
+ }
+ }
+ ]
+ }
+ },
+ "WriteTo:Fallible": {
+ "Name": "Fallible",
+ "Args": {
+ "configureSink": [
+ {
+ "Name": "Sink",
+ "Args": {
+ "sink": { "type": "Sample.AlwaysFailingSink, Sample" }
+ }
+ }
+ ],
+ "failureListener": {
+ "type": "Sample.SampleFailureListener, Sample"
+ }
+ }
+ },
"Enrich": [
"FromLogContext",
"WithThreadId",
@@ -79,6 +119,13 @@
"expression": "Application = 'Sample'",
"configureEnricher": [ "WithMachineName" ]
}
+ },
+ {
+ "Name": "WithComputed",
+ "Args": {
+ "name": "ShortContext",
+ "expression": "coalesce(SourceContext, '')"
+ }
}
],
"Properties": {
@@ -109,6 +156,12 @@
"switch": "$filterSwitch"
}
},
+ {
+ "Name": "ByExcluding",
+ "Args": {
+ "expression": "Application = 'OtherApp'"
+ }
+ },
{
"Name": "With",
"Args": {
@@ -118,6 +171,14 @@
}
}
}
+ ],
+ "AuditTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[AUDIT {Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
+ }
+ }
]
}
}
diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs
index 3b86170..19d269d 100644
--- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs
+++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs
@@ -403,10 +403,12 @@ static bool HasImplicitValueWhenNotSpecified(ParameterInfo paramInfo)
{
return paramInfo.HasDefaultValue
// parameters of type IConfiguration are implicitly populated with provided Configuration
- || paramInfo.ParameterType == typeof(IConfiguration);
+ || paramInfo.ParameterType == typeof(IConfiguration)
+ || paramInfo.IsDefined(typeof(ParamArrayAttribute), false)
+ || paramInfo.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.ParamCollectionAttribute");
}
- object? GetImplicitValueForNotSpecifiedKey(ParameterInfo parameter, MethodInfo methodToInvoke)
+ internal object? GetImplicitValueForNotSpecifiedKey(ParameterInfo parameter, MethodInfo methodToInvoke)
{
if (!HasImplicitValueWhenNotSpecified(parameter))
{
@@ -429,7 +431,12 @@ static bool HasImplicitValueWhenNotSpecified(ParameterInfo paramInfo)
$"This is not supported when only a `IConfigSection` has been provided. (method '{methodToInvoke}')");
}
- return parameter.DefaultValue;
+ if (parameter.IsDefined(typeof(ParamArrayAttribute), false) && parameter.ParameterType.GetElementType() is { } elementType)
+ {
+ return Array.CreateInstance(elementType, 0);
+ }
+
+ return parameter.HasDefaultValue ? parameter.DefaultValue : null;
}
internal static MethodInfo? SelectConfigurationMethod(IReadOnlyCollection candidateMethods, string name, IReadOnlyCollection suppliedArgumentNames)
@@ -571,8 +578,21 @@ internal static bool IsValidSwitchName(string input)
return Regex.IsMatch(input, LevelSwitchNameRegex);
}
- static LogEventLevel ParseLogEventLevel(string value)
- => Enum.TryParse(value, ignoreCase: true, out LogEventLevel parsedLevel)
- ? parsedLevel
- : throw new InvalidOperationException($"The value {value} is not a valid Serilog level.");
+ internal static LogEventLevel ParseLogEventLevel(string value)
+ {
+ // Try parsing as LevelAlias first (handles "Off", "Minimum", "Maximum")
+ if (string.Equals(value, "Off", StringComparison.OrdinalIgnoreCase))
+ return LevelAlias.Off;
+ if (string.Equals(value, "Minimum", StringComparison.OrdinalIgnoreCase))
+ return LevelAlias.Minimum;
+ if (string.Equals(value, "Maximum", StringComparison.OrdinalIgnoreCase))
+ return LevelAlias.Maximum;
+
+ // Try parsing as LogEventLevel enum
+ if (Enum.TryParse(value, ignoreCase: true, out LogEventLevel parsedLevel))
+ return parsedLevel;
+
+ throw new InvalidOperationException($"The value {value} is not a valid Serilog level.");
+ }
+
}
diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs
index 894273c..c6aeda1 100644
--- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs
+++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs
@@ -154,6 +154,11 @@ public StringArgumentValue(string providedValue)
}
}
+ if (toType.IsArray && string.IsNullOrWhiteSpace(argumentValue))
+ {
+ return Array.CreateInstance(toType.GetElementType()!, 0);
+ }
+
return Convert.ChangeType(argumentValue, toType, resolutionContext.ReaderOptions.FormatProvider);
}
diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs
index b758d7c..92d8a29 100644
--- a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs
+++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs
@@ -55,6 +55,19 @@ static LoggerConfiguration Logger(
LoggingLevelSwitch? levelSwitch = null)
=> loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch);
+ static LoggerConfiguration FallbackChain(
+ LoggerSinkConfiguration loggerSinkConfiguration,
+ Action configureSink,
+ Action configureFallback,
+ Action[]? configureSubsequentFallbacks = null)
+ => loggerSinkConfiguration.FallbackChain(configureSink, configureFallback, configureSubsequentFallbacks ?? []);
+
+ static LoggerConfiguration Fallible(
+ LoggerSinkConfiguration loggerSinkConfiguration,
+ Action configureSink,
+ ILoggingFailureListener failureListener)
+ => loggerSinkConfiguration.Fallible(configureSink, failureListener);
+
// .AuditTo...
// ========
static LoggerConfiguration Sink(
diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs
index da01316..7f47c14 100644
--- a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs
+++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs
@@ -294,4 +294,102 @@ public void NoConfigurationRootUsedStillValid()
AssertLogEventLevels(loggerConfig, LogEventLevel.Error);
}
+
+ [Theory]
+ // Standard LogEventLevel enum values
+ [InlineData("Verbose", LogEventLevel.Verbose)]
+ [InlineData("Debug", LogEventLevel.Debug)]
+ [InlineData("Information", LogEventLevel.Information)]
+ [InlineData("Warning", LogEventLevel.Warning)]
+ [InlineData("Error", LogEventLevel.Error)]
+ [InlineData("Fatal", LogEventLevel.Fatal)]
+ // Case insensitivity
+ [InlineData("verbose", LogEventLevel.Verbose)]
+ [InlineData("INFORMATION", LogEventLevel.Information)]
+ [InlineData("warning", LogEventLevel.Warning)]
+ // LevelAlias values
+ [InlineData("Off", LevelAlias.Off)]
+ [InlineData("off", LevelAlias.Off)]
+ [InlineData("OFF", LevelAlias.Off)]
+ [InlineData("Minimum", LevelAlias.Minimum)]
+ [InlineData("minimum", LevelAlias.Minimum)]
+ [InlineData("Maximum", LevelAlias.Maximum)]
+ [InlineData("maximum", LevelAlias.Maximum)]
+ public void ParseLogEventLevelHandlesAllLevelValues(string value, LogEventLevel expected)
+ {
+ var result = ConfigurationReader.ParseLogEventLevel(value);
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [InlineData("InvalidLevel")]
+ [InlineData("")]
+ [InlineData("None")]
+ public void ParseLogEventLevelThrowsForInvalidValues(string value)
+ {
+ Assert.Throws(() => ConfigurationReader.ParseLogEventLevel(value));
+ }
+
+ [Fact]
+ public void ParamsStringArrayParameter_WithNoArgsSupplied_IsMatchedAsOptional()
+ {
+ var candidateMethods = typeof(DummyLoggerConfigurationExtensions)
+ .GetTypeInfo()
+ .DeclaredMethods
+ .ToList();
+
+ var selected = ConfigurationReader.SelectConfigurationMethod(
+ candidateMethods, "DummyParamsArray", Array.Empty());
+
+ Assert.NotNull(selected);
+ }
+
+ [Fact]
+ public void ParamsStringArrayParameter_ImplicitValueIsEmptyArray()
+ {
+ var reader = new ConfigurationReader(
+ JsonStringConfigSource.LoadSection("{}", "Serilog"),
+ AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies),
+ new ConfigurationReaderOptions());
+
+ var method = typeof(DummyLoggerConfigurationExtensions).GetMethod("DummyParamsArray")!;
+ var param = method.GetParameters().Last(); // params string[] values
+
+ var result = reader.GetImplicitValueForNotSpecifiedKey(param, method);
+
+ var array = Assert.IsType(result);
+ Assert.Empty(array);
+ }
+
+ [Fact]
+ public void ParamsEnumerableParameter_GracefullyReturnsDefaultValue()
+ {
+ var reader = new ConfigurationReader(
+ JsonStringConfigSource.LoadSection("{}", "Serilog"),
+ AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies),
+ new ConfigurationReaderOptions());
+
+ var method = typeof(DummyLoggerConfigurationExtensions).GetMethod("DummyParamsEnumerable")!;
+ var param = method.GetParameters().Last(); // params IEnumerable
+
+ var result = reader.GetImplicitValueForNotSpecifiedKey(param, method);
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void ParamsSpanParameter_GracefullyReturnsDefaultValue()
+ {
+ var reader = new ConfigurationReader(
+ JsonStringConfigSource.LoadSection("{}", "Serilog"),
+ AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies),
+ new ConfigurationReaderOptions());
+
+ var method = typeof(DummyLoggerConfigurationExtensions).GetMethod("DummyParamsSpan")!;
+ var param = method.GetParameters().Last(); // params ReadOnlySpan
+
+ var result = reader.GetImplicitValueForNotSpecifiedKey(param, method);
+
+ Assert.Null(result);
+ }
}
diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs
index 5aeec1f..6ced053 100644
--- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs
+++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs
@@ -1575,4 +1575,130 @@ public void TestLogFilterSwitchesCallback()
var switch2 = Assert.Contains("$switch2", switches);
Assert.Equal("Prop = 2", switch2.Expression);
}
+
+ [Fact]
+ public void WriteToFallbackChainRoutesFailuresToFallbackSink()
+ {
+ // language=json
+ var json = """
+ {
+ "Serilog": {
+ "Using": ["TestDummies"],
+ "WriteTo": [{
+ "Name": "FallbackChain",
+ "Args": {
+ "configureSink": [{
+ "Name": "Sink",
+ "Args": {
+ "sink": {
+ "type": "TestDummies.DummyFailingSink, TestDummies"
+ }
+ }
+ }],
+ "configureFallback": [{
+ "Name": "DummyRollingFile",
+ "Args": { "pathFormat": "C:\\" }
+ }]
+ }
+ }]
+ }
+ }
+ """;
+
+ var log = ConfigFromJson(json)
+ .CreateLogger();
+
+ DummyFailingSink.Reset();
+ DummyRollingFileSink.Reset();
+
+ log.Write(Some.InformationEvent());
+
+ Assert.Equal(1, DummyFailingSink.EmitAttempts);
+ Assert.Single(DummyRollingFileSink.Emitted);
+ }
+
+ [Fact]
+ public void WriteToFallbackChainSupportsSubsequentFallbacks()
+ {
+ // language=json
+ var json = """
+ {
+ "Serilog": {
+ "Using": ["TestDummies"],
+ "WriteTo": [{
+ "Name": "FallbackChain",
+ "Args": {
+ "configureSink": [{
+ "Name": "Sink",
+ "Args": {
+ "sink": { "type": "TestDummies.DummyFailingSink, TestDummies" }
+ }
+ }],
+ "configureFallback": [{
+ "Name": "Sink",
+ "Args": {
+ "sink": { "type": "TestDummies.DummyFailingSink, TestDummies" }
+ }
+ }],
+ "configureSubsequentFallbacks": [
+ [{
+ "Name": "DummyRollingFile",
+ "Args": { "pathFormat": "C:\\" }
+ }]
+ ]
+ }
+ }]
+ }
+ }
+ """;
+
+ var log = ConfigFromJson(json)
+ .CreateLogger();
+
+ DummyFailingSink.Reset();
+ DummyRollingFileSink.Reset();
+
+ log.Write(Some.InformationEvent());
+
+ Assert.Equal(2, DummyFailingSink.EmitAttempts);
+ Assert.Single(DummyRollingFileSink.Emitted);
+ }
+
+ [Fact]
+ public void WriteToFallibleReportsFailuresToListener()
+ {
+ // language=json
+ var json = """
+ {
+ "Serilog": {
+ "Using": ["TestDummies"],
+ "WriteTo": [{
+ "Name": "Fallible",
+ "Args": {
+ "configureSink": [{
+ "Name": "Sink",
+ "Args": {
+ "sink": { "type": "TestDummies.DummyFailingSink, TestDummies" }
+ }
+ }],
+ "failureListener": {
+ "type": "TestDummies.DummyFailureListener, TestDummies"
+ }
+ }
+ }]
+ }
+ }
+ """;
+
+ var log = ConfigFromJson(json)
+ .CreateLogger();
+
+ DummyFailingSink.Reset();
+ DummyFailureListener.Reset();
+
+ log.Write(Some.InformationEvent());
+
+ Assert.Equal(1, DummyFailingSink.EmitAttempts);
+ Assert.Equal(1, DummyFailureListener.FailureCount);
+ }
}
diff --git a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs
index c8a00d6..6dcd8d2 100644
--- a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs
+++ b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs
@@ -1,6 +1,7 @@
using Serilog.Configuration;
using Serilog.Events;
using Serilog.Formatting;
+using TestDummies;
namespace Serilog.Settings.Configuration.Tests;
@@ -24,4 +25,25 @@ static class DummyLoggerConfigurationExtensions
{
return null;
}
+
+ public static LoggerConfiguration DummyParamsArray(
+ this LoggerSinkConfiguration loggerSinkConfiguration,
+ params string[] values)
+ {
+ return loggerSinkConfiguration.Sink(new DummyParamsSink(values));
+ }
+
+ public static LoggerConfiguration DummyParamsEnumerable(
+ this LoggerSinkConfiguration loggerSinkConfiguration,
+ params IEnumerable values)
+ {
+ return loggerSinkConfiguration.Sink(new DummyParamsSink(values.ToArray()));
+ }
+
+ public static LoggerConfiguration DummyParamsSpan(
+ this LoggerSinkConfiguration loggerSinkConfiguration,
+ params ReadOnlySpan values)
+ {
+ return loggerSinkConfiguration.Sink(new DummyParamsSink(values.ToArray()));
+ }
}
diff --git a/test/Serilog.Settings.Configuration.Tests/DummyParamsSink.cs b/test/Serilog.Settings.Configuration.Tests/DummyParamsSink.cs
new file mode 100644
index 0000000..267fd13
--- /dev/null
+++ b/test/Serilog.Settings.Configuration.Tests/DummyParamsSink.cs
@@ -0,0 +1,21 @@
+using Serilog.Core;
+using Serilog.Events;
+
+namespace TestDummies;
+
+public class DummyParamsSink : ILogEventSink
+{
+ public static string[]? LastValues { get; private set; }
+
+ public DummyParamsSink(params string[] values)
+ {
+ LastValues = values;
+ }
+
+ public void Emit(LogEvent logEvent) { }
+
+ public static void Reset()
+ {
+ LastValues = null;
+ }
+}
diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs
index be172cd..7ad3ba2 100644
--- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs
+++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs
@@ -374,4 +374,24 @@ public void StringValuesConvertToString(string expected)
Assert.Equal(expected, actual);
}
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void EmptyOrWhitespaceStringConvertsToEmptyArrayForArrayType(string input)
+ {
+ var value = new StringArgumentValue(input);
+ var actual = value.ConvertTo(typeof(string[]), new ResolutionContext());
+
+ var array = Assert.IsType(actual);
+ Assert.Empty(array);
+ }
+
+ [Fact]
+ public void NonEmptyStringDoesNotConvertToArray()
+ {
+ var value = new StringArgumentValue("Information");
+ Assert.Throws(() =>
+ value.ConvertTo(typeof(string[]), new ResolutionContext()));
+ }
}
diff --git a/test/TestDummies/DummyFailingSink.cs b/test/TestDummies/DummyFailingSink.cs
new file mode 100644
index 0000000..935827b
--- /dev/null
+++ b/test/TestDummies/DummyFailingSink.cs
@@ -0,0 +1,23 @@
+using Serilog.Core;
+using Serilog.Events;
+
+namespace TestDummies;
+
+public class DummyFailingSink : ILogEventSink
+{
+ [ThreadStatic]
+ static int _emitAttempts;
+
+ public static int EmitAttempts => _emitAttempts;
+
+ public void Emit(LogEvent logEvent)
+ {
+ _emitAttempts++;
+ throw new InvalidOperationException("DummyFailingSink always fails.");
+ }
+
+ public static void Reset()
+ {
+ _emitAttempts = 0;
+ }
+}
diff --git a/test/TestDummies/DummyFailureListener.cs b/test/TestDummies/DummyFailureListener.cs
new file mode 100644
index 0000000..6dfd872
--- /dev/null
+++ b/test/TestDummies/DummyFailureListener.cs
@@ -0,0 +1,27 @@
+using Serilog.Core;
+using Serilog.Events;
+
+namespace TestDummies;
+
+public class DummyFailureListener : ILoggingFailureListener
+{
+ [ThreadStatic]
+ static int _failureCount;
+
+ public static int FailureCount => _failureCount;
+
+ public void OnLoggingFailed(
+ object sender,
+ LoggingFailureKind kind,
+ string message,
+ IReadOnlyCollection? events,
+ Exception? exception)
+ {
+ _failureCount++;
+ }
+
+ public static void Reset()
+ {
+ _failureCount = 0;
+ }
+}