diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index d3441da..5fb611e 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -10,6 +10,8 @@ using Serilog; using Serilog.Core; using Serilog.Events; +using System.Collections.Generic; +using Serilog.Debugging; namespace Sample { diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index d952590..3a31dbf 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -2,6 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console" ], "LevelSwitches": { "$controlSwitch": "Verbose" }, + "FilterSwitches": { "$filterSwitch": "Application = 'Sample'" }, "MinimumLevel": { "Default": "Debug", "Override": { @@ -97,9 +98,9 @@ ], "Filter": [ { - "Name": "ByIncludingOnly", + "Name": "ControlledBy", "Args": { - "expression": "Application = 'Sample'" + "switch": "$filterSwitch" } }, { diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 4d44c32..1c2d619 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -41,6 +41,7 @@ internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyColle public void Configure(LoggerConfiguration loggerConfiguration) { ProcessLevelSwitchDeclarations(); + ProcessFilterSwitchDeclarations(); ApplyMinimumLevel(loggerConfiguration); ApplyEnrichment(loggerConfiguration); @@ -50,6 +51,63 @@ public void Configure(LoggerConfiguration loggerConfiguration) ApplyAuditSinks(loggerConfiguration); } + void ProcessFilterSwitchDeclarations() + { + var filterSwitchesDirective = _section.GetSection("FilterSwitches"); + + foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren()) + { + var filterSwitch = LoggingFilterSwitchProxy.Create(); + if (filterSwitch == null) + { + SelfLog.WriteLine($"FilterSwitches section found, but neither Serilog.Expressions nor Serilog.Filters.Expressions is referenced."); + break; + } + + var switchName = filterSwitchDeclaration.Key; + // switchName must be something like $switch to avoid ambiguities + if (!IsValidSwitchName(switchName)) + { + throw new FormatException($"\"{switchName}\" is not a valid name for a Filter Switch declaration. Filter switch must be declared with a '$' sign, like \"FilterSwitches\" : {{\"$switchName\" : \"{{FilterExpression}}\"}}"); + } + + SetFilterSwitch(throwOnError: true); + SubscribeToFilterExpressionChanges(); + + _resolutionContext.AddFilterSwitch(switchName, filterSwitch); + + void SubscribeToFilterExpressionChanges() + { + ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false)); + } + + void SetFilterSwitch(bool throwOnError) + { + var filterExpr = filterSwitchDeclaration.Value; + if (string.IsNullOrWhiteSpace(filterExpr)) + { + filterSwitch.Expression = null; + return; + } + + try + { + filterSwitch.Expression = filterExpr; + } + catch (Exception e) + { + var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}."; + if (throwOnError) + { + throw new InvalidOperationException(errMsg, e); + } + + SelfLog.WriteLine(errMsg); + } + } + } + } + void ProcessLevelSwitchDeclarations() { var levelSwitchesDirective = _section.GetSection("LevelSwitches"); @@ -94,7 +152,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); if (minLevelControlledByDirective.Value != null) { - var globalMinimumLevelSwitch = _resolutionContext.LookUpSwitchByName(minLevelControlledByDirective.Value); + var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch); } @@ -109,7 +167,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) } else { - var overrideSwitch = _resolutionContext.LookUpSwitchByName(overridenLevelOrSwitch); + var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch); } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs new file mode 100644 index 0000000..4736ee2 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs @@ -0,0 +1,49 @@ +using System; + +namespace Serilog.Settings.Configuration +{ + class LoggingFilterSwitchProxy + { + readonly Action _setProxy; + readonly Func _getProxy; + + LoggingFilterSwitchProxy(object realSwitch) + { + RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch)); + + var expressionProperty = realSwitch.GetType().GetProperty("Expression"); + + _setProxy = (Action)Delegate.CreateDelegate( + typeof(Action), + realSwitch, + expressionProperty.GetSetMethod()); + + _getProxy = (Func)Delegate.CreateDelegate( + typeof(Func), + realSwitch, + expressionProperty.GetGetMethod()); + } + + public object RealSwitch { get; } + + public string Expression + { + get => _getProxy(); + set => _setProxy(value); + } + + public static LoggingFilterSwitchProxy Create(string expression = null) + { + var filterSwitchType = + Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ?? + Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions"); + + if (filterSwitchType is null) + { + return null; + } + + return new LoggingFilterSwitchProxy(Activator.CreateInstance(filterSwitchType, expression)); + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs index 90ab06d..91ce8dc 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs @@ -12,11 +12,13 @@ namespace Serilog.Settings.Configuration sealed class ResolutionContext { readonly IDictionary _declaredLevelSwitches; + readonly IDictionary _declaredFilterSwitches; readonly IConfiguration _appConfiguration; public ResolutionContext(IConfiguration appConfiguration = null) { _declaredLevelSwitches = new Dictionary(); + _declaredFilterSwitches = new Dictionary(); _appConfiguration = appConfiguration; } @@ -26,7 +28,7 @@ public ResolutionContext(IConfiguration appConfiguration = null) /// the name of a switch to look up /// the LoggingLevelSwitch registered with the name /// if no switch has been registered with - public LoggingLevelSwitch LookUpSwitchByName(string switchName) + public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName) { if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch)) { @@ -36,6 +38,16 @@ public LoggingLevelSwitch LookUpSwitchByName(string switchName) throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}"); } + public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName) + { + if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch)) + { + return filterSwitch; + } + + throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}"); + } + public bool HasAppConfiguration => _appConfiguration != null; public IConfiguration AppConfiguration @@ -57,5 +69,12 @@ public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitc if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); _declaredLevelSwitches[levelSwitchName] = levelSwitch; } + + public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch) + { + if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName)); + if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch)); + _declaredFilterSwitches[filterSwitchName] = filterSwitch; + } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index 27834a1..2cd1373 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -32,7 +32,13 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext) if (toType == typeof(LoggingLevelSwitch)) { - return resolutionContext.LookUpSwitchByName(argumentValue); + return resolutionContext.LookUpLevelSwitchByName(argumentValue); + } + + if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" || + toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch") + { + return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch; } var toTypeInfo = toType.GetTypeInfo(); diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index 5c513a9..ed2e62b 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -338,6 +338,33 @@ public void LoggingLevelSwitchWithInvalidNameThrowsFormatException() Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message); } + [Fact] + public void LoggingFilterSwitchIsConfigured() + { + var json = @"{ + 'Serilog': { + 'FilterSwitches': { '$mySwitch': 'Prop = 42' }, + 'Filter:BySwitch': { + 'Name': 'ControlledBy', + 'Args': { + 'switch': '$mySwitch' + } + } + } + }"; + LogEvent evt = null; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + Assert.Null(evt); + + log.ForContext("Prop", 42).Write(Some.InformationEvent()); + Assert.NotNull(evt); + } + [Fact] public void LoggingLevelSwitchIsConfigured() { diff --git a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs index b1b1f43..6b439f6 100644 --- a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs @@ -21,6 +21,13 @@ public class DynamicLevelChangeTests } }, 'LevelSwitches': { '$mySwitch': 'Information' }, + 'FilterSwitches': { '$myFilter': null }, + 'Filter:Dummy': { + 'Name': 'ControlledBy', + 'Args': { + 'switch': '$myFilter' + } + }, 'WriteTo:Dummy': { 'Name': 'DummyConsole', 'Args': { @@ -64,10 +71,22 @@ public void ShouldRespectDynamicLevelChanges() UpdateConfig(overrideLevel: LogEventLevel.Debug); logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_1'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_2'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Empty(DummyConsoleSink.Emitted); } } - void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null) + void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string filterExpression = null) { if (minimumLevel.HasValue) { @@ -84,6 +103,11 @@ void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel _configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString()); } + if (filterExpression != null) + { + _configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression); + } + _configSource.Reload(); } } diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj index 95737d6..9a02189 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -36,6 +36,7 @@ +