Skip to content

Commit 4d95da9

Browse files
author
Sergey Komisarchik
committed
respect dynamic logging level changes for LevelSwitch declarations
1 parent c8dcb96 commit 4d95da9

File tree

7 files changed

+157
-60
lines changed

7 files changed

+157
-60
lines changed

sample/Sample/appsettings.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"Serilog": {
3-
"Using": ["Serilog.Sinks.Console"],
3+
"Using": [ "Serilog.Sinks.Console" ],
4+
"LevelSwitches": { "$controlSwitch": "Verbose" },
45
"MinimumLevel": {
56
"Default": "Debug",
67
"Override": {
@@ -12,6 +13,7 @@
1213
"Name": "Logger",
1314
"Args": {
1415
"configureLogger": {
16+
"MinimumLevel": "Verbose",
1517
"WriteTo": [
1618
{
1719
"Name": "Console",
@@ -22,7 +24,8 @@
2224
}
2325
]
2426
},
25-
"restrictedToMinimumLevel": "Debug"
27+
"restrictedToMinimumLevel": "Verbose",
28+
"levelSwitch": "$controlSwitch"
2629
}
2730
},
2831
"WriteTo:Async": {
@@ -39,7 +42,7 @@
3942
]
4043
}
4144
},
42-
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
45+
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
4346
"Properties": {
4447
"Application": "Sample"
4548
},

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ void ProcessLevelSwitchDeclarations()
6262
{
6363
throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. Level switch must be declared with a '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}");
6464
}
65+
6566
LoggingLevelSwitch newSwitch;
6667
if (string.IsNullOrEmpty(switchInitialLevel))
6768
{
@@ -72,6 +73,9 @@ void ProcessLevelSwitchDeclarations()
7273
var initialLevel = ParseLogEventLevel(switchInitialLevel);
7374
newSwitch = new LoggingLevelSwitch(initialLevel);
7475
}
76+
77+
SubscribeToLoggingLevelChanges(levelSwitchDeclaration, newSwitch);
78+
7579
// make them available later on when resolving argument values
7680
_resolutionContext.AddLevelSwitch(switchName, newSwitch);
7781
}
@@ -118,18 +122,23 @@ void ApplyMinimumLevel(IConfigurationSection directive, Action<LoggerMinimumLeve
118122
var levelSwitch = new LoggingLevelSwitch(minimumLevel);
119123
applyConfigAction(loggerConfiguration.MinimumLevel, levelSwitch);
120124

121-
ChangeToken.OnChange(
122-
directive.GetReloadToken,
123-
() =>
124-
{
125-
if (Enum.TryParse(directive.Value, out minimumLevel))
126-
levelSwitch.MinimumLevel = minimumLevel;
127-
else
128-
SelfLog.WriteLine($"The value {directive.Value} is not a valid Serilog level.");
129-
});
125+
SubscribeToLoggingLevelChanges(directive, levelSwitch);
130126
}
131127
}
132128

