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

New Instrumenter API #2596

Merged
merged 26 commits into from
Apr 5, 2021
Merged

Conversation

anuraaga
Copy link
Contributor

Still draft quality but I think this is ready for some initial feedback

The biggest difference from the current APIs is avoiding inheritance in favor of stitching together parts. The notion is that there are only two big logic behavioral differences between instrumenters - server instrumenters extract context and client ones inject it. Other than that, it's just customization - HTTP semantic conventions drives the customization of span attributes, etc, but there is no big difference in logic it's sort of configuration. So instead of using inheritance, I encapsulate attribute extraction, span name extraction, and status extraction into composable parts. This allows two things that I think an inheritance version has trouble with

  • Resolve different conflicting semantic conventions. For example, AWS spans are notorious for having different functionality that needs to be reconciled per-method for determining span name from options of RPC, Messaging, Database, or Armeria also can be used both for RPC or just plain HTTP. So these different types of conventions don't fit neatly into an inheritance tree, we need to be able to compose them. I haven't added an example of this yet in this PR though

  • Allow user customization of points they need with builder pattern. I think this style allows keeping instrumentation classes private without losing customization, because customization can be defined in terms of the instrumentation-api. With inheritance, we'd probably need to expose instrumentation classes to allow methods to be overridden.

More than happy to hear thoughts, there are probably some terrible aspects of this too. Though most issues won't be ironed out until trying it on more instrumentation in future PRs.

/cc @as-polyakov

Copy link
Member

@mateuszrzeszutek mateuszrzeszutek left a comment

Choose a reason for hiding this comment

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

Overall I really like this proposition - the composition is so much nicer (and flexible!) than the inheritance 👍 👍 👍

Various observations & ideas:

  • For some instrumentations (like DB instrumentations) we will probably have to create some DTOs that collect several objects into a REQUEST - I don't think that's bad though, and some instrumentations already do this because of various reasons.
  • Those instrumentations that have more lifecycle phases than just start & end will be a bit problematic: we'll have to work around all those loose onRequest(), onException(), onSomething() calls.


