Skip to content
This repository was archived by the owner on Dec 13, 2018. It is now read-only.

External scope storrage prototype#723

Merged
pakrym merged 12 commits intodevfrom
pakrym/externalscope
Oct 31, 2017
Merged

External scope storrage prototype#723
pakrym merged 12 commits intodevfrom
pakrym/externalscope

Conversation

@pakrym
Copy link
Contributor

@pakrym pakrym commented Oct 25, 2017

Goal: every logger should have scope support but it should be done in efficient way

Solution: provide opt in mechanism for logger providers to indicate that they can consume external scope and allow ILoggerFactory keep an single storage for all loggers.

When logger provider implements ISupportsExternalScope LoggerFactory would provider it's implementation of IExternalScopeProvider to it and never call StartScope in the future. In case implementation of ILoggerFactory does not support IExternalScopeProvider logger provider is able to instantiate and use LoggerExternalScopeProvider on it's own.

CollectScope<T>(Action<object, T> callback, T state) is implemented as a callback instead of returning IEnumerable to provider allocation free way of consuming scope values without exposing internal implementation details.

Current IExternalScopeProvider implementation is subject to change too.

@nblumhardt


namespace Microsoft.Extensions.Logging.Console
{
[Obsolete("Use " + nameof(LoggerExternalScopeProvider) + "instead" )]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this ever public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://media.giphy.com/media/fKk2I5iiWGN0I/giphy.gif

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok then :)


public IExternalScopeProvider ScopeProvider { get; set; }

[Obsolete("Changing this property would have no effect, please use " + nameof(ConsoleLoggerOptions) + "." + nameof(ConsoleLoggerOptions.IncludeScopes))]
Copy link
Contributor

Choose a reason for hiding this comment

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

"Changing this property has no effect. Use " + ... + " instead"

@NinoFloris
Copy link

What about the returned IDisposable instance for each new scope? What's the perf impact of that?

@pakrym
Copy link
Contributor Author

pakrym commented Oct 25, 2017

@NinoFloris this PR would alow to reduce overall count of IDisposables because we woldn't get one per provider per scope.

Future optimization is possible if all configured logger providers support external scope we can return IDisposable from IExternalScopeProvider without wrapping it in Logger.Scope

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

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

Some minor things in my first look (I know this is just a prototype, but prototypes become real code pretty quickly ;P). I like the pattern generally, but need to take a deeper look at it

else
var length = builder.Length;

ScopeProvider?.CollectScope((scope, stringBuilder) => stringBuilder.Append("=> ").Append(scope), builder);
Copy link
Contributor

Choose a reason for hiding this comment

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

You capture scopeProvider above, then use ScopeProvider here. Mistake?

scope.SetDisposable(0, _loggerFactory.ScopeProvider.Push(state));
}


Copy link
Contributor

Choose a reason for hiding this comment

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

nit: blank line


