Move OkHttp instrumentation to byte buddy

This commit is contained in:
Tyler Benson 2017-12-06 15:16:33 -08:00
parent 50c8c0268a
commit d933a4049a
24 changed files with 247 additions and 167 deletions

View File

@ -1,13 +1,17 @@
apply from: "${rootDir}/gradle/java.gradle"
description = 'dd-java-agent-ittests'
evaluationDependsOn(':dd-java-agent:tooling')
compileTestJava.dependsOn tasks.getByPath(':dd-java-agent:tooling:testClasses')
dependencies {
testCompile project(':dd-trace-annotations')
testCompile project(':dd-trace')
testCompile deps.opentracingMock
testCompile deps.testLogging
testCompile project(':dd-java-agent:tooling').sourceSets.test.output
testCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.4.2'
testCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.4.2'

View File

@ -1,19 +1,18 @@
package com.datadoghq.agent.integration.httpclient
import com.datadoghq.agent.integration.TestUtils
import com.datadoghq.agent.integration.TestHttpServer
import com.datadoghq.trace.DDBaseSpan
import com.datadoghq.trace.DDTracer
import com.datadoghq.trace.writer.ListWriter
import dd.test.TestUtils
import io.opentracing.tag.Tags
import java.net.URI
import java.util.List
import org.apache.http.HttpResponse
import org.apache.http.message.BasicHeader
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClientBuilder
import spock.lang.*
import org.apache.http.message.BasicHeader
import spock.lang.Shared
import spock.lang.Specification
class ApacheHttpClientTest extends Specification {
@ -40,21 +39,17 @@ class ApacheHttpClientTest extends Specification {
final HttpClientBuilder builder = HttpClientBuilder.create()
final HttpClient client = builder.build()
TestUtils.runUnderTrace(
"someTrace",
new Runnable() {
@Override
public void run() {
try {
HttpResponse response =
client.execute(new HttpGet(new URI("http://localhost:" + TestHttpServer.getPort())))
assert response.getStatusLine().getStatusCode() == 200
} catch (Exception e) {
e.printStackTrace()
throw new RuntimeException(e)
}
}
})
TestUtils.runUnderTrace("someTrace") {
try {
HttpResponse response =
client.execute(new HttpGet(new URI("http://localhost:" + TestHttpServer.getPort())))
assert response.getStatusLine().getStatusCode() == 200
} catch (Exception e) {
e.printStackTrace()
throw new RuntimeException(e)
}
}
expect:
// one trace on the server, one trace on the client
writer.size() == 2
@ -90,23 +85,18 @@ class ApacheHttpClientTest extends Specification {
final HttpClientBuilder builder = HttpClientBuilder.create()
final HttpClient client = builder.build()
TestUtils.runUnderTrace(
"someTrace",
new Runnable() {
@Override
public void run() {
try {
HttpGet request = new HttpGet(new URI("http://localhost:"
+ TestHttpServer.getPort()))
request.addHeader(new BasicHeader(TestHttpServer.IS_DD_SERVER, "false"))
HttpResponse response = client.execute(request)
assert response.getStatusLine().getStatusCode() == 200
} catch (Exception e) {
e.printStackTrace()
throw new RuntimeException(e)
}
}
})
TestUtils.runUnderTrace("someTrace") {
try {
HttpGet request = new HttpGet(new URI("http://localhost:"
+ TestHttpServer.getPort()))
request.addHeader(new BasicHeader(TestHttpServer.IS_DD_SERVER, "false"))
HttpResponse response = client.execute(request)
assert response.getStatusLine().getStatusCode() == 200
} catch (Exception e) {
e.printStackTrace()
throw new RuntimeException(e)
}
}
expect:
// only one trace (client).
writer.size() == 1

View File

@ -2,12 +2,12 @@ package com.datadoghq.agent.instrumentation.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import com.datadoghq.agent.integration.TestUtils;
import com.datadoghq.agent.test.SayTracedHello;
import com.datadoghq.trace.DDBaseSpan;
import com.datadoghq.trace.DDTracer;
import com.datadoghq.trace.integration.ErrorFlag;
import com.datadoghq.trace.writer.ListWriter;
import dd.test.TestUtils;
import io.opentracing.util.GlobalTracer;
import java.io.PrintWriter;
import java.io.StringWriter;

View File

@ -7,6 +7,7 @@ import com.datadoghq.trace.DDTracer;
import com.datadoghq.trace.writer.ListWriter;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Session;
import dd.test.TestUtils;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
@ -47,7 +48,7 @@ public class CassandraIntegrationTest {
session.execute("SELECT * FROM sync_test.users where name = 'alice' ALLOW FILTERING");
assertThat(writer.getList().size()).isEqualTo(origSize + 5);
DDBaseSpan<?> selectTrace = writer.get(writer.size() - 1).get(0);
final DDBaseSpan<?> selectTrace = writer.get(writer.size() - 1).get(0);
assertThat(selectTrace.getServiceName()).isEqualTo(DDTracer.UNASSIGNED_DEFAULT_SERVICE_NAME);
assertThat(selectTrace.getOperationName()).isEqualTo("execute");
@ -89,7 +90,7 @@ public class CassandraIntegrationTest {
}
Thread.sleep(1);
}
DDBaseSpan<?> selectTrace = writer.get(writer.size() - 1).get(0);
final DDBaseSpan<?> selectTrace = writer.get(writer.size() - 1).get(0);
assertThat(selectTrace.getServiceName()).isEqualTo(DDTracer.UNASSIGNED_DEFAULT_SERVICE_NAME);
assertThat(selectTrace.getOperationName()).isEqualTo("execute");

View File

@ -11,6 +11,7 @@ import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClients;
import com.mongodb.async.client.MongoDatabase;
import dd.test.TestUtils;
import io.opentracing.tag.Tags;
import java.net.InetAddress;
import java.nio.ByteBuffer;
@ -52,7 +53,7 @@ public class MongoAsyncClientInstrumentationTest {
@Test
public void insertOperation() throws InterruptedException, Exception {
MongoDatabase db = client.getDatabase(MONGO_DB_NAME);
final MongoDatabase db = client.getDatabase(MONGO_DB_NAME);
final String collectionName = "asyncCollection";
final AtomicBoolean done = new AtomicBoolean(false);
@ -60,7 +61,7 @@ public class MongoAsyncClientInstrumentationTest {
collectionName,
new SingleResultCallback<Void>() {
@Override
public void onResult(Void result, Throwable t) {
public void onResult(final Void result, final Throwable t) {
done.set(true);
}
});
@ -86,7 +87,7 @@ public class MongoAsyncClientInstrumentationTest {
.count(
new SingleResultCallback<Long>() {
@Override
public void onResult(Long result, Throwable t) {
public void onResult(final Long result, final Throwable t) {
Assert.assertEquals(1, result.longValue());
done.set(true);
}

View File

@ -6,7 +6,10 @@ import com.datadoghq.trace.writer.ListWriter;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import de.flapdoodle.embed.mongo.*;
import dd.test.TestUtils;
import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodProcess;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.IMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
import de.flapdoodle.embed.mongo.config.Net;
@ -34,9 +37,9 @@ public class MongoClientInstrumentationTest {
private static final DDTracer tracer = new DDTracer(writer);
public static void startLocalMongo() throws Exception {
MongodStarter starter = MongodStarter.getDefaultInstance();
final MongodStarter starter = MongodStarter.getDefaultInstance();
IMongodConfig mongodConfig =
final IMongodConfig mongodConfig =
new MongodConfigBuilder()
.version(Version.Main.PRODUCTION)
.net(new Net(MONGO_HOST, MONGO_PORT, Network.localhostIsIPv6()))
@ -84,10 +87,10 @@ public class MongoClientInstrumentationTest {
@Test
public void insertOperation() throws UnknownHostException {
MongoDatabase db = client.getDatabase(MONGO_DB_NAME);
final MongoDatabase db = client.getDatabase(MONGO_DB_NAME);
final String collectionName = "testCollection";
db.createCollection(collectionName);
MongoCollection<Document> collection = db.getCollection(collectionName);
final MongoCollection<Document> collection = db.getCollection(collectionName);
collection.insertOne(new Document("foo", "bar"));

View File

@ -1,28 +0,0 @@
package com.datadoghq.agent.integration;
import io.opentracing.ActiveSpan;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;
import java.lang.reflect.Field;
public class TestUtils {
public static void registerOrReplaceGlobalTracer(Tracer tracer) throws Exception {
try {
GlobalTracer.register(tracer);
} catch (final Exception e) {
// Force it anyway using reflection
final Field field = GlobalTracer.class.getDeclaredField("tracer");
field.setAccessible(true);
field.set(null, tracer);
}
}
public static void runUnderTrace(final String rootOperationName, Runnable r) {
ActiveSpan rootSpan = GlobalTracer.get().buildSpan(rootOperationName).startActive();
try {
r.run();
} finally {
rootSpan.deactivate();
}
}
}

View File

@ -0,0 +1 @@
enableCustomAnnotationTracingOver: ["com.datadoghq.benchmark"]

View File

@ -42,6 +42,9 @@ dependencies {
compile(project(':dd-java-agent:integrations:mongo-async-3.3')) {
transitive = false
}
compile(project(':dd-java-agent:integrations:okhttp-3')) {
transitive = false
}
compile(project(':dd-java-agent:integrations:servlet-2')) {
transitive = false
}
@ -61,7 +64,6 @@ dependencies {
compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25'
// ^ Generally a bad idea for libraries, but we're shadowing.
testCompile deps.testLogging
testCompile deps.opentracingMock
testCompile(project(path: ':dd-java-agent:integrations:helpers')) {

View File

@ -1,33 +0,0 @@
package com.datadoghq.agent.integration;
import static io.opentracing.contrib.okhttp3.OkHttpClientSpanDecorator.STANDARD_TAGS;
import io.opentracing.contrib.okhttp3.TracingInterceptor;
import java.util.Collections;
import okhttp3.OkHttpClient;
import org.jboss.byteman.rule.Rule;
/** Patch the OkHttp Client during the building steps. */
public class OkHttpHelper extends DDAgentTracingHelper<OkHttpClient.Builder> {
public OkHttpHelper(Rule rule) {
super(rule);
}
/**
* Strategy: Just before the okhttp3.OkHttpClient$Builder.build() method called, we add a new
* interceptor for the tracing part.
*
* @param builder The builder instance to patch
* @return The same builder instance with a new tracing interceptor
* @throws Exception
*/
protected OkHttpClient.Builder doPatch(OkHttpClient.Builder builder) throws Exception {
TracingInterceptor interceptor =
new TracingInterceptor(tracer, Collections.singletonList(STANDARD_TAGS));
builder.addInterceptor(interceptor);
builder.addNetworkInterceptor(interceptor);
return builder;
}
}

View File

@ -26,7 +26,6 @@ dependencies {
testCompile project(':dd-java-agent:tooling').sourceSets.test.output
testCompile project(':dd-java-agent:integrations:helpers')
testCompile deps.testLogging
testCompile group: 'org.apache.activemq.tooling', name: 'activemq-junit', version: '5.14.5'
testCompile group: 'org.apache.activemq', name: 'activemq-pool', version: '5.14.5'
testCompile group: 'org.apache.activemq', name: 'activemq-broker', version: '5.14.5'

View File

@ -26,7 +26,7 @@ class JMS1Test extends Specification {
def setupSpec() {
TestUtils.addByteBuddyAgent()
TestUtils.addTracer(tracer)
TestUtils.registerOrReplaceGlobalTracer(tracer)
EmbeddedActiveMQBroker broker = new EmbeddedActiveMQBroker()
broker.start()

View File

@ -26,7 +26,6 @@ dependencies {
testCompile project(':dd-java-agent:tooling').sourceSets.test.output
testCompile project(':dd-java-agent:integrations:helpers')
testCompile deps.testLogging
testCompile group: 'org.hornetq', name: 'hornetq-jms-client', version: '2.4.7.Final'
testCompile group: 'org.hornetq', name: 'hornetq-jms-server', version: '2.4.7.Final'

View File

@ -35,7 +35,7 @@ class JMS2Test extends Specification {
def setupSpec() {
TestUtils.addByteBuddyAgent()
TestUtils.addTracer(tracer)
TestUtils.registerOrReplaceGlobalTracer(tracer)
def tempDir = Files.createTempDir()
tempDir.deleteOnExit()

View File

@ -0,0 +1,28 @@
apply plugin: 'version-scan'
versionScan {
group = "com.squareup.okhttp3"
module = "okhttp"
versions = "[3.0,)"
legacyGroup = "com.squareup.okhttp"
verifyPresent = [
"okhttp3.Cookie" : null,
"okhttp3.ConnectionPool": null,
"okhttp3.Headers" : null,
]
}
apply from: "${rootDir}/gradle/java.gradle"
dependencies {
compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0'
compile project(':dd-trace')
compile project(':dd-java-agent:integrations:helpers')
compile project(':dd-java-agent:tooling')
compile deps.bytebuddy
compile deps.opentracing
testCompile project(':dd-java-agent:tooling').sourceSets.test.output
}

View File

@ -0,0 +1,47 @@
package dd.inst.okhttp3;
import static dd.trace.ClassLoaderMatcher.classLoaderHasClasses;
import static dd.trace.ExceptionHandlers.defaultExceptionHandler;
import static io.opentracing.contrib.okhttp3.OkHttpClientSpanDecorator.STANDARD_TAGS;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import com.google.auto.service.AutoService;
import dd.trace.Instrumenter;
import io.opentracing.contrib.okhttp3.TracingInterceptor;
import io.opentracing.util.GlobalTracer;
import java.util.Collections;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import okhttp3.OkHttpClient;
@AutoService(Instrumenter.class)
public class OkHttp3Instrumentation implements Instrumenter {
@Override
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
return agentBuilder
.type(
named("okhttp3.OkHttpClient"),
classLoaderHasClasses("okhttp3.Cookie", "okhttp3.ConnectionPool", "okhttp3.Headers"))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
isConstructor().and(takesArgument(0, named("okhttp3.OkHttpClient$Builder"))),
OkHttp3Advice.class.getName())
.withExceptionHandler(defaultExceptionHandler()))
.asDecorator();
}
public static class OkHttp3Advice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void addTracingInterceptor(
@Advice.Argument(0) final OkHttpClient.Builder builder) {
final TracingInterceptor interceptor =
new TracingInterceptor(GlobalTracer.get(), Collections.singletonList(STANDARD_TAGS));
builder.addInterceptor(interceptor);
builder.addNetworkInterceptor(interceptor);
}
}
}

View File

@ -0,0 +1,103 @@
import com.datadoghq.trace.DDTags
import com.datadoghq.trace.DDTracer
import com.datadoghq.trace.writer.ListWriter
import dd.test.TestUtils
import io.opentracing.tag.Tags
import okhttp3.OkHttpClient
import okhttp3.Request
import ratpack.http.Headers
import spock.lang.Shared
import spock.lang.Specification
import java.util.concurrent.atomic.AtomicReference
import static ratpack.groovy.test.embed.GroovyEmbeddedApp.ratpack
class OkHttp3Test extends Specification {
@Shared
def writer = new ListWriter()
@Shared
def tracer = new DDTracer(writer)
def setupSpec() {
TestUtils.addByteBuddyAgent()
TestUtils.registerOrReplaceGlobalTracer(tracer)
}
def setup() {
writer.start()
}
def "sending a request creates spans and sends headers"() {
setup:
def receivedHeaders = new AtomicReference<Headers>()
def server = ratpack {
handlers {
all {
receivedHeaders.set(request.headers)
response.status(200).send("pong")
}
}
}
def client = new OkHttpClient();
def request = new Request.Builder()
.url("http://localhost:$server.address.port/ping")
.build();
def response = client.newCall(request).execute()
expect:
response.body.string() == "pong"
writer.size() == 1
def trace = writer.firstTrace()
trace.size() == 2
and: // span 0
def span1 = trace[0]
span1.context().operationName == "GET"
span1.serviceName == "unnamed-java-app"
span1.resourceName == "GET"
span1.type == null
!span1.context().getErrorFlag()
span1.context().parentId == 0
def tags1 = span1.context().tags
tags1["component"] == "okhttp"
tags1["thread.name"] != null
tags1["thread.id"] != null
tags1.size() == 3
and: // span 1
def span2 = trace[1]
span2.context().operationName == "GET"
span2.serviceName == "unnamed-java-app"
span2.resourceName == "GET"
span2.type == null
!span2.context().getErrorFlag()
span2.context().parentId == span1.spanId
def tags2 = span2.context().tags
tags2[Tags.COMPONENT.key] == "okhttp"
tags2[Tags.SPAN_KIND.key] == Tags.SPAN_KIND_CLIENT
tags2[Tags.HTTP_METHOD.key] == "GET"
tags2[Tags.HTTP_URL.key] == "http://localhost:$server.address.port/ping"
tags2[Tags.PEER_HOSTNAME.key] == "localhost"
tags2[Tags.PEER_PORT.key] == server.address.port
tags2[Tags.PEER_HOST_IPV4.key] != null
tags2[DDTags.THREAD_NAME] != null
tags2[DDTags.THREAD_ID] != null
tags2.size() == 10
receivedHeaders.get().get("x-datadog-trace-id") == "$span2.traceId"
receivedHeaders.get().get("x-datadog-parent-id") == "$span2.spanId"
cleanup:
server.close()
}
}

View File

@ -1,13 +0,0 @@
apply plugin: 'version-scan'
versionScan {
group = "com.squareup.okhttp3"
module = "okhttp"
versions = "[3.0,)"
legacyGroup = "com.squareup.okhttp"
verifyPresent = [
"okhttp3.Cookie" : null,
"okhttp3.ConnectionPool": null,
"okhttp3.Headers" : null,
]
}

View File

@ -1,12 +0,0 @@
# Instrument OkHttp
# ===========================
RULE OkHttpClient$Builder-init
CLASS okhttp3.OkHttpClient$Builder
METHOD <init>
AT EXIT
IF TRUE
DO
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
ENDRULE

View File

@ -1,21 +0,0 @@
### The contributions are injected using this file and byteman
###
### Helpers are a DD specific, they are defined in the current project
### The rule names provided (RULE <rulename>) are also used in the configuration.
### The name is used to remove the script if there is no support of the framework
###
### So, ensure that the rule name is correct defined both here and in the dd-trace-supported-framework.yaml
###
# Instrument OkHttp
# ===========================
RULE opentracing-okhttp3
CLASS okhttp3.OkHttpClient$Builder
METHOD build()
HELPER com.datadoghq.agent.integration.OkHttpHelper
AT ENTRY
IF TRUE
DO
patch($0)
ENDRULE

View File

@ -1,14 +1,15 @@
package dd.test
import dd.trace.Instrumenter
import io.opentracing.ActiveSpan
import io.opentracing.Tracer
import io.opentracing.util.GlobalTracer
import net.bytebuddy.agent.ByteBuddyAgent
import net.bytebuddy.agent.builder.AgentBuilder
import java.lang.reflect.Field
import java.util.concurrent.Callable
import static net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy.Listener.StreamWriting.toSystemError
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith
import static org.assertj.core.api.Assertions.assertThat
@ -19,7 +20,7 @@ class TestUtils {
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(toSystemError())
// .with(AgentBuilder.Listener.StreamWriting.toSystemError())
.ignore(nameStartsWith("dd.inst"))
def instrumenters = ServiceLoader.load(Instrumenter)
@ -30,7 +31,7 @@ class TestUtils {
builder.installOn(ByteBuddyAgent.install())
}
static addTracer(Tracer tracer) {
static registerOrReplaceGlobalTracer(Tracer tracer) {
try {
Class.forName("com.datadoghq.agent.InstrumentationRulesManager")
.getMethod("registerClassLoad")
@ -47,4 +48,13 @@ class TestUtils {
}
assertThat(GlobalTracer.isRegistered()).isTrue()
}
static runUnderTrace(final String rootOperationName, Callable r) {
ActiveSpan rootSpan = GlobalTracer.get().buildSpan(rootOperationName).startActive();
try {
return r.call();
} finally {
rootSpan.deactivate();
}
}
}

View File

@ -27,8 +27,6 @@ dependencies {
compile deps.autoservice
compile group: 'org.msgpack', name: 'jackson-dataformat-msgpack', version: '0.8.13'
testCompile deps.testLogging
testCompile group: 'org.objenesis', name: 'objenesis', version: '2.6'
testCompile group: 'cglib', name: 'cglib-nodep', version: '3.2.5'

View File

@ -30,6 +30,7 @@ dependencies {
testCompile deps.spock
testCompile deps.groovy
testCompile deps.testLogging
testCompile group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.4.6'
}

View File

@ -21,7 +21,7 @@ include ':dd-java-agent:integrations:jms-1'
include ':dd-java-agent:integrations:jms-2'
include ':dd-java-agent:integrations:mongo-3.1'
include ':dd-java-agent:integrations:mongo-async-3.3'
include ':dd-java-agent:integrations:okhttp'
include ':dd-java-agent:integrations:okhttp-3'
include ':dd-java-agent:integrations:servlet-2'
include ':dd-java-agent:integrations:servlet-3'
include ':dd-java-agent:integrations:spring-web'