Skip to content

Commit af9efd3

Browse files
authored
Merge pull request #88 from tsimbalar/loglevelswitchsupport
Add support for LoggingLevelSwitch
2 parents 7d1f277 + 7034ca6 commit af9efd3

12 files changed

+973
-49
lines changed

.editorconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
root = true
2+
3+
[*]
4+
trim_trailing_whitespace = true
5+
insert_final_newline = true
6+
indent_style = space
7+
indent_size = 4
8+
9+
[*.{csproj,json,config,yml}]
10+
indent_size = 2
11+
12+
[*.sh]
13+
end_of_line = lf
14+
15+
[*.{cmd, bat}]
16+
end_of_line = crlf

serilog-settings-configuration.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E41FD57-5FA
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B904-1D11-4962-A4A8-DE28672AA28B}"
99
ProjectSection(SolutionItems) = preProject
10+
.editorconfig = .editorconfig
1011
appveyor.yml = appveyor.yml
1112
Build.ps1 = Build.ps1
1213
CHANGES.md = CHANGES.md
1314
LICENSE = LICENSE
1415
README.md = README.md
16+
serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings
1517
EndProjectSection
1618
EndProject
1719
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}"

serilog-settings-configuration.sln.DotSettings

Lines changed: 559 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 108 additions & 33 deletions
Large diffs are not rendered by default.

src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationSectionArgumentValue.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Serilog.Configuration;
22
using System;
3+
using System.Collections.Generic;
34
using System.Reflection;
5+
using Serilog.Core;
46

57
namespace Serilog.Settings.Configuration
68
{
@@ -13,10 +15,10 @@ public ConfigurationSectionArgumentValue(IConfigurationReader configReader)
1315
_configReader = configReader ?? throw new ArgumentNullException(nameof(configReader));
1416
}
1517

