Skip to content

Commit 62acc84

Browse files
authored
Merge pull request #100 from MV10/iconfig_parameter
Fixes #97, #83, #98
2 parents f396e87 + 0116625 commit 62acc84

File tree

10 files changed

+319
-107
lines changed

10 files changed

+319
-107
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,39 @@ For example, to set the minimum log level using the _Windows_ command prompt:
9191
set Serilog:MinimumLevel=Debug
9292
dotnet run
9393
```
94+
95+
### Nested configuration sections
96+
97+
Some Serilog packages require a reference to a logger configuration object. The sample program in this project illustrates this with the following entry configuring the _Serilog.Sinks.Async_ package to wrap the _Serilog.Sinks.File_ package. The `configure` parameter references the File sink configuration:
98+
99+
```json
100+
"WriteTo:Async": {
101+
"Name": "Async",
102+
"Args": {
103+
"configure": [
104+
{
105+
"Name": "File",
106+
"Args": {
107+
"path": "%TEMP%\\Logs\\serilog-configuration-sample.txt",
108+
"outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"
109+
}
110+
}
111+
]
112+
}
113+
},
114+
```
115+
116+
### IConfiguration parameter
117+
118+
If a Serilog package requires additional external configuration information (for example, access to a `ConnectionStrings` section, which would be outside of the `Serilog` section), the sink should include an `IConfiguration` parameter in the configuration extension method. This package will automatically populate that parameter. It should not be declared in the argument list in the configuration source.
119+
120+
### Complex parameter value binding
121+
122+
When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri` and `TimeSpan` objects and `enum` elements.
123+
124+
If the parameter value is not a discrete value, the package will use the configuration binding system provided by _Microsoft.Extensions.Options.ConfigurationExtensions_ to attempt to populate the parameter. Almost anything that can be bound by `IConfiguration.Get<T>` should work with this package. An example of this is the optional `List<Column>` parameter used to configure the .NET Standard version of the _Serilog.Sinks.MSSqlServer_ package.
125+
126+
### IConfigurationSection parameters
127+
128+
Certain Serilog packages may require configuration information that can't be easily represented by discrete values or direct binding-friendly representations. An example might be lists of values to remove from a collection of default values. In this case the method can accept an entire `IConfigurationSection` as a call parameter and this package will recognize that and populate the parameter. In this way, Serilog packages can support arbitrarily complex configuration scenarios.
129+

src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@ namespace Serilog
2626
/// </summary>
2727
public static class ConfigurationLoggerConfigurationExtensions
2828
{
29-
const string DefaultSectionName = "Serilog";
29+
/// <summary>
30+
/// Configuration section name required by this package.
31+
/// </summary>
32+
public const string DefaultSectionName = "Serilog";
3033

3134
/// <summary>
32-
/// Reads logger settings from the provided configuration object using the default section name.
35+
/// Reads logger settings from the provided configuration object using the default section name. Generally this
36+
/// is preferable over the other method that takes a configuration section. Only this version will populate
37+
/// IConfiguration parameters on target methods.
3338
/// </summary>
3439
/// <param name="settingConfiguration">Logger setting configuration.</param>
35-
/// <param name="configuration">A configuration object with a Serilog section.</param>
40+
/// <param name="configuration">A configuration object which contains a Serilog section.</param>
3641
/// <param name="dependencyContext">The dependency context from which sink/enricher packages can be located. If not supplied, the platform
3742
/// default will be used.</param>
3843
/// <returns>An object allowing configuration to continue.</returns>
@@ -42,28 +47,32 @@ public static LoggerConfiguration Configuration(
4247
DependencyContext dependencyContext = null)
4348
{
4449
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
45-
return settingConfiguration.ConfigurationSection(configuration.GetSection(DefaultSectionName), dependencyContext);
50+
return settingConfiguration.Settings(
51+
new ConfigurationReader(
52+
configuration,
53+
dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null)));
4654
}
4755

4856
/// <summary>
49-
/// Reads logger settings from the provided configuration section.
57+
/// Reads logger settings from the provided configuration section. Generally it is preferable to use the other
58+
/// extension method that takes the full configuration object.
5059
/// </summary>
5160
/// <param name="settingConfiguration">Logger setting configuration.</param>
52-
/// <param name="configuration">The Serilog configuration section</param>
61+
/// <param name="configSection">The Serilog configuration section</param>
5362
/// <param name="dependencyContext">The dependency context from which sink/enricher packages can be located. If not supplied, the platform
5463
/// default will be used.</param>
5564
/// <returns>An object allowing configuration to continue.</returns>
5665
public static LoggerConfiguration ConfigurationSection(
5766
this LoggerSettingsConfiguration settingConfiguration,
58-
IConfigurationSection configuration,
67+
IConfigurationSection configSection,
5968
DependencyContext dependencyContext = null)
6069
{
6170
if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration));
62-
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
71+
if (configSection == null) throw new ArgumentNullException(nameof(configSection));
6372