129+
void SubscribeToLoggingLevelChanges(IConfigurationSection levelSection, LoggingLevelSwitch levelSwitch)
130+
{
131+
ChangeToken.OnChange(
132+
levelSection.GetReloadToken,
133+
() =>
134+
{
135+
if (Enum.TryParse(levelSection.Value, out LogEventLevel minimumLevel))
136+
levelSwitch.MinimumLevel = minimumLevel;
137+
else
138+
SelfLog.WriteLine($"The value {levelSection.Value} is not a valid Serilog level.");
139+
});
140+
}
141+
133142
void ApplyFilters(LoggerConfiguration loggerConfiguration)
134143
{
135144
var filterDirective = _section.GetSection("Filter");
@@ -233,7 +242,7 @@ IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSecti
233242

234243
if (argumentSection.Value != null)
235244
{
236-
argumentValue = new StringArgumentValue(() => argumentSection.Value, argumentSection.GetReloadToken);
245+
argumentValue = new StringArgumentValue(argumentSection.Value);
237246
}
238247
else
239248
{

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

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,20 @@
33
using System.Linq;
44
using System.Reflection;
55
using System.Text.RegularExpressions;
6-
using Microsoft.Extensions.Primitives;
76

87
using Serilog.Core;
9-
using Serilog.Debugging;
10-
using Serilog.Events;
118

129
namespace Serilog.Settings.Configuration
1310
{
1411
class StringArgumentValue : IConfigurationArgumentValue
1512
{
16-
readonly Func<string> _valueProducer;
17-
readonly Func<IChangeToken> _changeTokenProducer;
13+
readonly string _providedValue;
1814

1915
static readonly Regex StaticMemberAccessorRegex = new Regex("^(?<shortTypeName>[^:]+)::(?<memberName>[A-Za-z][A-Za-z0-9]*)(?<typeNameExtraQualifiers>[^:]*)$");
2016

21-
public StringArgumentValue(Func<string> valueProducer, Func<IChangeToken> changeTokenProducer = null)
17+
public StringArgumentValue(string providedValue)
2218
{
23-
_valueProducer = valueProducer ?? throw new ArgumentNullException(nameof(valueProducer));
24-
_changeTokenProducer = changeTokenProducer;
19+
_providedValue = providedValue ?? throw new ArgumentNullException(nameof(providedValue));
2520
}
2621

2722
static readonly Dictionary<Type, Func<string, object>> ExtendedTypeConversions = new Dictionary<Type, Func<string, object>>
@@ -33,7 +28,7 @@ public StringArgumentValue(Func<string> valueProducer, Func<IChangeToken> change
3328

3429
public object ConvertTo(Type toType, ResolutionContext resolutionContext)
3530
{
36-
var argumentValue = Environment.ExpandEnvironmentVariables(_valueProducer());
31+
var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue);
3732

3833
if (toType == typeof(LoggingLevelSwitch))
3934
{
@@ -114,31 +109,6 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
114109
}
115110
}
116111

117-
if (toType == typeof(LoggingLevelSwitch))
118-
{
119-
if (!Enum.TryParse(argumentValue, out LogEventLevel minimumLevel))
120-
throw new InvalidOperationException($"The value `{argumentValue}` is not a valid Serilog level.");
121-
122-
var levelSwitch = new LoggingLevelSwitch(minimumLevel);
123-
124-
if (_changeTokenProducer != null)
125-
{
126-
ChangeToken.OnChange(
127-
_changeTokenProducer,
128-
() =>
129-
{
130-
var newArgumentValue = _valueProducer();
131-
132-
if (Enum.TryParse(newArgumentValue, out minimumLevel))
133-
levelSwitch.MinimumLevel = minimumLevel;
134-
else
135-
SelfLog.WriteLine($"The value `{newArgumentValue}` is not a valid Serilog level.");
136-
});
137-
}
138-
139-
return levelSwitch;
140-
}
141-
142112
return Convert.ChangeType(argumentValue, toType);
143113
}
144114

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading;
4+
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Configuration.Json;
7+
8+
using Serilog.Core;
9+
using Serilog.Events;
10+
using Serilog.Settings.Configuration.Tests.Support;
11+
12+
using Xunit;
13+
using TestDummies.Console;
14+
15+
namespace Serilog.Settings.Configuration.Tests
16+
{
17+
public class DynamicLevelChangeTests : IDisposable
18+
{
19+
const string ConfigFilename = "dynamicLevels.json";
20+
21+
readonly IConfigurationRoot _config;
22+
23+
LogEventLevel _minimumLevel, _overrideLevel, _switchLevel;
24+
25+
public DynamicLevelChangeTests()
26+
{
27+
UpdateConfig(LogEventLevel.Information, LogEventLevel.Information, LogEventLevel.Information);
28+
29+
var jsonConfigurationSource = new JsonConfigurationSource
30+
{
31+
FileProvider = null,
32+
Path = ConfigFilename,
33+
Optional = false,
34+
ReloadOnChange = true,
35+
ReloadDelay = 10
36+
};
37+
38+
jsonConfigurationSource.ResolveFileProvider();
39+
40+
_config = new ConfigurationBuilder()
41+
.Add(jsonConfigurationSource)
42+
.Build();
43+
}
44+
45+
public void Dispose()
46+
{
47+
if (File.Exists(ConfigFilename))
48+
{
49+
File.Delete(ConfigFilename);
50+
}
51+
}
52+
53+
[Fact]
54+
public void ShouldRespectDynamicLevelChanges()
55+
{
56+
using (var logger = new LoggerConfiguration().ReadFrom.Configuration(_config).CreateLogger())
57+
{
58+
DummyConsoleSink.Emitted.Clear();
59+
logger.Write(Some.DebugEvent());
60+
Assert.Empty(DummyConsoleSink.Emitted);
61+
62+
DummyConsoleSink.Emitted.Clear();
63+
UpdateConfig(minimumLevel: LogEventLevel.Debug);
64+
logger.Write(Some.DebugEvent());
65+
Assert.Empty(DummyConsoleSink.Emitted);
66+
67+
DummyConsoleSink.Emitted.Clear();
68+
UpdateConfig(switchLevel: LogEventLevel.Debug);
69+
logger.Write(Some.DebugEvent());
70+
logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent());
71+
Assert.Single(DummyConsoleSink.Emitted);
72+
73+
DummyConsoleSink.Emitted.Clear();
74+
UpdateConfig(overrideLevel: LogEventLevel.Debug);
75+
logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent());
76+
Assert.Single(DummyConsoleSink.Emitted);
77+
}
78+
}
79+
80+
void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? overrideLevel = null, LogEventLevel? switchLevel = null)
81+
{
82+
File.WriteAllText(ConfigFilename, BuildConfiguration());
83+
Thread.Sleep(50);
84+
85+
string BuildConfiguration()
86+
{
87+
_minimumLevel = minimumLevel ?? _minimumLevel;
88+
_overrideLevel = overrideLevel ?? _overrideLevel;
89+
_switchLevel = switchLevel ?? _switchLevel;
90+
91+
var config = @"{
92+
'Serilog': {
93+
'Using': [ 'TestDummies' ],
94+
'MinimumLevel': {
95+
'Default': '" + _minimumLevel + @"',
96+
'Override': {
97+
'Root.Test': '" + _overrideLevel + @"'
98+
}
99+
},
100+
'LevelSwitches': { '$mySwitch': '" + _switchLevel + @"' },
101+
'WriteTo:Dummy': {
102+
'Name': 'DummyConsole',
103+
'Args': {
104+
'levelSwitch': '$mySwitch'
105+
}
106+
}
107+
}
108+
}";
109+
110+
return config;
111+
}
112+
}
113+
}
114+
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class StringArgumentValueTests
1515
[Fact]
1616
public void StringValuesConvertToDefaultInstancesIfTargetIsInterface()
1717
{
18-
var stringArgumentValue = new StringArgumentValue(() => "Serilog.Formatting.Json.JsonFormatter, Serilog");
18+
var stringArgumentValue = new StringArgumentValue("Serilog.Formatting.Json.JsonFormatter, Serilog");
1919

2020
var result = stringArgumentValue.ConvertTo(typeof(ITextFormatter), new ResolutionContext());
2121

@@ -25,7 +25,7 @@ public void StringValuesConvertToDefaultInstancesIfTargetIsInterface()
2525
[Fact]
2626
public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass()
2727
{
28-
var stringArgumentValue = new StringArgumentValue(() => "Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests");
28+
var stringArgumentValue = new StringArgumentValue("Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests");
2929

3030
var result = stringArgumentValue.ConvertTo(typeof(AbstractClass), new ResolutionContext());
3131

@@ -81,7 +81,7 @@ public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, str
8181
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractField, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))]
8282
public void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType)
8383
{
84-
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
84+
var stringArgumentValue = new StringArgumentValue($"{input}");
8585

8686
var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext());
8787