public void CollectScope<T>(Action<object, T> callback, T state)
{
var curent = _currentScope.Value;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: current (two rs)

{
private readonly AsyncLocal<Scope> _currentScope = new AsyncLocal<Scope>();

public void CollectScope<T>(Action<object, T> callback, T state)
Copy link
Contributor

Choose a reason for hiding this comment

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

Put a doc comment on here describing how the walk is done (from leaf nodes up to the root). Doesn't have to be amazing prose, but something that makes the behavior relatively clear for us to wordsmith later (and to help provider authors)

@nblumhardt
Copy link
Contributor

@pakrym thanks for the heads-up!

I think Serilog is unaffected because, by integrating at the ILoggerFactory level, we're working at a layer above this infrastructure. (Please correct me if I'm wrong.)

Integrating this into the Seq provider seems straightforward - it's using a cut-down copy of Serilog's infrastructure, so effectively we'd call CollectScope() within an ILogEventEnricher and apply the logic that's in https://github.com/datalust/seq-extensions-logging/blob/dev/src/Seq.Extensions.Logging/Serilog/Extensions/Logging/SerilogLoggerScope.cs#L48.

One thing that doesn't quite sit right with me about the way MEL is evolving is the disconnect between scopes, events, and filters in the pipeline. In Serilog, properties from the scope go onto the event; later, filters can determine whether an event is included based on its properties, and we can even do tricky things like enrich or route events selectively based on their properties.

Events being a collection of properties is such a fundamental structured logging concept that it's strange to flesh out the implementation side of MEL without acknowledging it in the design.

I suspect that the ultimate reason for avoiding an "event" type was to eliminate mandatory allocations, and keeping it out of the MEL interface/abstraction was a great move, but with each tactical feature addition like this one, allocations appear in real-world use of the pipeline anyway. (There's a likely per-event allocation for the first delegate argument to CollectScope(), for example.)

In the long run, would struct LogEvent and some kind of low-footprint Properties collection on it, unify the implementation-side of MEL and help connect all of these features logically?

@nblumhardt
Copy link
Contributor

(In case the relevance of the LogEvent topic is unclear - the alternative to the design explored in the PR is to have the scope set properties on a LogEvent for consumption by the providers, instead of having providers pull information from the scope directly.)

@pakrym
Copy link
Contributor Author

pakrym commented Oct 26, 2017

@nblumhardt unfortunately we are not able to make large design changes at this point so what we left with is trying to improve current situation with targeted fixes.

There's a likely per-event allocation for the first delegate argument to CollectScope(), for example.
My hope was that having T state would alow to prevent allocation.

@nblumhardt
Copy link
Contributor

@pakrym under those constraints it's fine - I am only thinking of how each API added tactically will make larger-scale changes trickier in the future (always that catch-22).

The Action is the object that will be allocated - at least in the case of the console provider, the closure will end up on the heap. No big deal.

The T state argument makes sense 👍. Naming did trip me up at first - scanning the code I read it as TState state. It's consistent with other callbacks like ParameterizedThreadStart though.

@analogrelay
Copy link
Contributor

The Action is the object that will be allocated - at least in the case of the console provider, the closure will end up on the heap. No big deal.

Even less deal if the Action only depends on the state parameter, which means there'd only be one per process (hence why that pattern appears everywhere :))

instead of having providers pull information from the scope directly

This PR is kinda trying to move us towards that model in a compatible way. I completely agree that the model for this should really just be that scope properties are aggregated with the log event properties and logger providers never really need to see scopes. This model allows us to start migrating providers to a model where they don't do scope management at all.

@pakrym pakrym changed the title [WIP] External scope storrage prototype External scope storrage prototype Oct 26, 2017
/// <summary>
/// An interface for configuring logging providers.
/// </summary>
public interface ISupportsExternalScope
Copy link
Member

Choose a reason for hiding this comment

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

Nit: ISupportExternalScope

Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to write a response about the I not being part of the name and we should keep the s, but then I found this: https://source.dot.net/#System.ComponentModel.Primitives/System/ComponentModel/ISupportInitialize.cs,f5af3a8671036626 ;P

So, yeah, carry on then.


public interface IExternalScopeProvider
{
void CollectScope<T>(Action<object, T> callback, T state);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I prefer AggregateScopes or ReduceScopes.

Maybe TAccumulate AggregateScopes<TAccumulate>(Function<TAccumulate, object, TAccumulate>. It should be easy to convert any Action<object, T> callback into this by just returning the T/TAccumlate.

I actually don't care to much if the method is void or not, but putting Aggregate or Reduce in the name makes it more clear that the callback is called potentially multiple times inline.

Copy link
Contributor

Choose a reason for hiding this comment

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

AggregateScopes is fine with me if it lets you flow an accumulator, to match Enumerable.Aggregate. I'm not a fan of ReduceScopes because Reduce isn't a term we generally use in our APIs (even if I do like it :)).

I agree Collect isn't a great word since it implies collecting into a single unit (kinda like Aggregate). Perhaps just ForEachScope (precedent with List<T>.ForEach) or EnumerateScopes (other BCL examples of this return IEnumerable but I think we wouldn't want to do that here, however it would be pretty clear what the callback is doing when you see the void return).

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 like ForEach more, don't think Aggregate pattern would be useful in this case: TAccumulate would often be collection or collection like object and passed in by reference.

public interface IExternalScopeProvider
{
void CollectScope<T>(Action<object, T> callback, T state);
IDisposable Push(object state);
Copy link
Member

Choose a reason for hiding this comment

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

Since BeginScope takes TState state, shouldn't this too?

Copy link
Contributor

Choose a reason for hiding this comment

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

Push<TState>(TState state)?

This is one place where the external scope provider isn't able to have the advantages that each provider has. We preserve generics all the way through in order to allow structs to remain unboxed, but the external provider will be forced to box them in order to store them for later.

var scopeProvider = _loggerFactory.ScopeProvider;
var scopeCount = scopeProvider != null ? 1 : 0;

// TODO: do not do this every time
Copy link
Member

Choose a reason for hiding this comment

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

Are you going to cache the result for this?

/// <param name="callback">The callback to be executed for every scope object</param>
/// <param name="state">The state object to be passed into the callback</param>
/// <typeparam name="T"></typeparam>
void CollectScope<T>(Action<object, T> callback, T state);
Copy link
Member

Choose a reason for hiding this comment

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

nit: T -> TState

_providerRegistrations = providers.Select(provider => new ProviderRegistration { Provider = provider }).ToList();
foreach (var provider in providers)
{
AddProviderRegistration(provider, false);
Copy link
Member

Choose a reason for hiding this comment

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

nit: use a named parameter for the false argument.

return newScope;
}

private class Scope: IDisposable
Copy link
Member

Choose a reason for hiding this comment

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

nit: spacing

@pakrym pakrym force-pushed the pakrym/externalscope branch from 1ed010d to 2758848 Compare October 27, 2017 20:48
@pakrym
Copy link
Contributor Author

pakrym commented Oct 30, 2017

🆙📅

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

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

Small suggestion to change ForEachScope.

{
return;
}
Report(current.Parent);
Copy link
Contributor

Choose a reason for hiding this comment

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

Recursion :(. This could be done iteratively, no? It would probably be better to avoid stack diving (even though we're generally going to have a smallish number of scopes)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not without additional data structure, I'm using recursion to reverse order of items in collection

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see. Didn't notice the position of the recursion was above the consumption. Ok then.

var (builder, length) = state;
var first = length == builder.Length;
builder.Append(first ? "=> " : " => ").Append(scope);
}, (stringBuilder, initialLength));
Copy link
Contributor

Choose a reason for hiding this comment

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

Clever. Almost makes me want this as a compiler-assisted pattern (capture clauses, a la C++?)


public override string ToString()
{
return State?.ToString();
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Can State really be null? If so, is it OK to return null from ToString()?

Copy link
Contributor Author

@pakrym pakrym Oct 30, 2017

Choose a reason for hiding this comment

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

Copied from

but not sure how valid it is.

@kierenj
Copy link

kierenj commented Feb 27, 2018

very cool, am (re-)implementing scope at the moment for a logger and of course this would be a plus. is there any kind of timeline for this appearing in a particular version? (am sneaking the 2.1.0-preview1-final version at the moment)

@snakefoot
Copy link

snakefoot commented Oct 6, 2018

@pakrym Any reason why you removed this optimization:

if (loggers.Length == 1)
    return loggers[0].Logger.BeginScope(state);

It will hurt NLog and Log4net that doesn't replace the LoggerFactory like Serilog does.

Please re-introduce the optimization ASAP.

@snakefoot
Copy link

@pakrym Because you have removed this optimization and because the internal combined Scope-class fails to override ToString() then it is actually a breaking change as this now fails:

using (var scopeState = _logger.BeginScope("{World} People", "Earth"))
{
    _logger.LogInformation(scopeState.ToString());  // No longer prints Earth People
}

@davidfowl
Copy link
Member

@snakefoot I'm more worried about breaking changes then the "optimization". Can you file a new bug describing the problem NLog is having with a repro project? That'll help us quickly identify the issue.

@snakefoot
Copy link

@pakrym It would be nice if LoggerProviders could still be notified on BeginScope / DisposeScope. In case they had extra state to maintain without having to enter allocation frenzy.

@davidfowl
Copy link
Member

@pakrym It would be nice if LoggerProviders could still be notified on BeginScope / DisposeScope. In case they had extra state to maintain without having to enter allocation frenzy.

Can you please file an issue for each of the problems you're having (with evidence)...

@snakefoot
Copy link

@davidfowl Created #893

@snakefoot
Copy link

@davidfowl Created #894

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants