Skip to content
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

basic-tracer initial implementation #99

Merged
merged 16 commits into from
Jul 26, 2019

Conversation

hekike
Copy link
Member

@hekike hekike commented Jul 12, 2019

It is challenging to build a minimal tracer that makes sense without propagation and exporter interfaces. Here is my initial take on it.

@hekike
Copy link
Member Author

hekike commented Jul 12, 2019

My GitHub is linked to a CNCF account I don't have anymore, so fixing the CLA will take some time :P

Copy link
Member

@mayurkale22 mayurkale22 left a comment

Choose a reason for hiding this comment

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

This is a good start, added a few comments in the first iteration.

packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
.gitignore Outdated Show resolved Hide resolved
packages/opentelemetry-core/package.json Outdated Show resolved Hide resolved
packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
export class Tracer implements types.Tracer {
static INVALID_ID = '0';

private logger!: types.Logger;
Copy link
Contributor

@draffensperger draffensperger Jul 12, 2019

Choose a reason for hiding this comment

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

Given that this is initialized in the constructor, can we remove the ! here?

@hekike
Copy link
Member Author

hekike commented Jul 12, 2019

@mayurkale22 @draffensperger thanks for the review. I addressed your comments.

packages/opentelemetry-basic-tracer/src/types.ts Outdated Show resolved Hide resolved
fn: T
): ReturnType<T> {
if (!this.scopeManager) {
this.logger.warn('withSpan(...) has no effect without a scopeManager');
Copy link
Member

Choose a reason for hiding this comment

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

I don't think there should be a warning for this as it's a perfectly valid use case, especially if you use the low level construct.

Copy link
Member Author

Choose a reason for hiding this comment

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

My thinking was that base tracer has no ScopeManager by default so calling ScopeManager related functions like withSpan is not valid. As it won't do what you would expect. To suppress the warning the user can pass a NoopScopeManager. I think it's okay as in the automatic tracer we will ship OT with CLSScopeManager and ZoneScopeManager, so this warning is really for people who try to do something custom, and I believe for them it's quite useful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Part of what I'm hoping for from the browser side is code simplicity (as in fewer total bytes of code shipped to the client), so if we can find a way to avoid null checks and warning messages by designing the interface a bit differently I'd prefer that.

What if we had something like new NoopScopeManager({warn: true}) by default that would write warning logs any time someone attempts to call one of its methods? That way in the browser, where we use something like a GlobalWindowScopeManager the code for the if statements and warning logs can get tree shaken out (at least in principle).

Copy link
Member

Choose a reason for hiding this comment

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

@hekike
Since NoopScopeManager will have the same behavior as the current one (just calling fn, it almost cost nothing to use it and avoid null check everywhere. If you think warning user is important then a option like @draffensperger suggested would be the way to go for me.

this.logger.warn(
'getCurrentSpan() returns an invalid default span without a scopeManager'
);
return BasicTracer.defaultSpan;
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be null. In the noop it makes sense to always return a span so that nothing breaks when you disable the tracer, but for the base implementation null makes more sense to indicate the absence of a span on the scope.

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the condition where we won't have a scope manager set? If we require that in the constructor, can we just delegate the default span behavior to the scope manager?

I would prefer to avoid returning null because then the TS type needs to be types.Span|null, which is annoying for other TypeScript code to consume because it requires null checks. And the null type could also be confusing to end users who would expect it to return non-null.

Copy link
Member Author

@hekike hekike Jul 13, 2019

Choose a reason for hiding this comment

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

See my previous comment (#99 (comment)) about the default scope manager.
Looks like there are two opinions here: return null, log warning. Both can work for getCurrentSpan() and although I like the idea of null to indicate you really have no ScopeManager, but the same strict check concept is not applicable for withSpan so I'd vote for warning log. I guess throwing in withSpan without ScopeManager would be quite abusive.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added to SIG agenda.

Copy link
Member

Choose a reason for hiding this comment

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

Actually i think it's also applicable to withSpan when using the NoopScopeManager, it will just always return a null active scope, which is fine (at least for me).

Copy link
Member Author

Choose a reason for hiding this comment

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

we have users that explicitly don't want scope management, and they definitely don't want a warning because of it.

Do you know what would be a use-case that you don't want scope management but you still want to call withSpan and getCurrentSpan however you know they don't do anything?

Copy link
Member Author

Choose a reason for hiding this comment

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

(the default null without warning for getCurrentSpan would make sense for me as it shows well that you cannot use this feature without scope manager, although the withSpan would just silently swallow this fact, I don't think you can enforce it on API level, except if you throw)

Copy link
Member

Choose a reason for hiding this comment

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

Do you know what would be a use-case that you don't want scope management but you still want to call withSpan and getCurrentSpan however you know they don't do anything?

The auto instrumentation will call these methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't ScopeManager a hard requirement for auto instrumentation? Otherwise, you don't have a root span. How would it work?

Copy link
Member

Choose a reason for hiding this comment

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

That's a good point. Hopefully it will be possible to have at least some level of auto instrumentation even without a scope manager, but it would definitely be challenging. How would the auto instrumentation detect the presence of a proper scope manager?


// make sampling decision
if (!this.sampler.shouldSample(parentSpanContext)) {
return BasicTracer.defaultSpan;
Copy link
Member

Choose a reason for hiding this comment

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

This will not propagate the trace parent and trace state properly.

Copy link
Member

Choose a reason for hiding this comment

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

I think even if sampling decision is false, we should assign correct SpanContext for DefaultSpan/NoopSpan? @rochdev this is what you meant, right?

Copy link
Member

Choose a reason for hiding this comment

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

I mean that we shouldn't return a single global instance, otherwise it will not be possible to attach the IDs on it for propagation.

// parent is a SpanContext
if (
options.parent &&
typeof (options.parent as types.SpanContext).traceId
Copy link
Member

Choose a reason for hiding this comment

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

How is this converted to JavaScript? It doesn't look like this check is JS compatible. Same for other occurrences of as.

Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that it's attempting to check whether the options.parent has a traceId field. Is that correct?

Copy link
Member

Choose a reason for hiding this comment

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

as is just used to force the type of a variable, in this case it would be transpiled to if (options.parent && options.parent.traceId) {}

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove the typeof then?

Copy link
Member Author

Choose a reason for hiding this comment

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

could someone show me an example of how would it work with TS?
I get a Type error without typeof, instanceof also doesn't work.

Copy link
Contributor

Choose a reason for hiding this comment

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

What error do you get if you remove the typeof?

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, I see now. The typeof can be removed, the as not.

packages/opentelemetry-core/src/trace/Tracer.ts Outdated Show resolved Hide resolved
*/
getCurrentSpan(): types.Span {
// Return with defaultSpan if no scope manager provided.
if (!this.scopeManager) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In what case could this.scopeManager be falsey? Isn't it set in the constructor? Or is this attempting to guard against someone changing it to undefined after the initial tracer construction?

Copy link
Member

Choose a reason for hiding this comment

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

I think once we set to default no-op ScopeManager in constructor (this.scopeManager = config.scopeManager || new NoopScopeManager()), this check can be removed.

// parent is a SpanContext
if (
options.parent &&
typeof (options.parent as types.SpanContext).traceId
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that it's attempting to check whether the options.parent has a traceId field. Is that correct?

this.logger.warn(
'getCurrentSpan() returns an invalid default span without a scopeManager'
);
return BasicTracer.defaultSpan;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the condition where we won't have a scope manager set? If we require that in the constructor, can we just delegate the default span behavior to the scope manager?

I would prefer to avoid returning null because then the TS type needs to be types.Span|null, which is annoying for other TypeScript code to consume because it requires null checks. And the null type could also be confusing to end users who would expect it to return non-null.

@mayurkale22
Copy link
Member

@hekike Please rebase and add -p ../../ to codecov based on #101.

@codecov-io
Copy link

codecov-io commented Jul 13, 2019

Codecov Report

Merging #99 into master will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##           master      #99   +/-   ##
=======================================
  Coverage   98.74%   98.74%           
=======================================
  Files          29       29           
  Lines        1915     1915           
  Branches      221      221           
=======================================
  Hits         1891     1891           
  Misses         24       24
Impacted Files Coverage Δ
...kages/opentelemetry-scope-async-hooks/src/index.ts 100% <0%> (ø) ⬆️
packages/opentelemetry-scope-base/src/index.ts 100% <0%> (ø) ⬆️
packages/opentelemetry-scope-base/src/types.ts 100% <0%> (ø) ⬆️
...ry-scope-async-hooks/src/AsyncHooksScopeManager.ts 96.49% <0%> (ø) ⬆️
...s/opentelemetry-scope-base/src/NoopScopeManager.ts 100% <0%> (ø) ⬆️

@hekike hekike changed the title feat(tracer): initial implementation basic-tracer initial implementation Jul 14, 2019
Copy link
Member

@mayurkale22 mayurkale22 left a comment

Choose a reason for hiding this comment

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

Overall looks good to me, added few last comments.


// make sampling decision
if (!this.sampler.shouldSample(parentSpanContext)) {
return BasicTracer.defaultSpan;
Copy link
Member

Choose a reason for hiding this comment

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

I think even if sampling decision is false, we should assign correct SpanContext for DefaultSpan/NoopSpan? @rochdev this is what you meant, right?

@hekike
Copy link
Member Author

hekike commented Jul 19, 2019

@mayurkale22 so we would only use NoopSpan in the NoopTracer? Or the NoopSpan would accept SpanContext? How would we keep the allocation pattern low?

@mayurkale22
Copy link
Member

@mayurkale22 so we would only use NoopSpan in the NoopTracer? Or the NoopSpan would accept SpanContext? How would we keep the allocation pattern low?

This is my take, feel free to suggest if anything is wrong :)

Current Span types:
NoopSpan: all the operations are no-op except context propagation.
Span: actual implementation of Span interface, basically everything (real operations + context propagation) is possible. #102

Problem:
Although NoopSpan is lightweight in nature, but it is still allocated and some users it's not acceptable.
How would we keep the allocation pattern low? So need some solution to cover three scenarios

  1. Singleton Span: Approx. Zero allocation, all operations are no-op.
  2. NoRecording Span: All operations are no-op except context propagation.
  3. Span: All real operations + context propagation.

Solution 1:
Use three different spans:
NoopSpan - Singleton, all the operations are no-op.
NoRecordingSpan - all the operations are no-op except context propagation.
RecordingSpan - all real operations.

Solution 2:
Use two spans:
NoopSpan - Singleton, all the operations are no-op.
Span - this can handle NoRecording and Recording scenarios. But have to do if (...) else condition check on every operations based on whether Span is recording or no-recording.

Solution 3:
Use two spans:
NoopSpan - Create and use static instance with invalid SpanContext (for zero allocation - export const SingletonNoopSpan = new NoopSpan(INVALID_SPAN_CONTEXT)) and have another method to create new instance of NoopSpan based on passed SpanContext. (This is based on Java's implementation).
Span - All real operations + context propagation.

@hekike
Copy link
Member Author

hekike commented Jul 19, 2019

I would prefer Solution 1 where NoRecordingSpan could be maybe inherited from NoopSpan.

@hekike
Copy link
Member Author

hekike commented Jul 19, 2019

Should we wait for this decision or should we merge this PR and fix later with a todo comment?

@mayurkale22
Copy link
Member

Should we wait for this decision or should we merge this PR and fix later with a todo comment?

I would suggest to open an issue and add @todo with the issue link to address later. This merge would unblock real Span (#102) and Automatic-Tracer implementation (#91) work.

@mayurkale22 mayurkale22 requested a review from rochdev July 25, 2019 20:46
@hekike
Copy link
Member Author

hekike commented Jul 26, 2019

@mayurkale22 I addressed all the comments (or added a todo) and rebased with the master. I think it's good to go.

@mayurkale22 mayurkale22 merged commit 775f0e3 into open-telemetry:master Jul 26, 2019
dyladan added a commit to dyladan/opentelemetry-js that referenced this pull request Sep 9, 2022
dyladan added a commit to dyladan/opentelemetry-js that referenced this pull request Sep 9, 2022
pichlermarc pushed a commit to dynatrace-oss-contrib/opentelemetry-js that referenced this pull request Dec 15, 2023
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.

7 participants