Add Apache HTTP Client 3 instrumentation
ADD
instrumentation/apache-httpclient-3.0/apache-httpclient-3.0.gradle
+muzzle {
+ pass {
+ group = "commons-httpclient"
+ module = "commons-httpclient"
+ versions = "[3.0,4.0)"
+ assertInverse = true
+ }
+}
+
+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: '+'
+}

ADD
instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientDecorator.java
+package io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0;
+
+import io.opentelemetry.OpenTelemetry;
+import io.opentelemetry.auto.decorator.HttpClientDecorator;
+import io.opentelemetry.trace.Tracer;
+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 ApacheHttpClientDecorator extends HttpClientDecorator<HttpMethod, HttpMethod> {
+ public static final ApacheHttpClientDecorator DECORATE = new ApacheHttpClientDecorator();
+
+ public static final Tracer TRACER =
+ OpenTelemetry.getTracerFactory().get("io.opentelemetry.auto.apache-httpclient-3.0");
+
+ @Override
+ protected String getComponentName() {
+ return "apache-httpclient";
+ }
+
+ @Override
+ protected String method(final HttpMethod httpMethod) {
+ return httpMethod.getName();
+ }
+
+ @Override
+ protected URI url(final HttpMethod httpMethod) throws URISyntaxException {
+ final org.apache.commons.httpclient.URI uri;
+ try {
+ uri = httpMethod.getURI();
+ } catch (final URIException e) {
+ return null;
+ }
+ return new URI(uri.toString());
+ }
+
+ @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
instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientInstrumentation.java
+package io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0;
+
+import static io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0.ApacheHttpClientDecorator.DECORATE;
+import static io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0.ApacheHttpClientDecorator.TRACER;
+import static io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0.HttpHeadersInjectAdapter.SETTER;
+import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static io.opentelemetry.trace.Span.Kind.CLIENT;
+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.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 io.opentelemetry.auto.bootstrap.CallDepthThreadLocalMap;
+import io.opentelemetry.auto.instrumentation.api.SpanWithScope;
+import io.opentelemetry.auto.tooling.Instrumenter;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.trace.Span;
+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 ApacheHttpClientInstrumentation extends Instrumenter.Default {
+
+ public ApacheHttpClientInstrumentation() {
+ super("httpclient", "apache-httpclient", "apache-http-client");
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return safeHasSuperType(named("org.apache.commons.httpclient.HttpClient"));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ packageName + ".HttpHeadersInjectAdapter",
+ "io.opentelemetry.auto.decorator.BaseDecorator",
+ "io.opentelemetry.auto.decorator.ClientDecorator",
+ "io.opentelemetry.auto.decorator.HttpClientDecorator",
+ packageName + ".ApacheHttpClientDecorator"
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isMethod()
+ .and(named("executeMethod"))
+ .and(not(isAbstract()))
+ .and(takesArguments(3))
+ .and(takesArgument(0, named("org.apache.commons.httpclient.HostConfiguration")))
+ .and(takesArgument(1, named("org.apache.commons.httpclient.HttpMethod")))
+ .and(takesArgument(2, named("org.apache.commons.httpclient.HttpState"))),
+ ApacheHttpClientInstrumentation.class.getName() + "$ExecuteAdvice");
+ }
+
+ public static class ExecuteAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static SpanWithScope methodEnter(@Advice.Argument(1) final HttpMethod httpMethod) {
+ final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClient.class);
+ if (callDepth > 0) {
+ return null;
+ }
+ final Span span = TRACER.spanBuilder("http.request").setSpanKind(CLIENT).startSpan();
+ final Scope scope = TRACER.withSpan(span);
+
+ 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) {
+ TRACER.getHttpTextFormat().inject(span.getContext(), httpMethod, SETTER);
+ }
+ return new SpanWithScope(span, scope);
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void methodExit(
+ @Advice.Enter final SpanWithScope spanWithScope,
+ @Advice.Argument(1) final HttpMethod httpMethod,
+ @Advice.Thrown final Throwable throwable) {
+
+ if (spanWithScope == null) {
+ return;
+ }
+ CallDepthThreadLocalMap.reset(HttpClient.class);
+
+ try {
+ final Span span = spanWithScope.getSpan();
+ DECORATE.onResponse(span, httpMethod);
+ DECORATE.onError(span, throwable);
+ DECORATE.beforeFinish(span);
+ span.end();
+ } finally {
+ spanWithScope.closeScope();
+ }
+ }
+ }
+}

