Skip to content

Commit 07d7a8d

Browse files
authored
Merge pull request #162 from skomis-mm/filterswitch
LoggingFilterSwitch support
2 parents aff1d22 + 9af2f30 commit 07d7a8d

File tree

9 files changed

+194
-7
lines changed

9 files changed

+194
-7
lines changed

sample/Sample/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using Serilog;
1111
using Serilog.Core;
1212
using Serilog.Events;
13+
using System.Collections.Generic;
14+
using Serilog.Debugging;
1315

1416
namespace Sample
1517
{

sample/Sample/appsettings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"Serilog": {
33
"Using": [ "Serilog.Sinks.Console" ],
44
"LevelSwitches": { "$controlSwitch": "Verbose" },
5+
"FilterSwitches": { "$filterSwitch": "Application = 'Sample'" },
56
"MinimumLevel": {
67
"Default": "Debug",
78
"Override": {
@@ -97,9 +98,9 @@
9798
],
9899
"Filter": [
99100
{
100-
"Name": "ByIncludingOnly",
101+
"Name": "ControlledBy",
101102
"Args": {
102-
"expression": "Application = 'Sample'"
103+
"switch": "$filterSwitch"
103104
}
104105
},
105106
{

src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyColle
4141
public void Configure(LoggerConfiguration loggerConfiguration)
4242
{
4343
ProcessLevelSwitchDeclarations();
44+
ProcessFilterSwitchDeclarations();
4445

4546
ApplyMinimumLevel(loggerConfiguration);
4647
ApplyEnrichment(loggerConfiguration);
@@ -50,6 +51,63 @@ public void Configure(LoggerConfiguration loggerConfiguration)
5051
ApplyAuditSinks(loggerConfiguration);
5152
}
5253

54+
void ProcessFilterSwitchDeclarations()
55+
{
56+
var filterSwitchesDirective = _section.GetSection("FilterSwitches");
57+
58+
foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren())
59+
{
60+
var filterSwitch = LoggingFilterSwitchProxy.Create();
61+
if (filterSwitch == null)
62+
{
63+
SelfLog.WriteLine($"FilterSwitches section found, but neither Serilog.Expressions nor Serilog.Filters.Expressions is referenced.");
64+
break;
65+
}
66+
67+
var switchName = filterSwitchDeclaration.Key;
68+
// switchName must be something like $switch to avoid ambiguities
69+
if (!IsValidSwitchName(switchName))
70+
{
71+
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}}\"}}");
72+
}
73+
74+
SetFilterSwitch(throwOnError: true);
75+
SubscribeToFilterExpressionChanges();
76+
77+
_resolutionContext.AddFilterSwitch(switchName, filterSwitch);
78+
79+
void SubscribeToFilterExpressionChanges()
80+
{
81+
ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false));
82+
}
83+
84+
void SetFilterSwitch(bool throwOnError)
85+
{
86+
var filterExpr = filterSwitchDeclaration.Value;
87+
if (string.IsNullOrWhiteSpace(filterExpr))
88+
{
89+
filterSwitch.Expression = null;
90+
return;
91+
}
92+
93+
try
94+
{
95+
filterSwitch.Expression = filterExpr;
96+
}
97+
catch (Exception e)
98+
{
99+
var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}.";
100+
if (throwOnError)
101+
{
102+
throw new InvalidOperationException(errMsg, e);
103+
}
104+
105+
SelfLog.WriteLine(errMsg);
106+
}
107+
}
108+
}
109+
}
110+
53111
void ProcessLevelSwitchDeclarations()
54112
{
55113
var levelSwitchesDirective = _section.GetSection("LevelSwitches");
@@ -94,7 +152,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
94152
var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy");
95153
if (minLevelControlledByDirective.Value != null)
96154
{
97-
var globalMinimumLevelSwitch = _resolutionContext.LookUpSwitchByName(minLevelControlledByDirective.Value);
155+
var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value);
98156
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
99157
loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch);
100158
}
@@ -109,7 +167,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
109167
}
110168
else
111169
{
112-
var overrideSwitch = _resolutionContext.LookUpSwitchByName(overridenLevelOrSwitch);
170+
var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch);
113171
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
114172
loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch);
115173
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
3+
namespace Serilog.Settings.Configuration
4+
{
5+
class LoggingFilterSwitchProxy
6+
{
7+
readonly Action<string> _setProxy;
8+
readonly Func<string> _getProxy;
9+
10+
LoggingFilterSwitchProxy(object realSwitch)
11+
{
12+
RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch));
13+
14+
var expressionProperty = realSwitch.GetType().GetProperty("Expression");
15+
16+
_setProxy = (Action<string>)Delegate.CreateDelegate(
17+
typeof(Action<string>),
18+
realSwitch,
19+
expressionProperty.GetSetMethod());
20+
21+
_getProxy = (Func<string>)Delegate.CreateDelegate(
22+
typeof(Func<string>),
23+
realSwitch,
24+
expressionProperty.GetGetMethod());
25+
}
26+
27+
public object RealSwitch { get; }
28+
29+
public string Expression
30+
{
31+
get => _getProxy();
32+
set => _setProxy(value);
33+
}
34+
35+
public static LoggingFilterSwitchProxy Create(string expression = null)
36+
{
37+
var filterSwitchType =
38+
Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ??
39+
Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions");
40+
41+
if (filterSwitchType is null)
42+
{
43+
return null;
44+
}
45+
46+
return new LoggingFilterSwitchProxy(Activator.CreateInstance(filterSwitchType, expression));
47+
}
48+
}
49+
}

