Conversation
|
|
||
| namespace Microsoft.Extensions.Logging.Console | ||
| { | ||
| [Obsolete("Use " + nameof(LoggerExternalScopeProvider) + "instead" )] |
There was a problem hiding this comment.
Why was this ever public?
|
|
||
| public IExternalScopeProvider ScopeProvider { get; set; } | ||
|
|
||
| [Obsolete("Changing this property would have no effect, please use " + nameof(ConsoleLoggerOptions) + "." + nameof(ConsoleLoggerOptions.IncludeScopes))] |
There was a problem hiding this comment.
"Changing this property has no effect. Use " + ... + " instead"
|
What about the returned IDisposable instance for each new scope? What's the perf impact of that? |
|
@NinoFloris this PR would alow to reduce overall count of Future optimization is possible if all configured logger providers support external scope we can return IDisposable from |
analogrelay
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
You capture scopeProvider above, then use ScopeProvider here. Mistake?
| scope.SetDisposable(0, _loggerFactory.ScopeProvider.Push(state)); | ||
| } | ||
|
|
||
|
|
|
|
||
| public void CollectScope<T>(Action<object, T> callback, T state) | ||
| { | ||
| var curent = _currentScope.Value; |
| { | ||
| private readonly AsyncLocal<Scope> _currentScope = new AsyncLocal<Scope>(); | ||
|
|
||
| public void CollectScope<T>(Action<object, T> callback, T state) |
There was a problem hiding this comment.
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)
|
@pakrym thanks for the heads-up! I think Serilog is unaffected because, by integrating at the Integrating this into the Seq provider seems straightforward - it's using a cut-down copy of Serilog's infrastructure, so effectively we'd call 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 In the long run, would |
|
(In case the relevance of the |
|
@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.
|
|
@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 The |
Even less deal if the Action only depends on the
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. |
| /// <summary> | ||
| /// An interface for configuring logging providers. | ||
| /// </summary> | ||
| public interface ISupportsExternalScope |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Since BeginScope takes TState state, shouldn't this too?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
| _providerRegistrations = providers.Select(provider => new ProviderRegistration { Provider = provider }).ToList(); | ||
| foreach (var provider in providers) | ||
| { | ||
| AddProviderRegistration(provider, false); |
There was a problem hiding this comment.
nit: use a named parameter for the false argument.
| return newScope; | ||
| } | ||
|
|
||
| private class Scope: IDisposable |
1ed010d to
2758848
Compare
|
🆙📅 |
analogrelay
left a comment
There was a problem hiding this comment.
Small suggestion to change ForEachScope.
| { | ||
| return; | ||
| } | ||
| Report(current.Parent); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Not without additional data structure, I'm using recursion to reverse order of items in collection
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Clever. Almost makes me want this as a compiler-assisted pattern (capture clauses, a la C++?)
|
|
||
| public override string ToString() | ||
| { | ||
| return State?.ToString(); |
There was a problem hiding this comment.
Nit: Can State really be null? If so, is it OK to return null from ToString()?
There was a problem hiding this comment.
Copied from
but not sure how valid it is.|
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) |
|
@pakrym Any reason why you removed this optimization: It will hurt NLog and Log4net that doesn't replace the LoggerFactory like Serilog does. Please re-introduce the optimization ASAP. |
|
@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: |
|
@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. |
|
@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)... |
|
@davidfowl Created #893 |
|
@davidfowl Created #894 |

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
ILoggerFactorykeep an single storage for all loggers.When logger provider implements
ISupportsExternalScopeLoggerFactorywould provider it's implementation ofIExternalScopeProviderto it and never callStartScopein the future. In case implementation ofILoggerFactorydoes not supportIExternalScopeProviderlogger provider is able to instantiate and useLoggerExternalScopeProvideron it's own.CollectScope<T>(Action<object, T> callback, T state)is implemented as a callback instead of returningIEnumerableto provider allocation free way of consuming scope values without exposing internal implementation details.Current
IExternalScopeProviderimplementation is subject to change too.@nblumhardt