ADD
instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/HttpHeadersInjectAdapter.java
+package io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0;
+
+import io.opentelemetry.context.propagation.HttpTextFormat;
+import org.apache.commons.httpclient.HttpMethod;
+
+public class HttpHeadersInjectAdapter implements HttpTextFormat.Setter<HttpMethod> {
+
+ public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter();
+
+ @Override
+ public void put(final HttpMethod carrier, final String key, final String value) {
+ carrier.addRequestHeader(key, value);
+ }
+}

ADD
instrumentation/apache-httpclient-3.0/src/test/groovy/ApacheHttpClientTest.groovy
+import io.opentelemetry.auto.instrumentation.apachehttpclient.v3_0.ApacheHttpClientDecorator
+import io.opentelemetry.auto.test.base.HttpClientTest
+import org.apache.commons.httpclient.HostConfiguration
+import org.apache.commons.httpclient.HttpClient
+import org.apache.commons.httpclient.HttpMethod
+import org.apache.commons.httpclient.HttpState
+import org.apache.commons.httpclient.methods.GetMethod
+import org.apache.commons.httpclient.methods.HeadMethod
+import org.apache.commons.httpclient.methods.PostMethod
+import org.apache.commons.httpclient.methods.PutMethod
+import spock.lang.Shared
+
+import java.util.concurrent.ExecutionException
+
+abstract class ApacheHttpClientTest extends HttpClientTest<ApacheHttpClientDecorator> {
+ @Shared
+ def client = new HttpClient()
+
+ @Override
+ ApacheHttpClientDecorator decorator() {
+ return ApacheHttpClientDecorator.DECORATE
+ }
+
+ @Override
+ int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
+ def httpMethod
+ switch (method) {
+ case "GET":
+ httpMethod = new GetMethod(uri.toString())
+ break
+ case "POST":
+ httpMethod = new PostMethod(uri.toString())
+ break
+ case "PUT":
+ httpMethod = new PutMethod(uri.toString())
+ break
+ case "HEAD":
+ httpMethod = new HeadMethod(uri.toString())
+ break
+ default:
+ throw new IllegalStateException("Unexpected http method: " + method)
+ }
+
+ headers.entrySet().each {
+ httpMethod.addRequestHeader(it.key, it.value)
+ }
+
+ def statusCode = executeRequest(httpMethod, uri)
+ callback?.call()
+ httpMethod.releaseConnection()
+
+ return statusCode
+ }
+
+ abstract int executeRequest(HttpMethod request, URI uri)
+
+ @Override
+ boolean testCircularRedirects() {
+ // only creates 1 server request instead of 2 server requests before throwing exception like others
+ false
+ }
+
+ @Override
+ Integer statusOnRedirectError() {
+ return 302
+ }
+
+ def "basic #method request with circular redirects"() {
+ given:
+ def uri = server.address.resolve("/circular-redirect")
+
+ when:
+ doRequest(method, uri)
+
+ then:
+ def ex = thrown(Exception)
+ def thrownException = ex instanceof ExecutionException ? ex.cause : ex
+
+ and:
+ assertTraces(1) {
+ trace(0, 2) {
+ clientSpan(it, 0, null, method, false, false, uri, statusOnRedirectError(), thrownException)
+ serverSpan(it, 1, span(0))
+ }
+ }
+
+ where:
+ method = "GET"
+ }
+}
+
+
+class ApacheClientHttpMethod extends ApacheHttpClientTest {
+ @Override
+ int executeRequest(HttpMethod httpMethod, URI uri) {
+ client.executeMethod(httpMethod)
+ }
+}
+
+class ApacheClientHostConfiguration extends ApacheHttpClientTest {
+ @Override
+ int executeRequest(HttpMethod httpMethod, URI uri) {
+ client.executeMethod(new HostConfiguration(), httpMethod)
+ }
+}
+
+class ApacheClientHttpState extends ApacheHttpClientTest {
+ @Override
+ int executeRequest(HttpMethod httpMethod, URI uri) {
+ client.executeMethod(new HostConfiguration(), httpMethod, new HttpState())
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -46,6 +46,7 @@ include ':smoke-tests:springboot'
// instrumentation:
include ':instrumentation:akka-http-10.0'
include ':instrumentation:apache-httpasyncclient-4.0'
+include ':instrumentation:apache-httpclient-3.0'
include ':instrumentation:apache-httpclient-4.0'
include ':instrumentation:aws-java-sdk-1.11'
include ':instrumentation:aws-java-sdk-2.2'

Add Apache HTTP Client 3 instrumentation (#190)