Skip to content

Commit 12f563e

Browse files
author
Thibaud DESODT
committed
Add support for LoggingLevelSwitch
solves #69 Supports : - declaration of LoggingLevelSwitch - MinimumLevel.ControlledBy(switch) - MinimumLevel.Override(prefix, switch) - passing a switch by reference as the argument to a Sink (or any callable method)
1 parent 7d1f277 commit 12f563e

9 files changed

+385
-36
lines changed

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

+97-23
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
using Serilog.Debugging;
1313
using Serilog.Events;
1414
using System.Linq.Expressions;
15+
using System.Text.RegularExpressions;
1516

1617
namespace Serilog.Settings.Configuration
1718
{
1819
class ConfigurationReader : IConfigurationReader
1920
{
21+
const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$";
22+
2023
readonly IConfigurationSection _configuration;
2124
readonly DependencyContext _dependencyContext;
2225
readonly Assembly[] _configurationAssemblies;
@@ -37,14 +40,48 @@ public ConfigurationReader(IConfigurationSection configuration, DependencyContex
3740

3841
public void Configure(LoggerConfiguration loggerConfiguration)
3942
{
40-
ApplyMinimumLevel(loggerConfiguration);
41-
ApplyEnrichment(loggerConfiguration);
42-
ApplyFilters(loggerConfiguration);
43-
ApplySinks(loggerConfiguration);
44-
ApplyAuditSinks(loggerConfiguration);
45-
}
46-
47-
void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
43+
var declaredLevelSwitches = ProcessLevelSwitchDeclarations();
44+
ApplyMinimumLevel(loggerConfiguration, declaredLevelSwitches);
45+
ApplyEnrichment(loggerConfiguration, declaredLevelSwitches);
46+
ApplyFilters(loggerConfiguration, declaredLevelSwitches);
47+
ApplySinks(loggerConfiguration, declaredLevelSwitches);
48+
ApplyAuditSinks(loggerConfiguration, declaredLevelSwitches);
49+
}
50+
51+
private IReadOnlyDictionary<string, LoggingLevelSwitch> ProcessLevelSwitchDeclarations()
52+
{
53+
var levelSwitchesDirective = _configuration.GetSection("LevelSwitches");
54+
var namedSwitches = new Dictionary<string, LoggingLevelSwitch>();
55+
if (levelSwitchesDirective != null)
56+
{
57+
foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren())
58+
{
59+
var switchName = levelSwitchDeclaration.Key;
60+
var switchInitialLevel = levelSwitchDeclaration.Value;
61+
// switchName must be something like $switch to avoid ambiguities
62+
if (!IsValidSwitchName(switchName))
63+
{
64+
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\"}}");
65+
}
66+
LoggingLevelSwitch newSwitch;
67+
if (string.IsNullOrEmpty(switchInitialLevel))
68+
{
69+
newSwitch = new LoggingLevelSwitch();
70+
}
71+
else
72+
{
73+
var initialLevel = ParseLogEventLevel(switchInitialLevel);
74+
newSwitch = new LoggingLevelSwitch(initialLevel);
75+
}
76+
namedSwitches.Add(switchName, newSwitch);
77+
}
78+
}
79+
80+
return namedSwitches;
81+
}
82+
83+
void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration,
84+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
4885
{
4986
var minimumLevelDirective = _configuration.GetSection("MinimumLevel");
5087

@@ -54,15 +91,33 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration)
5491
ApplyMinimumLevel(defaultMinLevelDirective, (configuration, levelSwitch) => configuration.ControlledBy(levelSwitch));
5592
}
5693

94+
var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy");
95+
if (minLevelControlledByDirective?.Value != null)
96+
{
97+
var globalMinimumLevelSwitch = declaredLevelSwitches.LookUpSwitchByName(minLevelControlledByDirective.Value);
98+
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
99+
loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch);
100+
}
101+
57102
foreach (var overrideDirective in minimumLevelDirective.GetSection("Override").GetChildren())
58103
{
59-
ApplyMinimumLevel(overrideDirective, (configuration, levelSwitch) => configuration.Override(overrideDirective.Key, levelSwitch));
104+
var overridePrefix = overrideDirective.Key;
105+
var overridenLevelOrSwitch = overrideDirective.Value;
106+
if (Enum.TryParse(overridenLevelOrSwitch, out LogEventLevel _))
107+
{
108+
ApplyMinimumLevel(overrideDirective, (configuration, levelSwitch) => configuration.Override(overridePrefix, levelSwitch));
109+
}
110+
else
111+
{
112+
var overrideSwitch = declaredLevelSwitches.LookUpSwitchByName(overridenLevelOrSwitch);
113+
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
114+
loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch);
115+
}
60116
}
61117

62118
void ApplyMinimumLevel(IConfigurationSection directive, Action<LoggerMinimumLevelConfiguration, LoggingLevelSwitch> applyConfigAction)
63119
{
64-
if (!Enum.TryParse(directive.Value, out LogEventLevel minimumLevel))
65-
throw new InvalidOperationException($"The value {directive.Value} is not a valid Serilog level.");
120+
var minimumLevel = ParseLogEventLevel(directive.Value);
66121

67122
var levelSwitch = new LoggingLevelSwitch(minimumLevel);
68123
applyConfigAction(loggerConfiguration.MinimumLevel, levelSwitch);
@@ -79,49 +134,56 @@ void ApplyMinimumLevel(IConfigurationSection directive, Action<LoggerMinimumLeve
79134
}
80135
}
81136

