-
Notifications
You must be signed in to change notification settings - Fork 108
ScopeManager #113
base: master
Are you sure you want to change the base?
ScopeManager #113
Conversation
897e88f
to
0974d2f
Compare
0974d2f
to
98c21ac
Compare
See https://github.com/opentracing/specification/blob/f7ca62c9/rfc/scope_manager.md However, use continuation passing style which better fits JS and available APIs. Implementations: * AsyncHookSpanManager - uses Node.js async_hooks * AsyncWrapSpanManager - uses async-hook-jl which uses Node.js AsyncWrap * ZoneSpanManager - uses Zone.js
98c21ac
to
71e124f
Compare
Update: looks like https://github.com/RisingStack/jaeger-node and https://github.com/RisingStack/opentracing-auto use async_hooks. I believe that would be the most common method of CLS, since if you use Node.js and a recent version of it, it has the fewest drawbacks. |
@@ -88,6 +144,11 @@ export class Tracer { | |||
options.references = [childOf]; | |||
} | |||
delete(options.childOf); | |||
} else if (!options.references) { |
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.
Java and Python have an "ignore active span" option to prevent this automatic reference. Any chance you'd want to follow suite?
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, this should
``` | ||
|
||
JavaScript support for continuation local storage is a patchwork. | ||
This library includes simple wrappers around a few common APIs for continuation contexts. |
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.
This should be selected automatically for the user according to the environment. This will avoid the user needing extensive knowledge of all the possible ways to do context propagation in JavaScript.
I've mentioned how I think this should be selected at the bottom of #112 (comment)
tracer.activeSpan() // span | ||
})(); | ||
}); | ||
tracer.activeSpan() // null |
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.
Should these be on tracer.scopeManager()
instead?
new AsyncHookSpanManager(); | ||
``` | ||
|
||
**AsyncWrapSpanManager** |
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.
Since AsyncWrap
cannot have multiple hooks, and has many bugs, possible crashes and memory leaks in older Node versions, I would probably skip it entirely and add something based on a fixed fork of async-listener
as described in #112 (comment)
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.
FWIW, in my experience it's also slower than async-listener
.
* @return {A} the return value of the executed function. | ||
* @template A | ||
*/ | ||
async runSpanAsync<A>(name: string, options: SpanOptions, f: () => Promise<A>): Promise<A> { |
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.
Not sure I understand why this is necessary as await
can be used on any promise which can be returned by runSpan
as well.
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.
It's necessary if you want to stop the span once the promise is resolved.
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.
Why not simply detect if the returned value is a promise in runSpan
and handle it there?
* @return {A} the return value of the executed function. | ||
* @template A | ||
*/ | ||
runSpan<A>(name: string, options: SpanOptions, f: () => A): A { |
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.
This name sounds a bit weird to me, as this method doesn't run a span.
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.
Other suggestion?
It starts and stops (runs?) the span.
It runs the function, with a new active span for its duration.
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 I would actually remove it completely for the first version, to keep the API surface as small as possible. Then more helpers can be added over time if necessary and discussed individually.
* | ||
* @return {SpanManager} - the span manager, which may be a noop. | ||
*/ | ||
spanManager(): SpanManager { |
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 it this could be called simply scope()
. It makes sense to call tracer.scope().span()
and tracer.scope().run()
because these actions are indeed on the current scope.
* | ||
* @return the active Span. This is a shorthand for tracer.spanManager().active(). | ||
*/ | ||
activeSpan(): Span | null { |
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.
Similar to my comment below, I don't see the point of this since the alternative is not any more involved.
}); | ||
|
||
/** | ||
* ScopeManger using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html} |
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.
* ScopeManger using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html} | |
* ScopeManager using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html} |
Addresses #112
See https://github.com/opentracing/specification/blob/f7ca62c9/rfc/scope_manager.md
Continuation passing style
I first tried adhering to the draft RFC (somewhat working code), but it got clumsy, particularly for the Zone.js implementation. Just about every JS continuation library uses continuation passing style (Zone.js, Node.js domains, continuation-local-storage, etc.), and I believe it's the correct choice. Plus, the existing RFC IMO has a lot of ambiguity for the implementer. I added my findings to opentracing/specification#126.
Support
Unfortunately, there just isn't a great universal, out-of-the-box CLS solution.
There are two things that can be addressed by CLS: native async syntax (async/await), and common APIs (e.g. fs.readFile).
The are a surprising number of issues with current solutions. https://github.com/othiym23/node-continuation-local-storage is widely used, but can't be used with async/await which in 2018 is increasingly common. https://github.com/gms1/node-async-context can be, but (so far) doesn't have a lot of adoption and requires Node 8+. https://github.com/angular/zone.js/ has related TC39 proposal, but it's been stalled for two years.
In the end, the JS community has not figured out CLS (yet). I added a few implementations with very minimal dependencies that between them have fairly broad coverage. (See README.md for details.)
Implementations:
In the future, I expect the situation to improve. But this seems a good start, these wrappers are rather simple (~50 lines each).