Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8679daf
Dev version bump [skip ci]
nblumhardt Nov 26, 2025
ba865b9
Support LevelAlias names in configuration parsing
mohammed-saalim Jan 15, 2026
512647e
Add xUnit tests for ParseLogEventLevel
mohammed-saalim Jan 19, 2026
e0b845c
Merge pull request #465 from mohammed-saalim/add-levelalias-parsing
nblumhardt Jan 20, 2026
31a7ce8
issue-468: Fix empty/whitespace string converting to array type
gyurebalint-CID Mar 28, 2026
0dad5b3
issue-468: regression test asserting that a non-empty single string (…
gyurebalint-CID Mar 28, 2026
2cc6358
issue-468 change dummy sting value
gyurebalint-CID Mar 28, 2026
3d4ef5c
Fixes #337: Update appsettings.json to use shortened syntax
gyurebalint Apr 6, 2026
11dcbc9
Merge pull request #470 from gyurebalint/fix-conditional-sink-syntax
nblumhardt Apr 7, 2026
6c97d2b
params array parameters now treated as optional when Args omitted
gyurebalint Apr 7, 2026
076613f
add tests for params array parameter optional behaviour
gyurebalint Apr 7, 2026
aaf4240
typo plus force added util for test
gyurebalint Apr 7, 2026
7dde00f
update tests to align more with code base
gyurebalint Apr 7, 2026
fad3294
add DummyParamsSink.cs
gyurebalint Apr 7, 2026
955b652
Merge pull request #469 from gyurebalint-CID/fix/issue-468
nblumhardt Apr 9, 2026
472dc57
Add FallbackChain and Fallible support in configuration
ArieGato Apr 19, 2026
f8d19de
Document wrapper sinks and expand sample configuration
ArieGato Apr 19, 2026
d08b415
Merge pull request #474 from ArieGato/feature/fallback-chain-configur…
nblumhardt Apr 30, 2026
54837b2
handle C# 13 params collections to ensure graceful degradation and ad…
gyurebalint May 1, 2026
427ec51
Merge pull request #471 from gyurebalint/fix/issue-441
nblumhardt May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<!-- This must match the major and minor components of the referenced Microsoft.Extensions.Configuration package. -->
<VersionPrefix>10.0.0</VersionPrefix>
<VersionPrefix>10.0.1</VersionPrefix>
</PropertyGroup>
</Project>
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions sample/Sample/AlwaysFailingSink.cs
Original file line number Diff line number Diff line change
@@ -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.");
}
}
22 changes: 22 additions & 0 deletions sample/Sample/SampleFailureListener.cs
Original file line number Diff line number Diff line change
@@ -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<LogEvent>? 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}");
}
}
67 changes: 64 additions & 3 deletions sample/Sample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"LevelSwitches": { "controlSwitch": "Verbose" },
"FilterSwitches": { "$filterSwitch": "Application = 'Sample'" },
"MinimumLevel": {
"Default": "Debug",
"ControlledBy": "$controlSwitch",
"Override": {
"Microsoft": "Warning",
"MyApp.Something.Tricky": "Verbose"
Expand Down Expand Up @@ -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}"
}
}
}
]
Expand All @@ -46,7 +49,7 @@
"WriteTo:ConditionalSink": {
"Name": "Conditional",
"Args": {
"expression": "@Level in ['Error', 'Fatal']",
"expression": "@l in ['Error', 'Fatal']",
"configureSink": [
{
"Name": "File",
Expand All @@ -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",
Expand All @@ -79,6 +119,13 @@
"expression": "Application = 'Sample'",
"configureEnricher": [ "WithMachineName" ]
}
},
{
"Name": "WithComputed",
"Args": {
"name": "ShortContext",
"expression": "coalesce(SourceContext, '<no-context>')"
}
}
],
"Properties": {
Expand Down Expand Up @@ -109,6 +156,12 @@
"switch": "$filterSwitch"
}
},
{
"Name": "ByExcluding",
"Args": {
"expression": "Application = 'OtherApp'"
}
},
{
"Name": "With",
"Args": {
Expand All @@ -118,6 +171,14 @@
}
}
}
],
"AuditTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[AUDIT {Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -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<MethodInfo> candidateMethods, string name, IReadOnlyCollection<string> suppliedArgumentNames)
Expand Down Expand Up @@ -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.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ static LoggerConfiguration Logger(
LoggingLevelSwitch? levelSwitch = null)
=> loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch);

static LoggerConfiguration FallbackChain(
LoggerSinkConfiguration loggerSinkConfiguration,
Action<LoggerSinkConfiguration> configureSink,
Action<LoggerSinkConfiguration> configureFallback,
Action<LoggerSinkConfiguration>[]? configureSubsequentFallbacks = null)
=> loggerSinkConfiguration.FallbackChain(configureSink, configureFallback, configureSubsequentFallbacks ?? []);

static LoggerConfiguration Fallible(
LoggerSinkConfiguration loggerSinkConfiguration,
Action<LoggerSinkConfiguration> configureSink,
ILoggingFailureListener failureListener)
=> loggerSinkConfiguration.Fallible(configureSink, failureListener);

// .AuditTo...
// ========
static LoggerConfiguration Sink(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(() => ConfigurationReader.ParseLogEventLevel(value));
}

[Fact]
public void ParamsStringArrayParameter_WithNoArgsSupplied_IsMatchedAsOptional()
{
var candidateMethods = typeof(DummyLoggerConfigurationExtensions)
.GetTypeInfo()
.DeclaredMethods
.ToList();

var selected = ConfigurationReader.SelectConfigurationMethod(
candidateMethods, "DummyParamsArray", Array.Empty<string>());

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<string[]>(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<string>

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<string>

var result = reader.GetImplicitValueForNotSpecifiedKey(param, method);

Assert.Null(result);
}
}
Loading
Loading