6473
return settingConfiguration.Settings(
6574
new ConfigurationReader(
66-
configuration,
75+
configSection,
6776
dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null)));
6877
}
6978
}

src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626

2727
<ItemGroup Condition="'$(TargetFramework)' == 'net451'">
2828
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="1.1.2" />
29+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
2930
</ItemGroup>
3031

3132
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
3233
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.1" />
34+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0" />
3335
</ItemGroup>
3436

3537
</Project>

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

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,33 @@ class ConfigurationReader : IConfigurationReader
2020
{
2121
const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$";
2222

23-
readonly IConfigurationSection _configuration;
23+
static IConfiguration _configuration;
24+
25+
readonly IConfigurationSection _section;
2426
readonly DependencyContext _dependencyContext;
2527
readonly IReadOnlyCollection<Assembly> _configurationAssemblies;
2628

27-
public ConfigurationReader(IConfigurationSection configuration, DependencyContext dependencyContext)
29+
public ConfigurationReader(IConfiguration configuration, DependencyContext dependencyContext)
2830
{
2931
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
32+
_section = configuration.GetSection(ConfigurationLoggerConfigurationExtensions.DefaultSectionName);
3033
_dependencyContext = dependencyContext;
3134
_configurationAssemblies = LoadConfigurationAssemblies();
3235
}
3336

34-
ConfigurationReader(IConfigurationSection configuration, IReadOnlyCollection<Assembly> configurationAssemblies, DependencyContext dependencyContext)
37+
// Generally the initial call should use IConfiguration rather than IConfigurationSection, otherwise
38+
// IConfiguration parameters in the target methods will not be populated.
39+
ConfigurationReader(IConfigurationSection configSection, DependencyContext dependencyContext)
3540
{
36-
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
41+
_section = configSection ?? throw new ArgumentNullException(nameof(configSection));
42+
_dependencyContext = dependencyContext;
43+
_configurationAssemblies = LoadConfigurationAssemblies();
44+
}
45+
46+
// Used internally for processing nested configuration sections -- see GetMethodCalls below.
47+
internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyCollection<Assembly> configurationAssemblies, DependencyContext dependencyContext)
48+
{
49+
_section = configSection ?? throw new ArgumentNullException(nameof(configSection));
3750
_dependencyContext = dependencyContext;
3851
_configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies));
3952
}
@@ -50,40 +63,35 @@ public void Configure(LoggerConfiguration loggerConfiguration)
5063

5164
IReadOnlyDictionary<string, LoggingLevelSwitch> ProcessLevelSwitchDeclarations()
5265
{
53-
var levelSwitchesDirective = _configuration.GetSection("LevelSwitches");
66+
var levelSwitchesDirective = _section.GetSection("LevelSwitches");
5467
var namedSwitches = new Dictionary<string, LoggingLevelSwitch>();
55-
if (levelSwitchesDirective != null)
68+
foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren())
5669
{
57-
foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren())
70+
var switchName = levelSwitchDeclaration.Key;
71+
var switchInitialLevel = levelSwitchDeclaration.Value;
72+
// switchName must be something like $switch to avoid ambiguities
73+
if (!IsValidSwitchName(switchName))
5874
{
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);
75+
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\"}}");
7776
}
77+
LoggingLevelSwitch newSwitch;
78+
if (string.IsNullOrEmpty(switchInitialLevel))
79+
{
80+
newSwitch = new LoggingLevelSwitch();
81+
}
82+
else
83+
{
84+
var initialLevel = ParseLogEventLevel(switchInitialLevel);
85+
newSwitch = new LoggingLevelSwitch(initialLevel);
86+
}
87+
namedSwitches.Add(switchName, newSwitch);
7888
}
79-
8089
return namedSwitches;
8190
}
8291

83-
void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration,
84-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
92+
void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
8593
{
86-
var minimumLevelDirective = _configuration.GetSection("MinimumLevel");
94+
var minimumLevelDirective = _section.GetSection("MinimumLevel");
8795

8896
var defaultMinLevelDirective = minimumLevelDirective.Value != null ? minimumLevelDirective : minimumLevelDirective.GetSection("Default");
8997
if (defaultMinLevelDirective.Value != null)
@@ -92,7 +100,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration,
92100
}
93101