@@ -98,7 +98,7 @@ public void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type
9898
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))]
9999
public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType)
100100
{
101-
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
101+
var stringArgumentValue = new StringArgumentValue($"{input}");
102102
Assert.Throws<TypeLoadException>(() =>
103103
stringArgumentValue.ConvertTo(targetType, new ResolutionContext())
104104
);
@@ -117,7 +117,7 @@ public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Typ
117117
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
118118
public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType)
119119
{
120-
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
120+
var stringArgumentValue = new StringArgumentValue($"{input}");
121121
var exception = Assert.Throws<InvalidOperationException>(() =>
122122
stringArgumentValue.ConvertTo(targetType, new ResolutionContext())
123123
);
@@ -134,7 +134,7 @@ public void LevelSwitchesCanBeLookedUpByName()
134134
var resolutionContext = new ResolutionContext();
135135
resolutionContext.AddLevelSwitch(switchName, @switch);
136136

137-
var stringArgumentValue = new StringArgumentValue(() => switchName);
137+
var stringArgumentValue = new StringArgumentValue(switchName);
138138

139139
var resolvedSwitch = stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext);
140140

@@ -149,7 +149,7 @@ public void ReferencingUndeclaredLevelSwitchThrows()
149149
var resolutionContext = new ResolutionContext();
150150
resolutionContext.AddLevelSwitch("$anotherSwitch", new LoggingLevelSwitch(LogEventLevel.Verbose));
151151