Span span = Span.fromContext(context);
if (span.isRecording()) {
ctx.log()
.whenComplete()
.thenAccept(
log -> {
clientTracer.getNetPeerAttributes().setNetPeer(span, ctx.remoteAddress());

long requestEndTimeNanos = requestStartTimeNanos + log.responseDurationNanos();
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 that the Armeria instrumentation was the only one that passed the start/end timestamps manually - can we remove that safely? I think it'd be great to simplify that bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Decided at least for now to remove it and might rethink later


AttributesBuilder attributesBuilder = Attributes.builder();
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor : extractors) {
extractor.onStart(attributesBuilder, request);
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 just a minor thing, but using the AttributeSetter interface could help us avoid creating additional Attributes instance and copying to the span/spanBuilder.

Copy link
Member

Choose a reason for hiding this comment

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

@anuraaga @jkwatson what do you think about moving AttributeSetter interface to the API to unify setting attributes on Span and SpanBuilder?

Another option we have is to just use SpanBuilder here, and come up with a separate solution for the 2 instrumentations that don't fit this usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah the thought has come to mind but also with the caveat that indeed, I think it's very rare to share the same code for both SpanBuilder and Span which sounds like it's the case.

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 it's very rare to share the same code for both SpanBuilder and Span which sounds like it's the case.

That interface was added to reuse NetPeerUtils methods, which was called in various situations 😄
NetAttributesExtractor fixes that issue, so the only use case left that I can imagine is onRequest() situation - which we have to solve or find a workaround for anyway.

import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import org.checkerframework.checker.nullness.qual.Nullable;

public abstract class HttpAttributesExtractor<REQUEST, RESPONSE>
Copy link
Member

Choose a reason for hiding this comment

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

Should we put HTTP-related classes in a ...instrumenter.http subpackage? Same for DB/messaging/RPC attribute/span name extractors (in the future). The base package would contain just the base classes/interfaces

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm - I think it means we wouldn't have SpanStatusExtractor.http() (not in same package anymore) and just use HttpStatusExtractor.create() or something. I'm wondering about this vs extracting an attributes package with HttpAttributesExtractor, NetAttributesExtractor, etc, that does keep the public API together with some separation.

Copy link
Member

Choose a reason for hiding this comment

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

Or HttpSemanticConventions.spanName()/HttpSemanticConventions.status() in the http package

import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapGetter;

public abstract class ServerInstrumenter<REQUEST, RESPONSE>
Copy link
Member

Choose a reason for hiding this comment

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

There's one interesting thing that I've noticed when comparing this class to HttpServerTracer: the tracer class has the STORAGE type parameter and attachServerContext() and getServerContext() methods that interact with it.
This can actually be removed from the tracer/instrumenter, it's only used for manual context propagation and I think it actually SHOULDN'T be here - that's a completely separate concern.
(Also, not all server instrumentations implement it)

@mateuszrzeszutek
Copy link
Member

@anuraaga I've implemented several of my comments in this PR: anuraaga#20
I'd be happy to hear some feedback & comments.
In short, I've completely removed inheritance and added a instrumenter builder that allows the API user to choose which behavior (server or client propagation, internal span) they want in their instrumenter.

@trask
Copy link
Member

trask commented Mar 22, 2021

thx @anuraaga and @mateuszrzeszutek for giving so much thought to this!

I agree with all the big ideas 👍

once you're ready, I suggest we go for it: merge and start migrating instrumentation one-by-one, uncovering edge cases and working out the smaller stuff as we go

@trask trask mentioned this pull request Mar 22, 2021
Mateusz Rzeszutek and others added 8 commits March 23, 2021 09:21
* New Instrumenter API - InstrumenterBuilder

* New Instrumenter API - InstrumenterBuilder - code review comments
…nstrumentation into instrumentation-extractor
…nstrumentation into instrumentation-extractor
…nstrumentation into instrumentation-extractor
@anuraaga anuraaga marked this pull request as ready for review March 29, 2021 06:31
@anuraaga
Copy link
Contributor Author

Sorry for the delay, this should be ready for review

Comment on lines 55 to 68
Instrumenter(
Tracer tracer,
SpanNameExtractor<? super REQUEST> spanNameExtractor,
SpanKindExtractor<? super REQUEST> spanKindExtractor,
StatusExtractor<? super REQUEST, ? super RESPONSE> statusExtractor,
List<? extends AttributesExtractor<? super REQUEST, ? super RESPONSE>> extractors,
ErrorCauseExtractor errorCauseExtractor) {
this.tracer = tracer;
this.spanNameExtractor = spanNameExtractor;
this.spanKindExtractor = spanKindExtractor;
this.statusExtractor = statusExtractor;
this.extractors = extractors;
this.errorCauseExtractor = errorCauseExtractor;
}
Copy link
Member

Choose a reason for hiding this comment

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

As long as this is package protected passing all the arguments is fine, but if we plan to make this public would suggest to have an Option pojo to allow us to add more arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This wouldn't become public, that's why we have the builder

Copy link
Member

@mateuszrzeszutek mateuszrzeszutek left a comment

Choose a reason for hiding this comment

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

@anuraaga please let me know if you need any help with implementing suggestions from comments, I'd like to help with this PR.

import io.opentelemetry.context.propagation.TextMapSetter;
import java.util.List;

final class ClientInstrumenter<REQUEST, RESPONSE> extends Instrumenter<REQUEST, RESPONSE> {
Copy link
Member

Choose a reason for hiding this comment

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

We should probably rename ClientInstrumenter/ServerInstrumenter to DownstreamPropagatingInstrumenter/UpstreamPropagatingInstrumenter (also see anuraaga#20 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah - I understood the comment, I am still on the fence on it. Considering how much more common server/client is than the other forms I wonder if it's not so bad to have server / client words even when it's actually consumer / producer. Was thinking of addressing this in a follow-up when using from messaging.

Copy link
Member

Choose a reason for hiding this comment

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

Just to clarify: we can still have newServerInstrumenter(TextMapGetter), new(Upstream|Downstream)Instrumenter() would be just for the situation when you have to pass a custom SpanKindExtractor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it's a tradeoff between introducing new concepts vs having precise terminology. If server and producer are close enough to be two ducks (i.e, if it quacks it's a duck) then consolidating might be ok. I'm not too sure on it yet.

…n/test/base/HttpClientTest.groovy

Co-authored-by: Mateusz Rzeszutek <[email protected]>
@anuraaga
Copy link
Contributor Author

Thanks a lot @mateuszrzeszutek - I've left some thumbs on ones that seem trivial for this PR and comments on ones I lean towards following up. If you'd like to send a pr for them sounds great or else will address tomorrow

@mateuszrzeszutek
Copy link
Member

anuraaga#21

Mateusz Rzeszutek and others added 6 commits March 31, 2021 09:04
@anuraaga
Copy link
Contributor Author

anuraaga commented Apr 1, 2021

This should be ready for another review now, and I think it's probaby a reasonable first try to iterate on in future PRs

Copy link
Contributor

@iNikem iNikem left a comment

Choose a reason for hiding this comment

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

I am currently rather concerned about the simplicity of building an instrumenter for a given instrumentation. E.g. when using builder it is not clear what pieces are actually mandatory: do I have to provide httpAttributesExtractor? If not how can I understand if I should? Maybe a proper example will reduce that confusion.

Comment on lines +25 to +32
protected final void onStart(AttributesBuilder attributes, REQUEST request) {
set(attributes, SemanticAttributes.HTTP_METHOD, method(request));
set(attributes, SemanticAttributes.HTTP_URL, url(request));
set(attributes, SemanticAttributes.HTTP_TARGET, target(request));
set(attributes, SemanticAttributes.HTTP_HOST, host(request));
set(attributes, SemanticAttributes.HTTP_ROUTE, route(request));
set(attributes, SemanticAttributes.HTTP_SCHEME, scheme(request));
set(attributes, SemanticAttributes.HTTP_USER_AGENT, userAgent(request));
Copy link
Contributor

Choose a reason for hiding this comment

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

From http semantic convention:

more than the required attributes can be supplied, but this is recommended only if they cannot be inferred from the sent ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this API, we just set what was provided indeed, I don't think we can model something like that at this layer since we don't know what can be inferred, or if we do then it needs to be more explicit in the spec I guess, that spec language is way too vague to be actually implementable I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

WDYM, vague? Spec gives several options for required attribute combinations and then warns agains duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well these attributes are defined by the spec so it could say exactly which attributes should be filled. If it's the three sets of attributes and says attributes SHOULD NOT be filled from one set if another is filled, it would be clearer. I don't know either if this is a ordered list by priority. So vague ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

E.g, in that text server name is mentioned descriptively but it could just be said it should always be filled when available.

Copy link
Member

@trask trask left a comment

Choose a reason for hiding this comment

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

I think it's probaby a reasonable first try to iterate on in future PRs

👍

@@ -1,5 +1,5 @@
apply from: "$rootDir/gradle/instrumentation-library.gradle"
apply plugin: "net.ltgt.errorprone"
// apply plugin: "net.ltgt.errorprone"
Copy link
Member

Choose a reason for hiding this comment

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

add a TODO here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops

…nstrumentation into instrumentation-extractor
@anuraaga anuraaga merged commit cc47da5 into open-telemetry:main Apr 5, 2021
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.

5 participants