Add commons-httpclient integration
ADD
dd-java-agent/instrumentation/commons-httpclient-3/commons-httpclient-3.gradle
+muzzle {
+ // There are lots of weird versions in this project like "20020423"
+ // Keep things simple with just 3.x
+ pass {
+ group = "commons-httpclient"
+ module = "commons-httpclient"
+ versions = "3.+"
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest {
+ dirName = 'test'
+ }
+}
+
+dependencies {
+ compileOnly group: 'commons-httpclient', name: 'commons-httpclient', version: '3.0'
+
+ testCompile group: 'commons-httpclient', name: 'commons-httpclient', version: '3.0'
+
+ latestDepTestCompile group: 'commons-httpclient', name: 'commons-httpclient', version: '3.+'
+}

ADD
dd-java-agent/instrumentation/commons-httpclient-3/src/main/java/datadog/trace/instrumentation/commonshttpclient/CommonsHttpClientDecorator.java
+package datadog.trace.instrumentation.commonshttpclient;
+
+import datadog.trace.agent.decorator.HttpClientDecorator;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.StatusLine;
+import org.apache.commons.httpclient.URIException;
+
+public class CommonsHttpClientDecorator extends HttpClientDecorator<HttpMethod, HttpMethod> {
+ public static final CommonsHttpClientDecorator DECORATE = new CommonsHttpClientDecorator();
+
+ @Override
+ protected String[] instrumentationNames() {
+ return new String[] {"commons-http-client"};
+ }
+
+ @Override
+ protected String component() {
+ return "commons-http-client";
+ }
+
+ @Override
+ protected String method(final HttpMethod httpMethod) {
+ return httpMethod.getName();
+ }
+
+ @Override
+ protected URI url(final HttpMethod httpMethod) throws URISyntaxException {
+ try {
+ // org.apache.commons.httpclient.URI -> java.net.URI
+ return new URI(httpMethod.getURI().toString());
+ } catch (final URIException e) {
+ throw new URISyntaxException("", e.getMessage());
+ }
+ }
+
+ @Override
+ protected String hostname(final HttpMethod httpMethod) {
+ try {
+ return httpMethod.getURI().getHost();
+ } catch (final URIException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected Integer port(final HttpMethod httpMethod) {
+ try {
+ return httpMethod.getURI().getPort();
+ } catch (final URIException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected Integer status(final HttpMethod httpMethod) {
+ final StatusLine statusLine = httpMethod.getStatusLine();
+ return statusLine == null ? null : statusLine.getStatusCode();
+ }
+}

ADD
dd-java-agent/instrumentation/commons-httpclient-3/src/main/java/datadog/trace/instrumentation/commonshttpclient/CommonsHttpClientInstrumentation.java
+package datadog.trace.instrumentation.commonshttpclient;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasNoResources;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.extendsClass;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.propagate;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
+import static datadog.trace.instrumentation.commonshttpclient.CommonsHttpClientDecorator.DECORATE;
+import static datadog.trace.instrumentation.commonshttpclient.HttpHeadersInjectAdapter.SETTER;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.bootstrap.CallDepthThreadLocalMap;
+import datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethod;
+
+@AutoService(Instrumenter.class)
+public class CommonsHttpClientInstrumentation extends Instrumenter.Default {
+
+ public CommonsHttpClientInstrumentation() {
+ super("commons-http-client");
+ }
+
+ @Override
+ public ElementMatcher<ClassLoader> classLoaderMatcher() {
+ // Optimization for expensive typeMatcher.
+ return not(classLoaderHasNoResources("org/apache/commons/httpclient/HttpClient.class"));
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return extendsClass(named("org.apache.commons.httpclient.HttpClient"));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ "datadog.trace.agent.decorator.BaseDecorator",
+ "datadog.trace.agent.decorator.ClientDecorator",
+ "datadog.trace.agent.decorator.HttpClientDecorator",
+ packageName + ".CommonsHttpClientDecorator",
+ packageName + ".HttpHeadersInjectAdapter",
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+
+ return singletonMap(
+ isMethod()
+ .and(named("executeMethod"))
+ .and(takesArguments(3))
+ .and(takesArgument(1, named("org.apache.commons.httpclient.HttpMethod"))),
+ CommonsHttpClientInstrumentation.class.getName() + "$ExecAdvice");
+ }
+
+ public static class ExecAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static AgentScope methodEnter(@Advice.Argument(1) final HttpMethod httpMethod) {
+ final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
+ if (callDepth > 0) {
+ return null;
+ }
+
+ final AgentSpan span = startSpan("http.request");
+ final AgentScope scope = activateSpan(span, true);
+
+ DECORATE.afterStart(span);
+ DECORATE.onRequest(span, httpMethod);
+
+ final boolean awsClientCall =
+ httpMethod.getRequestHeaders("amz-sdk-invocation-id").length > 0;
+ // AWS calls are often signed, so we can't add headers without breaking the signature.
+ if (!awsClientCall) {
+ propagate().inject(span, httpMethod, SETTER);
+ }
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void methodExit(
+ @Advice.Enter final AgentScope scope,
+ @Advice.Argument(1) final HttpMethod httpMethod,
+ @Advice.Thrown final Throwable throwable) {
+
+ if (scope == null) {
+ return;
+ }
+ try {
+ final AgentSpan span = scope.span();
+
+ DECORATE.onResponse(span, httpMethod);
+ DECORATE.onError(span, throwable);
+ DECORATE.beforeFinish(span);
+ } finally {
+ scope.close();
+ CallDepthThreadLocalMap.reset(HttpClient.class);
+ }
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/commons-httpclient-3/src/main/java/datadog/trace/instrumentation/commonshttpclient/HttpHeadersInjectAdapter.java
+package datadog.trace.instrumentation.commonshttpclient;
+
+import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethod;
+
+public class HttpHeadersInjectAdapter implements AgentPropagation.Setter<HttpMethod> {
+
+ public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
+
+ @Override
+ public void set(final HttpMethod carrier, final String key, final String value) {
+ carrier.setRequestHeader(new Header(key, value));
+ }
+}

ADD
dd-java-agent/instrumentation/commons-httpclient-3/src/test/groovy/CommonsHttpClientTest.groovy
+import datadog.trace.agent.test.base.HttpClientTest
+import datadog.trace.instrumentation.commonshttpclient.CommonsHttpClientDecorator
+import org.apache.commons.httpclient.HttpClient
+import org.apache.commons.httpclient.HttpMethod
+import org.apache.commons.httpclient.methods.DeleteMethod
+import org.apache.commons.httpclient.methods.GetMethod
+import org.apache.commons.httpclient.methods.HeadMethod
+import org.apache.commons.httpclient.methods.OptionsMethod
+import org.apache.commons.httpclient.methods.PostMethod
+import org.apache.commons.httpclient.methods.PutMethod
+import org.apache.commons.httpclient.methods.TraceMethod
+import spock.lang.Shared
+
+class CommonsHttpClientTest extends HttpClientTest<CommonsHttpClientDecorator> {
+ @Shared
+ HttpClient client = new HttpClient()
+
+ @Override
+ int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
+ HttpMethod httpMethod
+
+ switch (method) {
+ case "GET":
+ httpMethod = new GetMethod(uri.toString())
+ break
+ case "PUT":
+ httpMethod = new PutMethod(uri.toString())
+ break
+ case "POST":
+ httpMethod = new PostMethod(uri.toString())
+ break
+ case "HEAD":
+ httpMethod = new HeadMethod(uri.toString())
+ break
+ case "DELETE":
+ httpMethod = new DeleteMethod(uri.toString())
+ break
+ case "OPTIONS":
+ httpMethod = new OptionsMethod(uri.toString())
+ break
+ case "TRACE":
+ httpMethod = new TraceMethod(uri.toString())
+ break
+ default:
+ throw new RuntimeException("Unsupported method: " + method)
+ }
+
+ headers.each { httpMethod.setRequestHeader(it.key, it.value) }
+
+ try {
+ client.executeMethod(httpMethod)
+ callback?.call()
+ return httpMethod.getStatusCode()
+ } finally {
+ httpMethod.releaseConnection()
+ }
+ }
+
+ @Override
+ CommonsHttpClientDecorator decorator() {
+ return CommonsHttpClientDecorator.DECORATE
+ }
+
+ @Override
+ boolean testRedirects() {
+ // Generates 4 spans
+ false
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -60,6 +60,7 @@ include ':dd-java-agent:instrumentation:classloading'
include ':dd-java-agent:instrumentation:classloading:jboss-testing'
include ':dd-java-agent:instrumentation:classloading:osgi-testing'
include ':dd-java-agent:instrumentation:classloading:tomcat-testing'
+include ':dd-java-agent:instrumentation:commons-httpclient-3'
include ':dd-java-agent:instrumentation:couchbase-2.0'
include ':dd-java-agent:instrumentation:couchbase-2.6'
include ':dd-java-agent:instrumentation:datastax-cassandra-3'

Add commons-httpclient integration