src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ namespace Serilog.Settings.Configuration
1212
sealed class ResolutionContext
1313
{
1414
readonly IDictionary<string, LoggingLevelSwitch> _declaredLevelSwitches;
15+
readonly IDictionary<string, LoggingFilterSwitchProxy> _declaredFilterSwitches;
1516
readonly IConfiguration _appConfiguration;
1617

1718
public ResolutionContext(IConfiguration appConfiguration = null)
1819
{
1920
_declaredLevelSwitches = new Dictionary<string, LoggingLevelSwitch>();
21+
_declaredFilterSwitches = new Dictionary<string, LoggingFilterSwitchProxy>();
2022
_appConfiguration = appConfiguration;
2123
}
2224

@@ -26,7 +28,7 @@ public ResolutionContext(IConfiguration appConfiguration = null)
2628
/// <param name="switchName">the name of a switch to look up</param>
2729
/// <returns>the LoggingLevelSwitch registered with the name</returns>
2830
/// <exception cref="InvalidOperationException">if no switch has been registered with <paramref name="switchName"/></exception>
29-
public LoggingLevelSwitch LookUpSwitchByName(string switchName)
31+
public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName)
3032
{
3133
if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch))
3234
{
@@ -36,6 +38,16 @@ public LoggingLevelSwitch LookUpSwitchByName(string switchName)
3638
throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}");
3739
}
3840

41+
public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName)
42+
{
43+
if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch))
44+
{
45+
return filterSwitch;
46+
}
47+
48+
throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}");
49+
}
50+
3951
public bool HasAppConfiguration => _appConfiguration != null;
4052

4153
public IConfiguration AppConfiguration
@@ -57,5 +69,12 @@ public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitc
5769
if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch));
5870
_declaredLevelSwitches[levelSwitchName] = levelSwitch;
5971
}
72+
73+
public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch)
74+
{
75+
if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName));
76+
if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch));
77+
_declaredFilterSwitches[filterSwitchName] = filterSwitch;
78+
}
6079
}
6180
}

src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,13 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
3232

3333
if (toType == typeof(LoggingLevelSwitch))
3434
{
35-
return resolutionContext.LookUpSwitchByName(argumentValue);
35+
return resolutionContext.LookUpLevelSwitchByName(argumentValue);
36+
}
37+
38+
if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" ||
39+
toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch")
40+
{
41+
return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch;
3642
}
3743

3844
var toTypeInfo = toType.GetTypeInfo();

test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,33 @@ public void LoggingLevelSwitchWithInvalidNameThrowsFormatException()
338338
Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message);
339339
}
340340

341+
[Fact]
342+
public void LoggingFilterSwitchIsConfigured()
343+
{
344+
var json = @"{
345+
'Serilog': {
346+
'FilterSwitches': { '$mySwitch': 'Prop = 42' },
347+
'Filter:BySwitch': {
348+
'Name': 'ControlledBy',
349+
'Args': {
350+
'switch': '$mySwitch'
351+
}
352+
}
353+
}
354+
}";
355+
LogEvent evt = null;
356+
357+
var log = ConfigFromJson(json)
358+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
359+
.CreateLogger();
360+
361+
log.Write(Some.InformationEvent());
362+
Assert.Null(evt);
363+
364+
log.ForContext("Prop", 42).Write(Some.InformationEvent());
365+
Assert.NotNull(evt);
366+
}
367+
341368
[Fact]
342369
public void LoggingLevelSwitchIsConfigured()
343370
{

test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ public class DynamicLevelChangeTests
2121
}
2222
},
2323
'LevelSwitches': { '$mySwitch': 'Information' },
24+
'FilterSwitches': { '$myFilter': null },
25+
'Filter:Dummy': {
26+
'Name': 'ControlledBy',
27+
'Args': {
28+
'switch': '$myFilter'
29+
}
30+
},
2431
'WriteTo:Dummy': {
2532
'Name': 'DummyConsole',
2633
'Args': {
@@ -64,10 +71,22 @@ public void ShouldRespectDynamicLevelChanges()
6471
UpdateConfig(overrideLevel: LogEventLevel.Debug);
6572
logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent());
6673
Assert.Single(DummyConsoleSink.Emitted);
74+
75+
DummyConsoleSink.Emitted.Clear();
76+
UpdateConfig(filterExpression: "Prop = 'Val_1'");
77+
logger.Write(Some.DebugEvent());
78+
logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent());
79+
Assert.Single(DummyConsoleSink.Emitted);
80+
81+
DummyConsoleSink.Emitted.Clear();
82+
UpdateConfig(filterExpression: "Prop = 'Val_2'");
83+
logger.Write(Some.DebugEvent());
84+
logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent());
85+
Assert.Empty(DummyConsoleSink.Emitted);
6786
}
6887
}
6988

70-
void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null)
89+
void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string filterExpression = null)
7190
{
7291
if (minimumLevel.HasValue)
7392
{
@@ -84,6 +103,11 @@ void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel
84103
_configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString());
85104
}
86105

106+
if (filterExpression != null)
107+
{
108+
_configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression);
109+
}
110+
87111
_configSource.Reload();
88112
}
89113
}

test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
<ItemGroup>
3838
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
39+
<PackageReference Include="Serilog.Filters.Expressions" Version="2.0.0" />
3940
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
4041
<PackageReference Include="xunit" Version="2.2.0" />
4142
</ItemGroup>

0 commit comments

Comments
 (0)