Skip to content

Conversation

tillig
Copy link
Contributor

@tillig tillig commented Apr 5, 2024

Fix #1944.

Changes LevelOverrideMap.GetEffectiveLevel() to use a case-insensitive comparison for context names, correcting some challenges with setting overrides through mechanisms like environment variables where ALL_CAPS may be used.

@nblumhardt
Copy link
Member

@serilog/maintainers I'm on board with this one but suspect it needs another set of eyes on it, if just to spot other downstream places this could break. I'll merge in a week otherwise :-)

Thanks @tillig!

@@ -68,7 +68,7 @@ public LevelOverride(string context, LoggingLevelSwitch levelSwitch)
{
foreach (var levelOverride in _overrides)
{
if (context.StartsWith(levelOverride.Context) &&
if (context.StartsWith(levelOverride.Context, StringComparison.OrdinalIgnoreCase) &&
(context.Length == levelOverride.Context.Length || context[levelOverride.Context.Length] == '.'))
Copy link
Member

@bartelink bartelink Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely the second part of this condition is more selective and cheaper to compute ?
i.e.

            var oLen = levelOverride.Context.Length;
            if ((context.Length == oLen || (context.Length > oLen && context[oLen] == '.'))
                && context.StartsWith(levelOverride.Context, StringComparison.OrdinalIgnoreCase))

(my C# is too rusty to convert that to an is expression here!)
Also maybe using context.Equals(levelOverride.Context, StringComparison.OrdinalIgnoreCase) when the lens are equal might be cheaper (though I'm guessing startswith probably special cases that internally already)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, good call @bartelink. The clause would need a slight modification:

            if (context.Length >= levelOverride.Context.Length &&
                (context.Length == levelOverride.Context.Length || context[levelOverride.Context.Length] == '.') &&
                context.StartsWith(levelOverride.Context, StringComparison.OrdinalIgnoreCase))

@tillig I wonder if this would be enough to claw back a few % of the performance regression related to the case-sensitivity check? I'll dig in and take a look when I have a chance, if you don't beat me to it :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm out of town for a week or so, so it'll be a bit before I'm back here. Happy to make the change, just will be a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm back and got this. Sorry for the delay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. It appears swapping those has started causing tests to fail. I'll have to look into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm dumb, I missed the thing @nblumhardt mentioned.

I split the == and > so in the full match scenario it should be just slightly faster since it won't have to check for equality again. I'm sure it's on the order of microseconds, but 🤷 .

@nblumhardt nblumhardt merged commit 56a26bd into serilog:dev Apr 23, 2024
@nblumhardt
Copy link
Member

Thank you @tillig! 👍

@tillig tillig deleted the feature/1944-case-insensitive-level branch April 24, 2024 00:57
(context.Length == levelOverride.Context.Length || context[levelOverride.Context.Length] == '.'))
if (
(
context.Length == levelOverride.Context.Length ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the necroposting on something that's already overkill, but I will forever be pondering whether using a String.Equals into the same length case will be cheaper, or whether StartsWith internally special cases this...

Copy link
Member

@khellang khellang Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the current StartsWith implementation does (when passing OrdinalIgnoreCase):

case StringComparison.OrdinalIgnoreCase:
    if (this.Length < value.Length)
    {
        return false;
    }
    return Ordinal.EqualsIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length);

And this is (effectively, I inlined a method call for brevity) what Equals does:

case StringComparison.OrdinalIgnoreCase:
    if (a.Length != b.Length)
    {
        return false;
    }
    return Ordinal.EqualsIgnoreCase(ref a.GetRawStringData(), ref b.GetRawStringData(), b.Length);

In the case where the length is the same, they both call into the same machinery with the same length, so I think they should be equal here (no pun intended).

Copy link
Member

@bartelink bartelink Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Kristian, this has made my day (especially for saving Travis the lookup from an annoying drive by commenter!)
(I had some idea this was the case, but you can never be sure whether your >40 hours of reading Stephen Toub posts has actually had any effect!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Only case sensitive check for minimumLevel override
4 participants