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

Move client span creation to decorator and automatically suppress creation of neste… #460

Merged
merged 9 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,59 @@
*/
package io.opentelemetry.auto.bootstrap.instrumentation.decorator;

import io.grpc.Context;
import io.opentelemetry.context.ContextUtils;
import io.opentelemetry.context.Scope;
import io.opentelemetry.trace.DefaultSpan;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.Span.Kind;
import io.opentelemetry.trace.Tracer;
import io.opentelemetry.trace.TracingContextUtils;

public abstract class ClientDecorator extends BaseDecorator {

// Keeps track of the client span in a subtree corresponding to a client request.
// Visible for testing
static final Context.Key<Span> CONTEXT_CLIENT_SPAN_KEY =
Context.key("opentelemetry-trace-auto-client-span-key");

/**
* Creates a {@link Context} with the provided {@link Span} and sets it as the current {@link
* Context}
*
* @return a {@link Scope} that will restore the previous context. All callers of this method must
* also call {@link Scope#close()} when this next context is no longer needed.
*/
public static Scope currentContextWith(final Span clientSpan) {
iNikem marked this conversation as resolved.
Show resolved Hide resolved
return ContextUtils.withScopedContext(withSpan(clientSpan, Context.current()));
}

/** Returns a new {@link Context} forked from {@code context} with the {@link Span} set. */
public static Context withSpan(final Span clientSpan, final Context context) {
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this is only called with Context.current(), so maybe this method isn't needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed the same lack of symmetry in TracingContextUtils and followed the pattern. Let me know if it's worth diverging.

Copy link
Contributor

Choose a reason for hiding this comment

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

io.opentelemetry.trace.TracingContextUtils#withSpan can potentially be called with contexts other than current during context extraction via io.opentelemetry.trace.propagation.HttpTraceContext#extract method. This client decorator is not general purpose util, but more specific one. Thus I think we indeed can merge these two methods together.

if (!clientSpan.getContext().isValid()) {
return TracingContextUtils.withSpan(clientSpan, context);
}
return TracingContextUtils.withSpan(
Copy link
Contributor

Choose a reason for hiding this comment

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

I would extract common call to TracingContextUtils.withSpan from the if above.

clientSpan, context.withValue(CONTEXT_CLIENT_SPAN_KEY, clientSpan));
}

/**
* Returns a new client {@link Span} if there is no client {@link Span} in the current {@link
* Context}, or an invalid {@link Span} otherwise.
*/
public static Span getOrCreateSpan(String name, Tracer tracer) {
final Context context = Context.current();
final Span clientSpan = CONTEXT_CLIENT_SPAN_KEY.get(context);

if (clientSpan != null) {
// We don't want to create two client spans for a given client call, suppress inner spans.
return DefaultSpan.getInvalid();
iNikem marked this conversation as resolved.
Show resolved Hide resolved
}

final Span current = TracingContextUtils.getSpan(context);
return tracer.spanBuilder(name).setSpanKind(Kind.CLIENT).setParent(current).startSpan();
iNikem marked this conversation as resolved.
Show resolved Hide resolved
iNikem marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public Span afterStart(final Span span) {
assert span != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.opentelemetry.auto.instrumentation.api.Tags;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.Status;
import io.opentelemetry.trace.Tracer;
import java.net.URI;
import java.net.URISyntaxException;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -35,6 +36,10 @@ public abstract class HttpClientDecorator<REQUEST, RESPONSE> extends ClientDecor

protected abstract Integer status(RESPONSE response);

public Span getOrCreateSpan(REQUEST request, Tracer tracer) {
return getOrCreateSpan(spanNameForRequest(request), tracer);
}

public String spanNameForRequest(final REQUEST request) {
iNikem marked this conversation as resolved.
Show resolved Hide resolved
if (request == null) {
return DEFAULT_SPAN_NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
*/
package io.opentelemetry.auto.bootstrap.instrumentation.decorator

import io.grpc.Context
import io.opentelemetry.OpenTelemetry
import io.opentelemetry.trace.Span
import io.opentelemetry.trace.Tracer
import io.opentelemetry.trace.TracingContextUtils

class ClientDecoratorTest extends BaseDecoratorTest {

private static final Tracer TRACER = OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto")

def span = Mock(Span)

def "test afterStart"() {
Expand All @@ -44,6 +50,75 @@ class ClientDecoratorTest extends BaseDecoratorTest {
0 * _
}

def "test getOrCreateSpan when no existing client span"() {
when:
def span = ClientDecorator.getOrCreateSpan("test", TRACER)

then:
assert span.getContext().isValid()
}

def "test getOrCreateSpan when existing client span"() {
setup:
def existing = ClientDecorator.getOrCreateSpan("existing", TRACER)
def scope = ClientDecorator.currentContextWith(existing)

when:
def span = ClientDecorator.getOrCreateSpan("test", TRACER)

then:
assert !span.getContext().isValid()

cleanup:
scope.close()
}

def "test getOrCreateSpan internal after client span"() {
setup:
def client = ClientDecorator.getOrCreateSpan("existing", TRACER)
def scope = ClientDecorator.currentContextWith(client)

when:
def internal = TRACER.spanBuilder("internal").setSpanKind(Span.Kind.INTERNAL).startSpan()
def scope2 = TracingContextUtils.currentContextWith(internal)

then:
assert internal.getContext().isValid()
assert ClientDecorator.CONTEXT_CLIENT_SPAN_KEY.get(Context.current()) == client
assert TracingContextUtils.getSpan(Context.current()) == internal

cleanup:
scope2.close()
scope.close()
}

def "test withSpan"() {
setup:
def span = ClientDecorator.getOrCreateSpan("test", TRACER)

when:
def context = ClientDecorator.withSpan(span, Context.ROOT)

then:
assert ClientDecorator.CONTEXT_CLIENT_SPAN_KEY.get(context) == span
assert TracingContextUtils.getSpan(context) == span
}

def "test currentContextWith"() {
setup:
def span = ClientDecorator.getOrCreateSpan("test", TRACER)

when:
def scope = ClientDecorator.currentContextWith(span)

then:
assert ClientDecorator.CONTEXT_CLIENT_SPAN_KEY.get(Context.current()) == span
assert TracingContextUtils.getSpan(Context.current()) == span

cleanup:
scope.close()
}

@Override
def newDecorator() {
return newDecorator("test-service")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkClientDecorator.DECORATE;

import io.opentelemetry.auto.bootstrap.instrumentation.decorator.ClientDecorator;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.Span.Kind;
import software.amazon.awssdk.core.interceptor.Context;
Expand All @@ -40,10 +41,7 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor {
public void beforeExecution(
final Context.BeforeExecution context, final ExecutionAttributes executionAttributes) {
final Span span =
AwsSdk.tracer()
.spanBuilder(DECORATE.spanName(executionAttributes))
.setSpanKind(kind)
.startSpan();
ClientDecorator.getOrCreateSpan(DECORATE.spanName(executionAttributes), AwsSdk.tracer());
DECORATE.afterStart(span);
executionAttributes.putAttribute(SPAN_ATTRIBUTE, span);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
import static io.opentelemetry.auto.tooling.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.auto.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.context.ContextUtils.withScopedContext;
import static io.opentelemetry.trace.Span.Kind.CLIENT;
import static io.opentelemetry.trace.TracingContextUtils.withSpan;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
Expand All @@ -34,6 +32,7 @@
import io.grpc.Context;
import io.opentelemetry.OpenTelemetry;
import io.opentelemetry.auto.bootstrap.CallDepthThreadLocalMap;
import io.opentelemetry.auto.bootstrap.instrumentation.decorator.ClientDecorator;
import io.opentelemetry.auto.instrumentation.api.SpanWithScope;
import io.opentelemetry.auto.tooling.Instrumenter;
import io.opentelemetry.context.Scope;
Expand Down Expand Up @@ -172,16 +171,14 @@ public Map<? extends ElementMatcher<? super MethodDescription>, String> transfor

public static class HelperMethods {
public static SpanWithScope doMethodEnter(final HttpUriRequest request) {
final Span span =
TRACER.spanBuilder(DECORATE.spanNameForRequest(request)).setSpanKind(CLIENT).startSpan();
final Span span = DECORATE.getOrCreateSpan(request, TRACER);

DECORATE.afterStart(span);
DECORATE.onRequest(span, request);

final Context context = withSpan(span, Context.current());
final boolean awsClientCall = request.getHeaders("amz-sdk-invocation-id").length > 0;
// AWS calls are often signed, so we can't add headers without breaking the signature.
if (!awsClientCall) {
Comment on lines -183 to -184
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing you know something that we don't about this not being needed anymore? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that there's only the SDK span and no Apache span, there's nothing to propagate here. That being said, it does seem safer to keep the check just in case something changes, at the same time, it seems pretty hacky to have the AWS-specific code here so it feels nice to remove it.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I understand now, right.

Do the aws-sdk tests protect us here? If so I'm good removing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup the tests cover it. It's how I found this line of code :)

final Context context = ClientDecorator.withSpan(span, Context.current());
// TODO(anuraaga): Seems like a bug that invalid context still gets injected by the injector.
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 noticed this is fixed in master of the SDK :)

if (span.getContext().isValid()) {
OpenTelemetry.getPropagators().getHttpTextFormat().inject(context, request, SETTER);
}
final Scope scope = withScopedContext(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
package io.opentelemetry.auto.instrumentation.awssdk.v1_11;

import static io.opentelemetry.auto.instrumentation.awssdk.v1_11.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY;
import static io.opentelemetry.trace.TracingContextUtils.currentContextWith;

import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.handlers.RequestHandler2;
import io.opentelemetry.OpenTelemetry;
import io.opentelemetry.auto.bootstrap.ContextStore;
import io.opentelemetry.auto.bootstrap.instrumentation.decorator.ClientDecorator;
import io.opentelemetry.auto.instrumentation.api.SpanWithScope;
import io.opentelemetry.trace.Span;
import io.opentelemetry.trace.Tracer;
Expand All @@ -47,11 +47,12 @@ public AmazonWebServiceRequest beforeMarshalling(final AmazonWebServiceRequest r

@Override
public void beforeRequest(final Request<?> request) {
final Span span = TRACER.spanBuilder(decorate.spanNameForRequest(request)).startSpan();
final Span span = decorate.getOrCreateSpan(request, TRACER);
decorate.afterStart(span);
decorate.onRequest(span, request);
request.addHandlerContext(
SPAN_SCOPE_PAIR_CONTEXT_KEY, new SpanWithScope(span, currentContextWith(span)));
SPAN_SCOPE_PAIR_CONTEXT_KEY,
new SpanWithScope(span, ClientDecorator.currentContextWith(span)));
}

@Override
Expand Down
Loading