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

AF-3505 : ActiveSpan #34

Merged
merged 4 commits into from
Feb 6, 2019
Merged

AF-3505 : ActiveSpan #34

merged 4 commits into from
Feb 6, 2019

Conversation

dustinlessard-wf
Copy link

@dustinlessard-wf dustinlessard-wf commented Dec 15, 2018

Description

For platforms not propagating the call-context, it's inconvenient to pass the active Span from function to function manually, so OpenTracing should provide, for those platforms, that Tracer contains a Scope Manager that grants access to the active Span through a container, called Scope (using some call-context storage, such as thread-local or coroutine-local)

Driving use case: We want to trace what happens from the time a pre-authenticated user navigates to Wdesk, until they have arrived at home and can see the file/folder list populated.

To permit this, we need to support the concept of ScopeManager and ActiveSpan in opentracing_dart.

Changes

  1. Added AbstractScope
  2. Added AbstractScopeManager
  3. Added startTime setter to AbstractSpan
  4. Added ScopeManager getter to AbstractTracer
  5. Added activeSpan getter to AbstractTracer
  6. Added NoOpScope
  7. Added NoOpScopeManager
  8. Switched to using a single instance for NoOpSpanContext in NoOpSpan
  9. Switched to using a single instance for a NoOpTracer's ScopeManager
  10. Added various tests.
  11. Some opportunistic refactors around lints.

Testing/QA

  • CI passes

Code Review

@Workiva/app-frameworks

@aviary2-wf
Copy link

Security Insights

No security relevant content was detected by automated scans.

Action Items

  • Review PR for security impact; comment "security review required" if needed or unsure
  • Verify aviary.yaml coverage of security relevant code

Questions or Comments? Reach out on Slack: #support-infosec.

