Add Apache HTTP Client 3 instrumentation (#190)

This commit is contained in:
Trask Stalnaker 2020-02-27 11:29:33 -08:00 committed by GitHub
parent f4011b63e3
commit d60f18f8a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 324 additions and 0 deletions

View File

@ -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: '+'
}

View File

@ -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<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();
}
}

View File

@ -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<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();
}
}
}
}

View File

@ -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<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);
}
}

View File

@ -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<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())
}
}

View File

@ -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'