16-
public object ConvertTo(Type toType)
18+
public object ConvertTo(Type toType, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
1719
{
1820
var typeInfo = toType.GetTypeInfo();
19-
if (!typeInfo.IsGenericType ||
21+
if (!typeInfo.IsGenericType ||
2022
typeInfo.GetGenericTypeDefinition() is Type genericType && genericType != typeof(Action<>))
2123
{
2224
throw new InvalidOperationException("Argument value should be of type Action<>.");
@@ -25,7 +27,7 @@ public object ConvertTo(Type toType)
2527
var configurationType = typeInfo.GenericTypeArguments[0];
2628
if (configurationType == typeof(LoggerSinkConfiguration))
2729
{
28-
return new Action<LoggerSinkConfiguration>(_configReader.ApplySinks);
30+
return new Action<LoggerSinkConfiguration>(loggerSinkConfig => _configReader.ApplySinks(loggerSinkConfig, declaredLevelSwitches));
2931
}
3032

3133
if (configurationType == typeof(LoggerConfiguration))
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
2+
using System.Collections.Generic;
3+
using Serilog.Core;
24

35
namespace Serilog.Settings.Configuration
46
{
57
interface IConfigurationArgumentValue
68
{
7-
object ConvertTo(Type toType);
9+
object ConvertTo(Type toType, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches);
810
}
911
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
using Serilog.Configuration;
1+
using System.Collections.Generic;
2+
using Serilog.Configuration;
3+
using Serilog.Core;
24

35
namespace Serilog.Settings.Configuration
46
{
57
interface IConfigurationReader : ILoggerSettings
68
{
7-
void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration);
9+
void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches);
810
}
911
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Serilog.Core;
4+
5+
namespace Serilog.Settings.Configuration
6+
{
7+
internal static class LevelSwitchDictionaryExtensions
8+
{
9+
/// <summary>
10+
/// Looks up a switch in the declared LoggingLevelSwitches
11+
/// </summary>
12+
/// <param name="namedLevelSwitches">the dictionary of switches to look up by name</param>
13+
/// <param name="switchName">the name of a switch to look up</param>
14+
/// <returns>the LoggingLevelSwitch registered with the name</returns>
15+
/// <exception cref="InvalidOperationException">if no switch has been registered with <paramref name="switchName"/></exception>
16+
public static LoggingLevelSwitch LookUpSwitchByName(this IReadOnlyDictionary<string, LoggingLevelSwitch> namedLevelSwitches, string switchName)
17+
{
18+
if (namedLevelSwitches == null) throw new ArgumentNullException(nameof(namedLevelSwitches));
19+
if (namedLevelSwitches.TryGetValue(switchName, out var levelSwitch))
20+
{
21+
return levelSwitch;
22+
}
23+
24+
throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}");
25+
}
26+
}
27+
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class StringArgumentValue : IConfigurationArgumentValue
1616
readonly Func<string> _valueProducer;
1717
readonly Func<IChangeToken> _changeTokenProducer;
1818

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

2121
public StringArgumentValue(Func<string> valueProducer, Func<IChangeToken> changeTokenProducer = null)
2222
{
@@ -30,10 +30,15 @@ public StringArgumentValue(Func<string> valueProducer, Func<IChangeToken> change
3030
{ typeof(TimeSpan), s => TimeSpan.Parse(s) }
3131
};
3232

33-
public object ConvertTo(Type toType)
33+
public object ConvertTo(Type toType, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
3434
{
3535
var argumentValue = Environment.ExpandEnvironmentVariables(_valueProducer());
3636

37+
if (toType == typeof(LoggingLevelSwitch))
38+
{
39+
return declaredLevelSwitches.LookUpSwitchByName(argumentValue);
40+
}
41+
3742
var toTypeInfo = toType.GetTypeInfo();
3843
if (toTypeInfo.IsGenericType && toType.GetGenericTypeDefinition() == typeof(Nullable<>))
3944
{

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Xunit;
44
using System.Reflection;
55
using System.Linq;
6+
using Serilog.Core;
67
using Serilog.Settings.Configuration.Tests.Support;
78

89
namespace Serilog.Settings.Configuration.Tests
@@ -70,11 +71,11 @@ public void WriteToSupportExpandedSyntaxWithArgs()
7071

7172
Assert.Equal(1, result["LiterateConsole"].Count());
7273

73-
var args = result["LiterateConsole"].Single().Cast<KeyValuePair<string, IConfigurationArgumentValue>>().ToArray();
74+
var args = result["LiterateConsole"].Single().ToArray();
7475

7576
Assert.Equal(1, args.Length);
7677
Assert.Equal("outputTemplate", args[0].Key);
77-
Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string)));
78+
Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string), new Dictionary<string, LoggingLevelSwitch>()));
7879
}
7980

8081
[Fact]

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

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Microsoft.Extensions.Configuration;
3+
using Serilog.Core;
34
using Serilog.Events;
45
using Serilog.Settings.Configuration.Tests.Support;
56
using TestDummies;
@@ -11,7 +12,7 @@ namespace Serilog.Settings.Configuration.Tests
1112
{
1213
public class ConfigurationSettingsTests
1314
{
14-
private static LoggerConfiguration ConfigFromJson(string jsonString)
15+
static LoggerConfiguration ConfigFromJson(string jsonString)
1516
{
1617
var config = new ConfigurationBuilder().AddJsonString(jsonString).Build();
1718
return new LoggerConfiguration()
@@ -197,5 +198,197 @@ public void SinksAreConfiguredWithStaticMember()
197198

198199
Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme);
199200
}
201+
202+
203+
[Theory]
204+
[InlineData("$switchName", true)]
205+
[InlineData("$SwitchName", true)]
206+
[InlineData("$switch1", true)]
207+
[InlineData("$sw1tch0", true)]
208+
[InlineData("$SWITCHNAME", true)]
209+
[InlineData("$$switchname", false)]
210+
[InlineData("$switchname$", false)]
211+
[InlineData("switch$name", false)]
212+
[InlineData("$", false)]
213+
[InlineData("", false)]
214+
[InlineData(" ", false)]
215+
[InlineData("$1switch", false)]
216+
[InlineData("$switch_name", false)]
217+
public void LoggingLevelSwitchNameValidityScenarios(string switchName, bool expectedValid)
218+
{
219+
Assert.True(ConfigurationReader.IsValidSwitchName(switchName) == expectedValid,
220+
$"expected IsValidSwitchName({switchName}) to return {expectedValid} ");
221+
}
222+
223+
[Fact]
224+
public void LoggingLevelSwitchWithInvalidNameThrowsFormatException()
225+
{
226+
var json = @"{
227+
""Serilog"": {
228+
""LevelSwitches"": {""switchNameNotStartingWithDollar"" : ""Warning"" }
229+
}
230+
}";
231+
232+
var ex = Assert.Throws<FormatException>(() => ConfigFromJson(json));
233+
234+
Assert.Contains("\"switchNameNotStartingWithDollar\"", ex.Message);
235+
Assert.Contains("'$' sign", ex.Message);
236+
Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message);
237+
}
238+
239+
[Fact]
240+
public void LoggingLevelSwitchIsConfigured()
241+
{
242+
var json = @"{
243+
""Serilog"": {
244+
""LevelSwitches"": {""$switch1"" : ""Warning"" },
245+
""MinimumLevel"" : {
246+
""ControlledBy"" : ""$switch1""
247+
}
248+
}
249+
}";
250+
LogEvent evt = null;
251+
252+
var log = ConfigFromJson(json)
253+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
254+
.CreateLogger();
255+
256+
log.Write(Some.DebugEvent());
257+
Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Debug messages");
258+
log.Write(Some.InformationEvent());
259+
Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Information messages");
260+
log.Write(Some.WarningEvent());
261+
Assert.True(evt != null, "LoggingLevelSwitch initial level was Warning. It should log Warning messages");
262+
}
263+
264+
[Fact]
265+
public void SettingMinimumLevelControlledByToAnUndeclaredSwitchThrows()
266+
{
267+
var json = @"{
268+
""Serilog"": {
269+
""LevelSwitches"": {""$switch1"" : ""Warning"" },
270+
""MinimumLevel"" : {
271+
""ControlledBy"" : ""$switch2""
272+
}
273+
}
274+
}";
275+
276+
var ex = Assert.Throws<InvalidOperationException>(() =>
277+
ConfigFromJson(json)
278+
.CreateLogger());
279+
280+
Assert.Contains("$switch2", ex.Message);
281+
Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message);
282+
}
283+
284+
[Fact]
285+
public void LoggingLevelSwitchIsPassedToSinks()
286+
{
287+
var json = @"{
288+
""Serilog"": {
289+
""Using"": [""TestDummies""],
290+
""LevelSwitches"": {""$switch1"" : ""Information"" },
291+
""MinimumLevel"" : {
292+
""ControlledBy"" : ""$switch1""
293+
},
294+
""WriteTo"": [{
295+
""Name"": ""DummyWithLevelSwitch"",
296+
""Args"": {""controlLevelSwitch"" : ""$switch1""}
297+
}]
298+
}
299+
}";
300+
301+
LogEvent evt = null;
302+
303+
var log = ConfigFromJson(json)
304+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
305+
.CreateLogger();
306+
307+
Assert.False(DummyWithLevelSwitchSink.ControlLevelSwitch == null, "Sink ControlLevelSwitch should have been initialized");
308+
309+
var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch;
310+
Assert.NotNull(controlSwitch);
311+
312+
log.Write(Some.DebugEvent());
313+
Assert.True(evt is null, "LoggingLevelSwitch initial level was information. It should not log Debug messages");
314+
315+
controlSwitch.MinimumLevel = LogEventLevel.Debug;
316+
log.Write(Some.DebugEvent());
317+
Assert.True(evt != null, "LoggingLevelSwitch level was changed to Debug. It should log Debug messages");
318+
}
319+
320+
[Fact]
321+
public void ReferencingAnUndeclaredSwitchInSinkThrows()
322+
{
323+
var json = @"{
324+
""Serilog"": {
325+
""Using"": [""TestDummies""],
326+
""LevelSwitches"": {""$switch1"" : ""Information"" },
327+
""MinimumLevel"" : {
328+
""ControlledBy"" : ""$switch1""
329+
},
330+
""WriteTo"": [{
331+
""Name"": ""DummyWithLevelSwitch"",
332+
""Args"": {""controlLevelSwitch"" : ""$switch2""}
333+
}]
334+
}
335+
}";
336+
337+
var ex = Assert.Throws<InvalidOperationException>(() =>
338+
ConfigFromJson(json)
339+
.CreateLogger());
340+
341+
Assert.Contains("$switch2", ex.Message);
342+
Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message);
343+
}
344+
345+
[Fact]
346+
public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides()
347+
{
348+
var json = @"{
349+
""Serilog"": {
350+
""Using"": [""TestDummies""],
351+
""LevelSwitches"": {""$specificSwitch"" : ""Warning"" },
352+
""MinimumLevel"" : {
353+
""Default"" : ""Debug"",
354+
""Override"" : {
355+
""System"" : ""$specificSwitch""
356+
}
357+
},
358+
""WriteTo"": [{
359+
""Name"": ""DummyWithLevelSwitch"",
360+
""Args"": {""controlLevelSwitch"" : ""$specificSwitch""}
361+
}]
362+
}
363+
}";
364+
365+
LogEvent evt = null;
366+
367+
var log = ConfigFromJson(json)
368+
.WriteTo.Sink(new DelegatingSink(e => evt = e))
369+
.CreateLogger();
370+
371+
var systemLogger = log.ForContext(Constants.SourceContextPropertyName, "System.Bar");
372+
373+
log.Write(Some.InformationEvent());
374+
Assert.False(evt is null, "Minimum level is Debug. It should log Information messages");
375+
376+
evt = null;
377+
// ReSharper disable HeuristicUnreachableCode
378+
systemLogger.Write(Some.InformationEvent());
379+
Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should not log Information messages for SourceContext System.Bar");
380+
381+
systemLogger.Write(Some.WarningEvent());
382+
Assert.False(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should log Warning messages for SourceContext System.Bar");
383+
384+
385+
evt = null;
386+
var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch;
387+
388+
controlSwitch.MinimumLevel = LogEventLevel.Information;
389+
systemLogger.Write(Some.InformationEvent());
390+
Assert.False(evt is null, "LoggingLevelSwitch level was changed to Information for logger System.*. It should now log Information events for SourceContext System.Bar.");
391+
// ReSharper restore HeuristicUnreachableCode
392+
}
200393
}
201394
}

0 commit comments

Comments
 (0)