@@ -50,15 +50,16 @@ void runFailureCase() {

HttpRequest.getString('http://httpstat.us/500').then((String result) {
span.log('data_received', payload: result);
}).catchError((error) {
}).catchError((dynamic error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Dart recommends not specifying types in these anonymous functions. Is there a reason we are declaring this as dynamic? If so, would it be better to inline a type cast to be more explicit?

Copy link
Author

Choose a reason for hiding this comment

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

I was getting an analyzer warning to 'Specify type annotations'.

class JsonSerializableSpan {
// ignore: public_member_api_docs
Copy link
Contributor

Choose a reason for hiding this comment

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

BTW you can ignore this lint for the whole file (https://www.dartlang.org/guides/language/analysis-options#ignoring-specific-analysis-rules)

// ignore_for_file: public_member_api_docs

Copy link
Author

Choose a reason for hiding this comment

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

ty

export 'src/noop_span.dart';
export 'src/noop_span_context.dart';
export 'src/noop_tracer.dart';
export 'src/reference.dart';
export 'src/reference.dart';
export 'src/span_context.dart';
Copy link
Contributor

Choose a reason for hiding this comment

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

This and the line above it are repeated export statements.

export 'src/noop_span.dart';
export 'src/noop_span_context.dart';
export 'src/noop_tracer.dart';
export 'src/reference.dart';
export 'src/reference.dart';
export 'src/span_context.dart';
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, do we need to export this stuff through Noop Tracer? It makes more sense to me that they would import the normal library entrypoint to get access to these things, and use this entrypoint for just the Noop-related exports.

import 'package:opentracing/opentracing.dart';

/// The No-op implementation of [Scope] in which all operations are no-op
class NoOpScope implements Scope {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we use Noop rather than NoOp for consistency?

@danielharasymiw-wf
Copy link
Contributor

danielharasymiw-wf commented Dec 20, 2018

Maybe I missed it, or it's not needed. Do we also need to add a method to set the active span?
As well perhaps this could be done in future work, but I think opentracing java has an ignoreActiveSpan method?

@dustinlessard-wf
Copy link
Author

dustinlessard-wf commented Dec 21, 2018

@danielharasymiw-wf said:

Maybe I missed it, or it's not needed. Do we also need to add a method to set the active span?
As well perhaps this could be done in future work, but I think opentracing java has an ignoreActiveSpan method?

We can set the active span by calling

Tracer.scope.activate(theActiveSpan);

As well perhaps this could be done in future work, but I think opentracing java has an ignoreActiveSpan method?

I did indeed see that method here:
https://github.com/opentracing/opentracing-java#starting-a-new-span
From what I've seen, this method is related to the builders we see in the Java port, which don't exist in this Dart library. My preference is to leave that out for the time being.

Thoughts? @danielharasymiw-wf

import 'package:opentracing/noop_tracer.dart';

void main() {
group('NoOpScope: verify', () {
Copy link
Contributor

@sebastianmalysa-wf sebastianmalysa-wf Dec 27, 2018

Choose a reason for hiding this comment

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

nit: s/NoOpScope/NoopScope here and in the test descriptions below

import 'package:opentracing/noop_tracer.dart';

void main() {
group('NoOpScopeManager: verify', () {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: s/NoOpScope/NoopScope here and in the test descriptions below

@@ -39,4 +39,10 @@ void main() {
expect(noopSpan.context.sampled, expectedContext.sampled);
expect(noopSpan.context.baggage, expectedContext.baggage);
});

test('Verify startSpan returns NoOpSpan from NoOpScopeManager', () {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: s/NoOp/Noop in NoOpSpan, NoOpScopeManager and noOpTracer below

Copy link
Contributor

@sebastianmalysa-wf sebastianmalysa-wf left a comment

Choose a reason for hiding this comment

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

just a few small nits otherwise looks good to me

@greglittlefield-wf greglittlefield-wf mentioned this pull request Dec 28, 2018
Copy link

@greglittlefield-wf greglittlefield-wf left a comment

Choose a reason for hiding this comment

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

Looks good besides a few small things (and sorry for all the doc nits).

Have you spiked out a reference implementation of this that consumes these classes? I'm a little leery of committing to a public interface that doesn't have any real-world concrete implementations.

Also, a bigger question: are we trying to go off of this ScopeManager proposal, or just the Java impl minus the builder stuff?

/// A [Scope] formalizes the activation and deactivation of a [Span], usually
/// from a CPU standpoint.
///
/// Many times a [Span] will be extant ([Span].finish() has not been called)

Choose a reason for hiding this comment

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

#nit Fix dartdoc reference

Suggested change
/// Many times a [Span] will be extant ([Span].finish() has not been called)
/// Many times a [Span] will be extant ([Span.finish] has not been called)

@@ -0,0 +1,21 @@
import 'package:opentracing/opentracing.dart';

/// A [Scope] formalizes the activation and deactivation of a [Span], usually

Choose a reason for hiding this comment

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

#nit the Dart style guide discourages redundant references to the surrounding context

Suggested change
/// A [Scope] formalizes the activation and deactivation of a [Span], usually
/// A Scope formalizes the activation and deactivation of a [Span], usually

/// Many times a [Span] will be extant ([Span].finish() has not been called)
/// despite being in a non-runnable state from a CPU/scheduler standpoint. For
/// instance, a [Span] representing the client side of an RPC will be unfinished
/// but blocked on IO while the RPC is still outstanding. A [Scope] defines when

Choose a reason for hiding this comment

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

#nit

Suggested change
/// but blocked on IO while the RPC is still outstanding. A [Scope] defines when
/// but blocked on IO while the RPC is still outstanding. A Scope defines when

/// Mark the end of the active period for the current thread and [Scope],
/// updating the [ScopeManager].active() in the process.
///
/// NOTE: Calling 'close' more than once on a single {@link Scope} instance

Choose a reason for hiding this comment

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

#nit

Suggested change
/// NOTE: Calling 'close' more than once on a single {@link Scope} instance
/// NOTE: Calling 'close' more than once on a single Scope instance

/// but blocked on IO while the RPC is still outstanding. A [Scope] defines when
/// a given [Span] is scheduled and on the path.
abstract class Scope {
/// Mark the end of the active period for the current thread and [Scope],

Choose a reason for hiding this comment

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

#nit

Suggested change
/// Mark the end of the active period for the current thread and [Scope],
/// Mark the end of the active period for the current thread and Scope,

/// a given [Span] is scheduled and on the path.
abstract class Scope {
/// Mark the end of the active period for the current thread and [Scope],
/// updating the [ScopeManager].active() in the process.

Choose a reason for hiding this comment

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

#nit

Suggested change
/// updating the [ScopeManager].active() in the process.
/// updating the [ScopeManager.active] in the process.

/// Mark the end of the active period for the current thread and [Scope],
/// updating the [ScopeManager].active() in the process.
///
/// NOTE: Calling 'close' more than once on a single {@link Scope} instance

Choose a reason for hiding this comment

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

#nit

Suggested change
/// NOTE: Calling 'close' more than once on a single {@link Scope} instance
/// NOTE: Calling 'close' more than once on a single Scope instance

/// Returns a [Scope] instance to control the end of the active period for the
/// [Span]. It is a programming error to neglect calling [Scope].close() on
/// the returned instance.
Scope activate(Span span, {bool finishSpanOnClose});

Choose a reason for hiding this comment

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

I noticed that the spec lists finishSpanOnClose as required.

A required boolean parameter finish span on close will mark whether...

This should be made a positional parameter or have the @required annotation added.

Copy link
Author

Choose a reason for hiding this comment

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

I agree with your suggestion, but later came across this: opentracing/opentracing-java#291

I also noticed that your reference isn't the spec, but rather a suggested change to it. I'm inclined to remove this parameter for now, until that spec suggestion is accepted and/or we have an immediate use-case for it. Thoughts?

Copy link
Author

Choose a reason for hiding this comment

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

After creating an actual implementation in basictracer_dart, I've changed my position on your suggestion and think we should proceed with it.

/// leads to undefined behavior.
void close();

/// Returns the [Span] that's been scoped by this [Scope]

Choose a reason for hiding this comment

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

#nit Getters should be documented using noun phrases.

Suggested change
/// Returns the [Span] that's been scoped by this [Scope]
/// The Span that's been scoped by this Scope.

Choose a reason for hiding this comment

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

I'll stop commenting on these; sorry, my OCD is kicking in... 😅

If you wouldn't be offended, I'd be more than happy to make a PR into this branch to update the doc comments to match the Dart style guide.


/// Returns the activer [Span]. This is a shorthand for
/// `Tracer.scopeManager().active().span()` and null will be returned if
/// [ScopeManager].active()} is null.

Choose a reason for hiding this comment

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

Based on the description, can we just provide a default implementation?

Span activeSpan() => scopeManager.active?.span;

/// Returns the activer [Span]. This is a shorthand for
/// `Tracer.scopeManager().active().span()` and null will be returned if
/// [ScopeManager].active()} is null.
Span activeSpan();

Choose a reason for hiding this comment

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

Are these changes missing the Tracer Changes from the spec?

Also, is there supposed to be an activeSpan on this class? I don't see that in the spec

Copy link
Author

@dustinlessard-wf dustinlessard-wf Dec 29, 2018

Choose a reason for hiding this comment

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

Are these changes missing the Tracer Changes from the spec?

Yes, these changes are missing some portions of the proposed spec changes. The only parts I implemented were those that would specifically help accomplish the targeted use-case:
consolidate the spans occurring from the time the user has started to navigate to Home being loaded and populated.

Also, is there supposed to be an activeSpan on this class? I don't see that in the spec

Yes, this is a convenience method that was borrowed from https://developer.lightbend.com/docs/telemetry/current/extensions/opentracing/api.html#globaltracer-active-span and https://github.com/opentracing-contrib/java-agent#creating-custom-rules .
We could probably just include it in our implementation if we don't think we want it in this abstract class.

@dustinlessard-wf
Copy link
Author

Ready for re-review.

/// Returns the activer [Span]. This is a shorthand for
/// `Tracer.scopeManager().active().span()` and null will be returned if
/// [ScopeManager].active()} is null.
Span activeSpan();
Span activeSpan() => scopeManager.active?.span;

Choose a reason for hiding this comment

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

Now that I think about it, this should just be a getter.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed!

@@ -54,6 +55,17 @@ abstract class AbstractTracer {
/// var serverSpan = Tracer.startSpan('...', { childOf : wireCtx });
SpanContext extract(String format, dynamic carrier);

/// Returns the current [ScopeManager], which may be a noop but may not be
/// null.
ScopeManager get scopeManager;

Choose a reason for hiding this comment

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

Adding these abstract members is considered a breaking change, since classes that extend this will no longer implement them.

To avoid a major change, could we implement this and return a NoopScopeManager?

Copy link
Author

@dustinlessard-wf dustinlessard-wf Jan 29, 2019

Choose a reason for hiding this comment

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

Good idea! That being said, I'm debating this and consulting with our Tracing people on it.

Copy link
Author

Choose a reason for hiding this comment

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

After consulting and debating the question of whether we should accept this getter, resulting in a breaking change, the following pros and cons were identified:

Pros:

  1. The AbstractTracer continues to make it obvious what must be implemented by extenders/implementors.
  2. We maintain the distinction between abstract classes and inert implementations.

Cons:

  1. Propagation of this breaking change will require no less than 7 pull requests to update consumers. (mitigated via automation by RepoCommander)

Based on these considerations, I believe we should accept the breaking change and propagate it via RepoCommander.

/// null.
ScopeManager get scopeManager;

set scopeManager(ScopeManager value);

Choose a reason for hiding this comment

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

See scopeManager getter comment

@@ -37,6 +37,9 @@ abstract class Span {
/// Each span has a UTC timestamp describing when it started.
DateTime get startTime;

/// Each span has a UTC timestamp describing when it started.
set startTime(DateTime value);

Choose a reason for hiding this comment

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

Adding these abstract members is considered a breaking change, since classes that extend this will no longer implement them.

Do we need this change?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, we need this change. Luckily at Workiva in Dart, we only use BasicTracer as our basis for all Tracing related classes.

Map getFields() {
Map<String, dynamic> fieldMap = {};
Map<String, dynamic> getFields() {
Map<String, dynamic> fieldMap = <String, dynamic>{};

Choose a reason for hiding this comment

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

#nit you only need to specify generic parameters on the LHS or RHS, not both; Dart infers the rest for you!

I think the convention is to use the RHS, since it allows the omission of the full type on the LHS

Suggested change
Map<String, dynamic> fieldMap = <String, dynamic>{};
final fieldMap = <String, dynamic>{};

Copy link
Author

Choose a reason for hiding this comment

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

screen shot 2019-01-29 at 11 53 39 am
How can we avoid this?

Choose a reason for hiding this comment

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

That is from one of the lints opted into in this repo; I think that lint should be disabled unless there was a reason it was enabled.

Related: I agree with the sentiment expressed here by one of the linter package maintainers: dart-lang/linter#1068 (comment)

I do not believe that any project should enable a lint because there isn't a stated reason not to. I think, rather, that a project should only enable those lints for which there is a stated reason for it to be enabled.

test('Verify startSpan returns NoopSpan from NoopScopeManager', () {
NoopTracer noOpTracer = new NoopTracer();
expect(
identical(noOpTracer.startSpan(''), noOpTracer.activeSpan()), isTrue);

Choose a reason for hiding this comment

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

#nit can use same matcher for a better error message

Suggested change
identical(noOpTracer.startSpan(''), noOpTracer.activeSpan()), isTrue);
same(noOpTracer.startSpan('')), noOpTracer.activeSpan());

Copy link
Author

Choose a reason for hiding this comment

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

ty! I didn't know that!

}

@override
Future<dynamic> flush() async {}

@override
Span activeSpan() {

Choose a reason for hiding this comment

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

Unnecessary override; this should use the same implementation as the abstract tracer, since this class seems to be the only thing providing activeSpan coverage.

@dustinlessard-wf
Copy link
Author

Ready for review, fyi @greglittlefield-wf

/// Returns a new NoopScope, which will use the supplied Span. If no Span is
/// supplied, a static instance of a NoopSpan will be used.
NoopScope({Span span}) {
_span = span ?? _span;

Choose a reason for hiding this comment

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

Could overwriting the static _span when a new NoopScope is created have the potential to be problematic?

For instance:

final span1 = globalTracer().startSpan('span1');
final span2 = globalTracer().startSpan('span2');

final noopScope1 = noopScopeManager.activate(span1, false);
print(noopScope1.span.operationName); // span1

final noopScope2 = noopScopeManager.activate(span2, false);
print(noopScope1.span.operationName); // span2 - this shouldn't have changed, right?
print(noopScope2.span.operationName); // span2

I would expect the implementation to look more like:

/// The No-op implementation of [Scope] in which all operations are no-op
class NoopScope implements Scope {
  static final Span _noopSpan = new NoopSpan();

  /// Returns a new NoopScope, which will use the supplied Span. If no Span is
  /// supplied, a static instance of a NoopSpan will be used.
  NoopScope({Span span}) :
      _span = span ?? _noopSpan;

  final Span _span;

  @override
  void close() {}

  @override
  Span get span => _span;
}

Copy link
Author

Choose a reason for hiding this comment

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

Good point

/// Returns the [Scope], or null if none could be found.
Scope get active;

set active(Scope value);

Choose a reason for hiding this comment

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

There shouldn't be a setter here; this value gets set in activate, and there is no setter in the Java implementation.

Copy link
Author

@dustinlessard-wf dustinlessard-wf Feb 5, 2019

Choose a reason for hiding this comment

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

Please see https://github.com/Workiva/basictracer-dart/pull/59/files#diff-7c439d7fe63b0d14ff72fcbc3d094df5R32 for the reason to include this setter.

This allows us to auto-nest scopes:

Span operationASpan = new BasicSpan('OperationA')
scopeManager.activate(operationASpan, finishSpanOnClose);
/// start Operation A
/// make a bunch of spans representing sub-steps of Operation A
Span operationA1 = new BasicSpan('operationA1');
scopeManager.activate(operationA1, finishSpanOnClose);
/// make a bunch of spans representing sub-steps of Operation A1
/// end Operation A1
scopeManager.active.close()
/// end Operation A 
scopeManager.active.close()

The resulting span structure would be similar to :

operationA
     |- spans for sub-steps
     |- operationA1
               |-spans for sub-steps

Thoughts?

Choose a reason for hiding this comment

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

That makes sense. Could we document that the setter should only be used by Scope/ScopeManager implementations, and discourage using it otherwise?

We would have to add ignore comments wherever used, but I think it'd be worth it to annotate it as @protected as well.

Copy link

@greglittlefield-wf greglittlefield-wf left a comment

Choose a reason for hiding this comment

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

+1, good stuff, @dustinlessard-wf!

@sebastianmalysa-wf
Copy link
Contributor

QA +1

[x] CI passes

@Workiva/release-management-pp

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

Successfully merging this pull request may close these issues.

None yet

8 participants