diff --git a/instrumentation/apache-httpclient-3.0/apache-httpclient-3.0.gradle b/instrumentation/apache-httpclient-3.0/apache-httpclient-3.0.gradle new file mode 100644 index 0000000000..37c4f02ed8 --- /dev/null +++ b/instrumentation/apache-httpclient-3.0/apache-httpclient-3.0.gradle @@ -0,0 +1,25 @@ +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: '+' +} diff --git a/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientDecorator.java b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientDecorator.java new file mode 100644 index 0000000000..25e02af698 --- /dev/null +++ b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientDecorator.java @@ -0,0 +1,62 @@ +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 { + 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(); + } +} diff --git a/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientInstrumentation.java b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientInstrumentation.java new file mode 100644 index 0000000000..f4e7851d1d --- /dev/null +++ b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/ApacheHttpClientInstrumentation.java @@ -0,0 +1,110 @@ +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 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, 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(); + } + } + } +} diff --git a/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/HttpHeadersInjectAdapter.java b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/HttpHeadersInjectAdapter.java new file mode 100644 index 0000000000..3900925a2c --- /dev/null +++ b/instrumentation/apache-httpclient-3.0/src/main/java/io/opentelemetry/auto/instrumentation/apachehttpclient/v3_0/HttpHeadersInjectAdapter.java @@ -0,0 +1,14 @@ +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 { + + public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void put(final HttpMethod carrier, final String key, final String value) { + carrier.addRequestHeader(key, value); + } +} diff --git a/instrumentation/apache-httpclient-3.0/src/test/groovy/ApacheHttpClientTest.groovy b/instrumentation/apache-httpclient-3.0/src/test/groovy/ApacheHttpClientTest.groovy new file mode 100644 index 0000000000..512e53c79e --- /dev/null +++ b/instrumentation/apache-httpclient-3.0/src/test/groovy/ApacheHttpClientTest.groovy @@ -0,0 +1,112 @@ +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 { + @Shared + def client = new HttpClient() + + @Override + ApacheHttpClientDecorator decorator() { + return ApacheHttpClientDecorator.DECORATE + } + + @Override + int doRequest(String method, URI uri, Map 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()) + } +} diff --git a/settings.gradle b/settings.gradle index 6bfbe6b35c..900de37ff2 100644 --- a/settings.gradle +++ b/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'