Split async and sync advice and misc cleanup.

This commit is contained in:
Tyler Benson 2019-04-08 14:04:31 -07:00
parent 315ae67fd2
commit b9207133d6
5 changed files with 171 additions and 49 deletions

View File

@ -6,9 +6,9 @@ import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract; import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not; import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
@ -20,7 +20,6 @@ import datadog.trace.bootstrap.CallDepthThreadLocalMap;
import datadog.trace.context.TraceScope; import datadog.trace.context.TraceScope;
import io.opentracing.Scope; import io.opentracing.Scope;
import io.opentracing.Span; import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer; import io.opentracing.util.GlobalTracer;
import java.util.Map; import java.util.Map;
import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.Advice;
@ -29,17 +28,15 @@ import net.bytebuddy.matcher.ElementMatcher;
/** Instrument the Twilio SDK to identify calls as a seperate service. */ /** Instrument the Twilio SDK to identify calls as a seperate service. */
@AutoService(Instrumenter.class) @AutoService(Instrumenter.class)
public class TwilioInstrumentation extends Instrumenter.Default { public class TwilioAsyncInstrumentation extends Instrumenter.Default {
public TwilioInstrumentation() { public TwilioAsyncInstrumentation() {
super("twilio-sdk"); super("twilio-sdk");
} }
/** Match any child class of the base Twilio service classes. */ /** Match any child class of the base Twilio service classes. */
@Override @Override
public net.bytebuddy.matcher.ElementMatcher< public ElementMatcher<? super net.bytebuddy.description.type.TypeDescription> typeMatcher() {
? super net.bytebuddy.description.type.TypeDescription>
typeMatcher() {
return safeHasSuperType( return safeHasSuperType(
named("com.twilio.base.Creator") named("com.twilio.base.Creator")
.or(named("com.twilio.base.Deleter")) .or(named("com.twilio.base.Deleter"))
@ -55,7 +52,7 @@ public class TwilioInstrumentation extends Instrumenter.Default {
"datadog.trace.agent.decorator.BaseDecorator", "datadog.trace.agent.decorator.BaseDecorator",
"datadog.trace.agent.decorator.ClientDecorator", "datadog.trace.agent.decorator.ClientDecorator",
packageName + ".TwilioClientDecorator", packageName + ".TwilioClientDecorator",
packageName + ".TwilioInstrumentation$SpanFinishingCallback" packageName + ".TwilioAsyncInstrumentation$SpanFinishingCallback",
}; };
} }
@ -75,16 +72,17 @@ public class TwilioInstrumentation extends Instrumenter.Default {
.and(isPublic()) .and(isPublic())
.and(not(isAbstract())) .and(not(isAbstract()))
.and( .and(
nameStartsWith("create") named("createAsync")
.or(nameStartsWith("delete")) .or(named("deleteAsync"))
.or(nameStartsWith("read")) .or(named("readAsync"))
.or(nameStartsWith("fetch")) .or(named("fetchAsync"))
.or(nameStartsWith("update"))), .or(named("updateAsync")))
TwilioClientAdvice.class.getName()); .and(returns(named("com.google.common.util.concurrent.ListenableFuture"))),
TwilioClientAsyncAdvice.class.getName());
} }
/** Advice for instrumenting Twilio service classes. */ /** Advice for instrumenting Twilio service classes. */
public static class TwilioClientAdvice { public static class TwilioClientAsyncAdvice {
/** Method entry instrumentation. */ /** Method entry instrumentation. */
@Advice.OnMethodEnter(suppress = Throwable.class) @Advice.OnMethodEnter(suppress = Throwable.class)
@ -100,20 +98,15 @@ public class TwilioInstrumentation extends Instrumenter.Default {
return null; return null;
} }
// By convention, all Twilio async methods end with Async
final boolean isAsync = methodName.endsWith("Async");
final Tracer tracer = GlobalTracer.get();
// Don't automatically close the span with the scope if we're executing an async method // Don't automatically close the span with the scope if we're executing an async method
final Scope scope = tracer.buildSpan("twilio.sdk").startActive(!isAsync); final Scope scope = GlobalTracer.get().buildSpan("twilio.sdk").startActive(false);
final Span span = scope.span(); final Span span = scope.span();
DECORATE.afterStart(span); DECORATE.afterStart(span);
DECORATE.onServiceExecution(span, that, methodName); DECORATE.onServiceExecution(span, that, methodName);
// If an async operation was invoked and we have a TraceScope, // If an async operation was invoked and we have a TraceScope,
if (isAsync && scope instanceof TraceScope) { if (scope instanceof TraceScope) {
// Enable async propagation, so the newly spawned task will be associated back with this // Enable async propagation, so the newly spawned task will be associated back with this
// original trace. // original trace.
((TraceScope) scope).setAsyncPropagation(true); ((TraceScope) scope).setAsyncPropagation(true);
@ -127,35 +120,28 @@ public class TwilioInstrumentation extends Instrumenter.Default {
public static void methodExit( public static void methodExit(
@Advice.Enter final Scope scope, @Advice.Enter final Scope scope,
@Advice.Thrown final Throwable throwable, @Advice.Thrown final Throwable throwable,
@Advice.Return final Object response, @Advice.Return final ListenableFuture response) {
@Advice.Origin("#m") final String methodName) {
// If we have a scope (i.e. we were the top-level Twilio SDK invocation), // If we have a scope (i.e. we were the top-level Twilio SDK invocation),
if (scope != null) { if (scope != null) {
try { try {
final boolean isAsync = methodName.endsWith("Async");
final Span span = scope.span(); final Span span = scope.span();
DECORATE.onError(span, throwable); if (throwable != null) {
DECORATE.beforeFinish(span); // There was an synchronous error,
// which means we shouldn't wait for a callback to close the span.
// If we're calling an async operation, we still need to finish the span when it's DECORATE.onError(span, throwable);
// complete and report the results; set an appropriate callback
if (isAsync && response instanceof ListenableFuture) {
Futures.addCallback(
(ListenableFuture) response,
new SpanFinishingCallback(span),
Twilio.getExecutorService());
} else {
DECORATE.beforeFinish(span); DECORATE.beforeFinish(span);
DECORATE.onResult(span, response); span.finish();
// span is implicitly closed with the scope } else {
// We're calling an async operation, we still need to finish the span when it's
// complete and report the results; set an appropriate callback
Futures.addCallback(
response, new SpanFinishingCallback(span), Twilio.getExecutorService());
} }
} finally { } finally {
scope.close(); scope.close(); // won't finish the span.
CallDepthThreadLocalMap.reset(Twilio.class); // reset call deptch count CallDepthThreadLocalMap.reset(Twilio.class); // reset call depth count
} }
} }
} }

View File

@ -9,8 +9,10 @@ import datadog.trace.api.DDTags;
import io.opentracing.Span; import io.opentracing.Span;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/** Decorate Twilio span's with relevant contextual information. */ /** Decorate Twilio span's with relevant contextual information. */
@Slf4j
public class TwilioClientDecorator extends ClientDecorator { public class TwilioClientDecorator extends ClientDecorator {
public static final TwilioClientDecorator DECORATE = new TwilioClientDecorator(); public static final TwilioClientDecorator DECORATE = new TwilioClientDecorator();
@ -58,7 +60,7 @@ public class TwilioClientDecorator extends ClientDecorator {
try { try {
result = ((ListenableFuture) result).get(0, TimeUnit.MICROSECONDS); result = ((ListenableFuture) result).get(0, TimeUnit.MICROSECONDS);
} catch (final Exception e) { } catch (final Exception e) {
e.printStackTrace(); log.debug("Error unwrapping result", e);
} }
} }
@ -73,7 +75,6 @@ public class TwilioClientDecorator extends ClientDecorator {
// Instrument the most popular resource types directly // Instrument the most popular resource types directly
if (result instanceof Message) { if (result instanceof Message) {
final Message message = (Message) result; final Message message = (Message) result;
span.setTag("twilio.type", result.getClass().getCanonicalName());
span.setTag("twilio.account", message.getAccountSid()); span.setTag("twilio.account", message.getAccountSid());
span.setTag("twilio.sid", message.getSid()); span.setTag("twilio.sid", message.getSid());
if (message.getStatus() != null) { if (message.getStatus() != null) {

View File

@ -0,0 +1,127 @@
package datadog.trace.instrumentation.twilio;
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.twilio.TwilioClientDecorator.DECORATE;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import com.google.auto.service.AutoService;
import com.twilio.Twilio;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.CallDepthThreadLocalMap;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.util.GlobalTracer;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
/** Instrument the Twilio SDK to identify calls as a seperate service. */
@AutoService(Instrumenter.class)
public class TwilioSyncInstrumentation extends Instrumenter.Default {
public TwilioSyncInstrumentation() {
super("twilio-sdk");
}
/** Match any child class of the base Twilio service classes. */
@Override
public net.bytebuddy.matcher.ElementMatcher<
? super net.bytebuddy.description.type.TypeDescription>
typeMatcher() {
return safeHasSuperType(
named("com.twilio.base.Creator")
.or(named("com.twilio.base.Deleter"))
.or(named("com.twilio.base.Fetcher"))
.or(named("com.twilio.base.Reader"))
.or(named("com.twilio.base.Updater")));
}
/** Return the helper classes which will be available for use in instrumentation. */
@Override
public String[] helperClassNames() {
return new String[] {
"datadog.trace.agent.decorator.BaseDecorator",
"datadog.trace.agent.decorator.ClientDecorator",
packageName + ".TwilioClientDecorator",
};
}
/** Return bytebuddy transformers for instrumenting the Twilio SDK. */
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
/*
We are listing out the main service calls on the Creator, Deleter, Fetcher, Reader, and
Updater abstract classes. The isDeclaredBy() matcher did not work in the unit tests and
we found that there were certain methods declared on the base class (particularly Reader),
which we weren't interested in annotating.
*/
return singletonMap(
isMethod()
.and(isPublic())
.and(not(isAbstract()))
.and(
named("create")
.or(named("delete"))
.or(named("read"))
.or(named("fetch"))
.or(named("update"))),
TwilioClientAdvice.class.getName());
}
/** Advice for instrumenting Twilio service classes. */
public static class TwilioClientAdvice {
/** Method entry instrumentation. */
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope methodEnter(
@Advice.This final Object that, @Advice.Origin("#m") final String methodName) {
// Ensure that we only create a span for the top-level Twilio client method; except in the
// case of async operations where we want visibility into how long the task was delayed from
// starting. Our call depth checker does not span threads, so the async case is handled
// automatically for us.
final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(Twilio.class);
if (callDepth > 0) {
return null;
}
final Scope scope = GlobalTracer.get().buildSpan("twilio.sdk").startActive(true);
final Span span = scope.span();
DECORATE.afterStart(span);
DECORATE.onServiceExecution(span, that, methodName);
return scope;
}
/** Method exit instrumentation. */
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(
@Advice.Enter final Scope scope,
@Advice.Thrown final Throwable throwable,
@Advice.Return final Object response) {
// If we have a scope (i.e. we were the top-level Twilio SDK invocation),
if (scope != null) {
try {
final Span span = scope.span();
DECORATE.onResult(span, response);
DECORATE.onError(span, throwable);
DECORATE.beforeFinish(span);
} finally {
scope.close();
CallDepthThreadLocalMap.reset(Twilio.class); // reset call depth count
}
}
}
}
}

View File

@ -511,19 +511,21 @@ class TwilioClientTest extends AgentTestRunner {
defaultTags() defaultTags()
} }
} }
// Spans are reported in reverse order of completion,
// so the error span is last even though it happened first.
span(3) { span(3) {
serviceName "twilio-sdk" serviceName "twilio-sdk"
operationName "http.request" operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json" resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT spanType DDSpanTypes.HTTP_CLIENT
errored true errored false
} }
span(4) { span(4) {
serviceName "twilio-sdk" serviceName "twilio-sdk"
operationName "http.request" operationName "http.request"
resourceName "POST /?/Accounts/abc/Messages.json" resourceName "POST /?/Accounts/abc/Messages.json"
spanType DDSpanTypes.HTTP_CLIENT spanType DDSpanTypes.HTTP_CLIENT
errored false errored true
} }
} }
} }
@ -617,7 +619,6 @@ class TwilioClientTest extends AgentTestRunner {
} }
} }
} }
} }
def "asynchronous call"(a) { def "asynchronous call"(a) {

View File

@ -8,8 +8,14 @@ muzzle {
apply from: "${rootDir}/gradle/java.gradle" apply from: "${rootDir}/gradle/java.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
latestDepTest
}
dependencies { dependencies {
compileOnly group: 'com.twilio.sdk', name: 'twilio', version: '7.36.2' compileOnly group: 'com.twilio.sdk', name: 'twilio', version: '0.0.1'
compile project(':dd-java-agent:agent-tooling') compile project(':dd-java-agent:agent-tooling')
@ -18,11 +24,12 @@ dependencies {
annotationProcessor deps.autoservice annotationProcessor deps.autoservice
implementation deps.autoservice implementation deps.autoservice
testCompile group: 'com.twilio.sdk', name: 'twilio', version: '7.36.2' testCompile group: 'com.twilio.sdk', name: 'twilio', version: '0.0.1'
testCompile project(':dd-java-agent:testing') testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4') testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
testCompile project(':dd-java-agent:instrumentation:java-concurrent') testCompile project(':dd-java-agent:instrumentation:java-concurrent')
testCompile group: 'org.objenesis', name: 'objenesis', version: '2.6' // Last version to support Java7 testCompile group: 'org.objenesis', name: 'objenesis', version: '2.6' // Last version to support Java7
testCompile group: 'nl.jqno.equalsverifier', name: 'equalsverifier', version: '2.5.2' // Last version to support Java7 testCompile group: 'nl.jqno.equalsverifier', name: 'equalsverifier', version: '2.5.2' // Last version to support Java7
latestDepTestCompile group: 'com.twilio.sdk', name: 'twilio', version: '+'
} }