-
Notifications
You must be signed in to change notification settings - Fork 798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for LevelSwith in KeyValuePairSettings #1020
Conversation
key="level-switch:foo" value="Information" will create an instance of LoggingLevelSwitch named foo with initial level Information
…trol MinimumLevel key="minimum-level:controlled-by" value="foo" will set MinimumLevel.ControlledBy with the previously declared named level-switch
KeyValuePairSettings.ParseVariableDeclarationDirectives and KeyValuePairSettings.LookUpVariable
Passing a parameter directive the name of a declared Switch will pass it to the configuration method
… level=information
.WriteTo.Sink(new DelegatingSink(e => evt = e)) | ||
.CreateLogger(); | ||
|
||
// TODO : maybe add Logger.LevelSwitch as an internal readonly property to avoid reflection here ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using reflection to access a private field _levelSwitch
of class Logger
....
I'm not super happy about that, but
- I believe accessing that member is the only way to test that the Level switch is really applied (I have no "pointer" to the actual object because it was defined from config....)
- I was not sure whether it was ok to make the field
internal
to make it visible from the test (or insite aninternal
get-only property)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just infer the level by testing with a couple of different events? May be a bit clunky, but should stay stable over the long term via the public API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that would prove that the initial value for the "level switch" was indeed passed, but not that it was assigned .... but I guess it is covered by the full test-case (passing the switch also to a sink)
if (directives.TryGetValue(MinimumLevelControlledByDirective, out minimumLevelControlledByLevelSwitchName)) | ||
{ | ||
var levelSwitch = LookUpVariable(minimumLevelControlledByLevelSwitchName, typeof(LoggingLevelSwitch), declaredVariables) as LoggingLevelSwitch; | ||
// TODO : maybe throw an exception to notify missing variable declaration ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what is the policy around exception in Serilog.
I know the idea is that a logging library shouldn't crash the app, but is it ok to throw exceptions during configuration, to help the developer fix the incorrect config ?
I believe it would be more user friendly to throw an exception that states what is exactly wrong (undeclared variable / wrong name etc), but I was not sure :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/serilog/serilog/wiki/Reliability should have some clues around this (it's a little vague, but sets out the general idea).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks ! I knew I had read something along those lines somewhere, I just couldn't remember :)
I'll change the code to throw a more explicit exception
@@ -185,6 +252,36 @@ internal static IEnumerable<Assembly> LoadConfigurationAssemblies(Dictionary<str | |||
return configurationAssemblies.Distinct(); | |||
} | |||
|
|||
internal static bool TryInstantiate(Type type, string constructorParamAsString, out object createdInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if I did not over-engineer that part ... Seeing how the "callable directives" relied on reflection to find the proper method to call, I did the same to try and find a constructor for the type we are trying to build .... but considering the only type we need to build is always LoggingLevelSwitch
, that might be overkill ...
If you think this should be simplified in order not to rely on reflection, that's fine, I'll do it :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just doing a quick first-pass over to try to provide some feedback, so haven't give it a huge amount of thought yet, but my instinct says just building something explicit to support LevelSwitch
would be reasonable right now.
We're likely to address general object construction through the requirement that sinks be able to receive complex parameters to their configuration methods - ColumnOptions
for the MSSQL sink, for example. My bet is we'll revisit general construction rules then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, go for YAGNI then :)
I added some comments in places where I was unsure about the proper way of doing things to be consistent with the rest of the code base ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Thibaud! I had a few minutes spare so tried to get some feedback down, apologies if it's a bit of a shallow skim. Had a few queries/suggestions, let me know what you think.
var settings = new Dictionary<string, string> | ||
{ | ||
["level-switch:Switch1"] = "Information", | ||
["minimum-level:controlled-by"] = "Switch1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<add key="serilog:level-switch:Switch1" value="Information" />
contains the same information but also enforces that if a switch is declared, we set its level. Separating the two means we can declare a switch without using it, or try to use a nonexistent switch. Do we gain anything by splitting the declaration and usage across two directives?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not sure I understand your comment U_U
I just based my implementation on your proposal from here : serilog/serilog-settings-appsettings#7 (comment)
The switch needs to be used twice, both for the :
.MinimumLevel.ControlledBy(LoggingLevelSwitch ls)
.WriteTo.Seq( LoggingLevelSwitch controlLevelSwitch)
... so we need to find a way to express that both parameters refer to the same instance of a LoggingSwitch , or did I miss something ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, yes, you're entirely correct, I misread the code in my attempt to skim through quickly.
if (directives.TryGetValue(MinimumLevelControlledByDirective, out minimumLevelControlledByLevelSwitchName)) | ||
{ | ||
var levelSwitch = LookUpVariable(minimumLevelControlledByLevelSwitchName, typeof(LoggingLevelSwitch), declaredVariables) as LoggingLevelSwitch; | ||
// TODO : maybe throw an exception to notify missing variable declaration ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/serilog/serilog/wiki/Reliability should have some clues around this (it's a little vague, but sets out the general idea).
@@ -185,6 +252,36 @@ internal static IEnumerable<Assembly> LoadConfigurationAssemblies(Dictionary<str | |||
return configurationAssemblies.Distinct(); | |||
} | |||
|
|||
internal static bool TryInstantiate(Type type, string constructorParamAsString, out object createdInstance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just doing a quick first-pass over to try to provide some feedback, so haven't give it a huge amount of thought yet, but my instinct says just building something explicit to support LevelSwitch
would be reasonable right now.
We're likely to address general object construction through the requirement that sinks be able to receive complex parameters to their configuration methods - ColumnOptions
for the MSSQL sink, for example. My bet is we'll revisit general construction rules then.
.WriteTo.Sink(new DelegatingSink(e => evt = e)) | ||
.CreateLogger(); | ||
|
||
// TODO : maybe add Logger.LevelSwitch as an internal readonly property to avoid reflection here ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just infer the level by testing with a couple of different events? May be a bit clunky, but should stay stable over the long term via the public API.
…tle private field
+ throw a "nicer" error message when referencing an undeclared LoggingLevelSwitch
I think I'm done with this PR. |
… override key="serilog:minimum-level:override:System" value ="switchName" will bind the override to a switch that may be controlled through the sink
I have added code to handle referencing a |
Nice catch on the This does have me thinking, when I see:
Alongside:
I'm reminded again of the possibility of disambiguating by forcing some kind of 'variable reference'-style syntax:
I don't want to take the discussion around in circles :-), but what are your thoughts about this case? |
I do like that there is an indication that it is a reference to a variable :) . This also help explicitly look up a value by name instead of first trying to look up a value by name and then fall back to conversion. For consistence, I would also add a specific symbol when declaring the variable ( If we do not want to go too deep into variables and stuff like that and really just support
<add key="serilog:level-switch:$systemSwitch" value="Warning" />
(I think we still need to fallback to conversion because of possible Sink configuration methods that would accept a string, and in which the string To support this, the only change needed is to change the regex for switch-level declarations, the rest would work as is, I believe. This allows to support the use case for now, without closing too many doors for a possible more generic implementation in the future. The final syntax would then be : <!-- declare the switch -->
<add key="serilog:level-switch:$Switch1" value="Debug" />
<!-- use it to control the root logger -->
<add key="serilog:minimum-level:controlled-by" value="$Switch1" />
<add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
<add key="serilog:write-to:Seq.serverUrl" value="http://localhost:5341" />
<!-- give the sink access to the switch -->
<add key="serilog:write-to:Seq.controlLevelSwitch" value="$Switch1" /> and for the overrides <add key="serilog:level-switch:$systemSwitch" value="Warning" />
<add key="serilog:minimum-level:override:System" value="$systemSwitch" />
<add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
<add key="serilog:write-to:Seq.serverUrl" value="http://localhost:5341" />
<!-- give the sink access to the switch -->
<add key="serilog:write-to:Seq.controlLevelSwitch" value="$systemSwitch" /> |
@tsimbalar that looks good to me 👍 (sorry about the slow reply). |
@nblumhardt no problem ;) OK, I'll change the regex to accept only switches in the format |
... and use $ also when referencing the declared swith This helps disambiguate between a variable and the minimum level for a switch ... and makes sure noone is goind to declared a switch with name "Information"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the way to go 👍
I took the liberty of nitpicking a bit, since I think the design is solid - keen to have your thoughts on these minor points but otherwise ready to
@@ -81,6 +88,9 @@ public void Configure(LoggerConfiguration loggerConfiguration) | |||
.Where(k => _supportedDirectives.Any(k.StartsWith)) | |||
.ToDictionary(k => k, k => _settings[k]); | |||
|
|||
var declaredLevelSwitches = ParseNamedLevelSwitchDeclarationDirectives(directives); | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: extra whitespace
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
your OCD might be worse than mine ! ;)
@@ -37,13 +41,16 @@ class KeyValuePairSettings : ILoggerSettings | |||
const string MinimumLevelOverrideDirectivePrefix = "minimum-level:override:"; | |||
|
|||
const string CallableDirectiveRegex = @"^(?<directive>audit-to|write-to|enrich|filter):(?<method>[A-Za-z0-9]*)(\.(?<argument>[A-Za-z0-9]*)){0,1}$"; | |||
const string LevelSwitchDeclarationDirectiveRegex = @"^level-switch:(?<switchName>\$[A-Za-z0-9]+)$"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of enforcing the presence of $
here, would it be friendlier to accept anything, but emit a warning or throw when the directive is found but the name is invalid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that would be friendlier indeed :)
I would say "throw" in here, the same way that right now for instance setting an invalid url in the Seq Sink throws a FormatException somewhere down the line.
I think throwing a FormatException
with a detailed message would make sense. I don't think creating a custom exception type would add any value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed 👍
var initialLevel = (LogEventLevel)SettingValueConversions.ConvertToType(switchDeclarationDirective.InitialSwitchLevel, typeof(LogEventLevel)); | ||
newSwitch = new LoggingLevelSwitch(initialLevel); | ||
} | ||
namedSwitches[switchDeclarationDirective.SwitchName] = newSwitch; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we assume keys are unique, and so use namedSwitches.Add()
here to express/enforce that expectation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense
|
||
static LoggingLevelSwitch LookUpSwitchByNameOrThrow(string minimumLevelControlledByLevelSwitchName, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches) | ||
{ | ||
if (!declaredLevelSwitches.ContainsKey(minimumLevelControlledByLevelSwitchName)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about:
if (declaredLevelSwitches.TryGetValue(minimumLevelControlledByLevelSwitchName, out var levelSwitch)
return levelSwitch;
throw new InvalidOperationException($"No `LoggingLevelSwitch` has been declared with name \"{minimumLevelControlledByLevelSwitchName}\". You might be missing a key \"{LevelSwitchDirective}:{minimumLevelControlledByLevelSwitchName}\"");
return new ReadOnlyDictionary<string, LoggingLevelSwitch>(namedSwitches); | ||
} | ||
|
||
static LoggingLevelSwitch LookUpSwitchByNameOrThrow(string minimumLevelControlledByLevelSwitchName, IReadOnlyDictionary<string, LoggingLevelSwitch> declaredLevelSwitches) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just call the argument switchName
? The result might not always be used in a minimum-level:controlled-by
directive, so the name might be a bit too specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep :) That's a leftover of R# Extract Method ;)
{ | ||
if (type == typeof(LoggingLevelSwitch)) | ||
{ | ||
return LookUpSwitchByNameOrThrow(valueOrSwitchName, declaredSwitches); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming scheme here isn't consistent - since ConvertOrLookupByName()
could throw, I'd expect it to also carry the OrThrow()
suffix if we're using that convention. Perhaps we could drop the OrThrow()
from LookUpSwitchByNameOrThrow()
, with the intention that we'd provide a TryXxxx()
variant for the non-throwing version?
Thanks for the review, I'll fix'em when I get a moment. |
…witch with a switch name not starting with a $ sign
use Dictionary.Add() instead of Dictionary[] to make it mor explicit that uniqueness is implied
I've updated my PR to include the suggested changes. Please tell me if I need to do anything else (for instance, should I squash / rebase my commits, or is it ok to see the sausage-making ?) |
Squashed and merged, thanks! |
* add support for declaring a named LoggingLevelSwitch key="level-switch:foo" value="Information" will create an instance of LoggingLevelSwitch named foo with initial level Information * add support for using a previously declared LoggingLevelSwitch to control MinimumLevel key="minimum-level:controlled-by" value="foo" will set MinimumLevel.ControlledBy with the previously declared named level-switch * [WIP] unit test for passing controlLevelSwitch to a sink * [Refactoring] extracted methods to make it clearer KeyValuePairSettings.ParseVariableDeclarationDirectives and KeyValuePairSettings.LookUpVariable * add the possibility to pass a declared LoggingLevelSwitch to a sink Passing a parameter directive the name of a declared Switch will pass it to the configuration method * add support for level-switch:Switch1 - value = "" and interpret it as level=information * Minor edits/typos in the unit tests * Change brittle tests in order not to rely on reflection to acces brittle private field * Simplify the code around "creating a new LoggingLevelSwitch" + throw a "nicer" error message when referencing an undeclared LoggingLevelSwitch * Add support for referencing a LoggingLevelSwitch from a minimul level override key="serilog:minimum-level:override:System" value ="switchName" will bind the override to a switch that may be controlled through the sink * Enforce declaration of switch as level-switch:$switchName ... and use $ also when referencing the declared swith This helps disambiguate between a variable and the minimum level for a switch ... and makes sure noone is goind to declared a switch with name "Information" * Minor tweaks after review * Explicitly throw a helpful FormatException when specifiying a level-switch with a switch name not starting with a $ sign * Minor refactoring of ParseNamedLevelSwitchDeclarationDirectives use Dictionary.Add() instead of Dictionary[] to make it mor explicit that uniqueness is implied * Refactoring : use TryGetValue instead of ContainsKey * Refactoring : renamed LookUpSwitchByNameOrThrow to LookUpSwitchByName for consistency
In order to support serilog/serilog-settings-appsettings#7