152-
var stringArgumentValue = new StringArgumentValue(() => "$mySwitch");
152+
var stringArgumentValue = new StringArgumentValue("$mySwitch");
153153

154154
var ex = Assert.Throws<InvalidOperationException>(() =>
155155
stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext)
@@ -163,7 +163,7 @@ public void ReferencingUndeclaredLevelSwitchThrows()
163163
public void StringValuesConvertToTypeFromShortTypeName()
164164
{
165165
var shortTypeName = "System.Version";
166-
var stringArgumentValue = new StringArgumentValue(() => shortTypeName);
166+
var stringArgumentValue = new StringArgumentValue(shortTypeName);
167167

168168
var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext());
169169

@@ -174,7 +174,7 @@ public void StringValuesConvertToTypeFromShortTypeName()
174174
public void StringValuesConvertToTypeFromAssemblyQualifiedName()
175175
{
176176
var assemblyQualifiedName = typeof(Version).AssemblyQualifiedName;
177-
var stringArgumentValue = new StringArgumentValue(() => assemblyQualifiedName);
177+
var stringArgumentValue = new StringArgumentValue(assemblyQualifiedName);
178178

179179
var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext());
180180

test/TestDummies/Console/DummyConsoleSink.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ public DummyConsoleSink(ConsoleTheme theme = null)
1717
public static ConsoleTheme Theme;
1818

1919
[ThreadStatic]
20-
// ReSharper disable ThreadStaticFieldHasInitializer
21-
public static List<LogEvent> Emitted = new List<LogEvent>();
22-
// ReSharper restore ThreadStaticFieldHasInitializer
20+
static List<LogEvent> EmittedList = new List<LogEvent>();
21+
22+
public static List<LogEvent> Emitted => EmittedList ?? (EmittedList = new List<LogEvent>());
2323

2424
public void Emit(LogEvent logEvent)
2525
{

test/TestDummies/DummyLoggerConfigurationExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ public static LoggerConfiguration DummyWithLevelSwitch(
109109
public static LoggerConfiguration DummyConsole(
110110
this LoggerSinkConfiguration loggerSinkConfiguration,
111111
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
112+
LoggingLevelSwitch levelSwitch = null,
112113
ConsoleTheme theme = null)
113114
{
114-
return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel);
115+
return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch);
115116
}
116117

117118
public static LoggerConfiguration Dummy(

0 commit comments

Comments
 (0)