94102
var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy");
95-
if (minLevelControlledByDirective?.Value != null)
103+
if (minLevelControlledByDirective.Value != null)
96104
{
97105
var globalMinimumLevelSwitch = declaredLevelSwitches.LookUpSwitchByName(minLevelControlledByDirective.Value);
98106
// not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already
@@ -134,60 +142,53 @@ void ApplyMinimumLevel(IConfigurationSection directive, Action<LoggerMinimumLeve
134142
}
135143
}
136144

137-
138-
139-
void ApplyFilters(LoggerConfiguration loggerConfiguration,
140-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
145+
void ApplyFilters(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
141146
{
142-
var filterDirective = _configuration.GetSection("Filter");
143-
if (filterDirective != null)
147+
var filterDirective = _section.GetSection("Filter");
148+
if (filterDirective.GetChildren().Any())
144149
{
145150
var methodCalls = GetMethodCalls(filterDirective);
146151
CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter, declaredLevelSwitches);
147152
}
148153
}
149154

150-
void ApplySinks(LoggerConfiguration loggerConfiguration,
151-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
155+
void ApplySinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
152156
{
153-
var writeToDirective = _configuration.GetSection("WriteTo");
154-
if (writeToDirective != null)
157+
var writeToDirective = _section.GetSection("WriteTo");
158+
if (writeToDirective.GetChildren().Any())
155159
{
156160
var methodCalls = GetMethodCalls(writeToDirective);
157161
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo, declaredLevelSwitches);
158162
}
159163
}
160164

161-
void ApplyAuditSinks(LoggerConfiguration loggerConfiguration,
162-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
165+
void ApplyAuditSinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
163166
{
164-
var auditToDirective = _configuration.GetSection("AuditTo");
165-
if (auditToDirective != null)
167+
var auditToDirective = _section.GetSection("AuditTo");
168+
if (auditToDirective.GetChildren().Any())
166169
{
167170
var methodCalls = GetMethodCalls(auditToDirective);
168171
CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo, declaredLevelSwitches);
169172
}
170173
}
171174

172-
void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration,
173-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
175+
void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
174176
{
175-
var methodCalls = GetMethodCalls(_configuration);
177+
var methodCalls = GetMethodCalls(_section);
176178
CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration, declaredLevelSwitches);
177179
}
178180

179-
void ApplyEnrichment(LoggerConfiguration loggerConfiguration,
180-
IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
181+
void ApplyEnrichment(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches)
181182
{
182-
var enrichDirective = _configuration.GetSection("Enrich");
183-
if (enrichDirective != null)
183+
var enrichDirective = _section.GetSection("Enrich");
184+
if (enrichDirective.GetChildren().Any())
184185
{
185186
var methodCalls = GetMethodCalls(enrichDirective);
186187
CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich, declaredLevelSwitches);
187188
}
188189

189-
var propertiesDirective = _configuration.GetSection("Properties");
190-
if (propertiesDirective != null)
190+
var propertiesDirective = _section.GetSection("Properties");
191+
if (propertiesDirective.GetChildren().Any())
191192
{
192193
foreach (var enrichProperyDirective in propertiesDirective.GetChildren())
193194
{
@@ -226,7 +227,7 @@ IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSecti
226227
}
227228
else
228229
{
229-
argumentValue = new ConfigurationSectionArgumentValue(new ConfigurationReader(argumentSection, _configurationAssemblies, _dependencyContext));
230+
argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies, _dependencyContext);
230231
}
231232

232233
return argumentValue;
@@ -246,8 +247,8 @@ IReadOnlyCollection<Assembly> LoadConfigurationAssemblies()
246247
{
247248
var assemblies = new Dictionary<string, Assembly>();
248249

249-
var usingSection = _configuration.GetSection("Using");
250-
if (usingSection != null)
250+
var usingSection = _section.GetSection("Using");
251+
if (usingSection.GetChildren().Any())
251252
{
252253
foreach (var simpleName in usingSection.GetChildren().Select(c => c.Value))
253254
{
@@ -307,6 +308,9 @@ static void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigu
307308
let directive = method.Value.FirstOrDefault(s => s.Key == p.Name)
308309
select directive.Key == null ? p.DefaultValue : directive.Value.ConvertTo(p.ParameterType, declaredLevelSwitches)).ToList();
309310

311+
var parm = methodInfo.GetParameters().FirstOrDefault(i => i.ParameterType == typeof(IConfiguration));
312+
if(parm != null) call[parm.Position - 1] = _configuration;
313+
310314
call.Insert(0, receiver);
311315

312316
methodInfo.Invoke(null, call.ToArray());
@@ -399,5 +403,6 @@ internal static LogEventLevel ParseLogEventLevel(string value)
399403
throw new InvalidOperationException($"The value {value} is not a valid Serilog level.");
400404
return parsedLevel;
401405
}
406+
402407
}
403408
}

0 commit comments

Comments
 (0)