82-
void ApplyFilters(LoggerConfiguration loggerConfiguration)
137+
138+
139+
void ApplyFilters(LoggerConfiguration loggerConfiguration,
140+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
83141
{
84142
var filterDirective = _configuration.GetSection("Filter");
85143
if (filterDirective != null)
86144
{
87145
var methodCalls = GetMethodCalls(filterDirective);
88-
CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter);
146+
CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter, declaredLevelSwitches);
89147
}
90148
}
91149

92-
void ApplySinks(LoggerConfiguration loggerConfiguration)
150+
void ApplySinks(LoggerConfiguration loggerConfiguration,
151+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
93152
{
94153
var writeToDirective = _configuration.GetSection("WriteTo");
95154
if (writeToDirective != null)
96155
{
97156
var methodCalls = GetMethodCalls(writeToDirective);
98-
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo);
157+
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo, declaredLevelSwitches);
99158
}
100159
}
101160

102-
void ApplyAuditSinks(LoggerConfiguration loggerConfiguration)
161+
void ApplyAuditSinks(LoggerConfiguration loggerConfiguration,
162+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
103163
{
104164
var auditToDirective = _configuration.GetSection("AuditTo");
105165
if (auditToDirective != null)
106166
{
107167
var methodCalls = GetMethodCalls(auditToDirective);
108-
CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo);
168+
CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo, declaredLevelSwitches);
109169
}
110170
}
111171

112-
void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration)
172+
void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration,
173+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
113174
{
114175
var methodCalls = GetMethodCalls(_configuration);
115-
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration);
176+
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration, declaredLevelSwitches);
116177
}
117178

118-
void ApplyEnrichment(LoggerConfiguration loggerConfiguration)
179+
void ApplyEnrichment(LoggerConfiguration loggerConfiguration,
180+
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
119181
{
120182
var enrichDirective = _configuration.GetSection("Enrich");
121183
if (enrichDirective != null)
122184
{
123185
var methodCalls = GetMethodCalls(enrichDirective);
124-
CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich);
186+
CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich, declaredLevelSwitches);
125187
}
126188

127189
var propertiesDirective = _configuration.GetSection("Properties");
@@ -234,7 +296,7 @@ where filter(assemblyFileName)
234296
return query.ToArray();
235297
}
236298

237-
static void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigurationArgumentValue>> methods, IList<MethodInfo> configurationMethods, object receiver)
299+
static void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigurationArgumentValue>> methods, IList<MethodInfo> configurationMethods, object receiver, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
238300
{
239301
foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x })))
240302
{
@@ -244,7 +306,7 @@ static void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigu
244306
{
245307
var call = (from p in methodInfo.GetParameters().Skip(1)
246308
let directive = method.Value.FirstOrDefault(s => s.Key == p.Name)
247-
select directive.Key == null ? p.DefaultValue : directive.Value.ConvertTo(p.ParameterType)).ToList();
309+
select directive.Key == null ? p.DefaultValue : directive.Value.ConvertTo(p.ParameterType, declaredLevelSwitches)).ToList();
248310

249311
call.Insert(0, receiver);
250312

@@ -322,5 +384,17 @@ internal static LoggerConfiguration Logger(LoggerSinkConfiguration loggerSinkCon
322384

323385
internal static MethodInfo GetSurrogateConfigurationMethod<TConfiguration, TArg1, TArg2>(Expression<Action<TConfiguration, TArg1, TArg2>> method)
324386
=> (method.Body as MethodCallExpression)?.Method;
387+
388+
internal static bool IsValidSwitchName(string input)
389+
{
390+
return Regex.IsMatch(input, LevelSwitchNameRegex);
391+
}
392+
393+
internal static LogEventLevel ParseLogEventLevel(string value)
394+
{
395+
if (!Enum.TryParse(value, out LogEventLevel parsedLevel))
396+
throw new InvalidOperationException($"The value {value} is not a valid Serilog level.");
397+
return parsedLevel;
398+
}
325399
}
326400
}

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

+5-3
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))
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
}
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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Serilog.Core;
7+
8+
namespace Serilog.Settings.Configuration
9+
{
10+
internal static class LevelSwitchDictionaryExtensions
11+
{
12+
/// <summary>
13+
/// Looks up a switch in the declared LoggingLevelSwitches
14+
/// </summary>
15+
/// <param name="namedLevelSwitches">the dictionary of switches to look up by name</param>
16+
/// <param name="switchName">the name of a switch to look up</param>
17+
/// <returns>the LoggingLevelSwitch registered with the name</returns>
18+
/// <exception cref="InvalidOperationException">if no switch has been registered with <paramref name="switchName"/></exception>
19+
public static LoggingLevelSwitch LookUpSwitchByName(this IReadOnlyDictionary<string, LoggingLevelSwitch> namedLevelSwitches, string switchName)
20+
{
21+
if (namedLevelSwitches == null) throw new ArgumentNullException(nameof(namedLevelSwitches));
22+
if (namedLevelSwitches.TryGetValue(switchName, out var levelSwitch))
23+
{
24+
return levelSwitch;
25+
}
26+
27+
throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}");
28+
}
29+
}
30+
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -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

+2-1
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
@@ -74,7 +75,7 @@ public void WriteToSupportExpandedSyntaxWithArgs()
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]

0 commit comments

Comments
 (0)