From 50cc8ddc8f75675aa37fcd19e20192e8612cfee9 Mon Sep 17 00:00:00 2001 From: Lauri Tulmin Date: Wed, 17 Feb 2021 22:03:29 +0200 Subject: [PATCH] Add support for apache httpclient5 (#2254) * Add support for apache httpclient5 * review fixes * copy commnet from httpclient-4 instrumentation * rebase --- .../apache-httpclient-5.0-javaagent.gradle | 13 + .../v5_0/ApacheHttpClientHelper.java | 24 ++ ...ApacheHttpClientInstrumentationModule.java | 363 ++++++++++++++++++ .../v5_0/ApacheHttpClientTracer.java | 87 +++++ .../v5_0/HttpHeadersInjectAdapter.java | 19 + .../v5_0/RequestWithHost.java | 61 +++ .../WrappingStatusSettingResponseHandler.java | 41 ++ ...ApacheHttpClientResponseHandlerTest.groovy | 96 +++++ .../test/groovy/ApacheHttpClientTest.groovy | 216 +++++++++++ settings.gradle | 1 + 10 files changed, 921 insertions(+) create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTracer.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeadersInjectAdapter.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientResponseHandlerTest.groovy create mode 100644 instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle new file mode 100644 index 0000000000..ff418b6773 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/apache-httpclient-5.0-javaagent.gradle @@ -0,0 +1,13 @@ +apply from: "$rootDir/gradle/instrumentation.gradle" + +muzzle { + pass { + group = "org.apache.httpcomponents.client5" + module = "httpclient5" + versions = "[5.0,)" + } +} + +dependencies { + library group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0' +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java new file mode 100644 index 0000000000..d24da767fc --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientHelper.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientTracer.tracer; + +import io.opentelemetry.context.Context; +import org.apache.hc.core5.http.HttpResponse; + +public class ApacheHttpClientHelper { + + public static void doMethodExit(Context context, Object result, Throwable throwable) { + if (throwable != null) { + tracer().endExceptionally(context, throwable); + } else if (result instanceof HttpResponse) { + tracer().end(context, (HttpResponse) result); + } else { + // ended in WrappingStatusSettingResponseHandler + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java new file mode 100644 index 0000000000..cab75b4d25 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientInstrumentationModule.java @@ -0,0 +1,363 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientTracer.tracer; +import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; +import static java.util.Collections.singletonList; +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.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.HashMap; +import java.util.List; +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.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +@AutoService(InstrumentationModule.class) +public class ApacheHttpClientInstrumentationModule extends InstrumentationModule { + + public ApacheHttpClientInstrumentationModule() { + super("apache-httpclient", "apache-httpclient-5.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HttpClientInstrumentation()); + } + + public static class HttpClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.hc.client5.http.classic.HttpClient"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("org.apache.hc.client5.http.classic.HttpClient")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + // There are 8 execute(...) methods. Depending on the version, they may or may not delegate + // to each other. Thus, all methods need to be instrumented. Because of argument position and + // type, some methods can share the same advice class. The call depth tracking ensures only 1 + // span is created + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(1)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))), + ApacheHttpClientInstrumentationModule.class.getName() + "$RequestAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.protocol.HttpContext"))), + ApacheHttpClientInstrumentationModule.class.getName() + "$RequestAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))), + ApacheHttpClientInstrumentationModule.class.getName() + "$RequestWithHostAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.protocol.HttpContext"))), + ApacheHttpClientInstrumentationModule.class.getName() + "$RequestWithHostAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(2)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and( + takesArgument(1, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + ApacheHttpClientInstrumentationModule.class.getName() + "$RequestWithHandlerAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.protocol.HttpContext"))) + .and( + takesArgument(2, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + ApacheHttpClientInstrumentationModule.class.getName() + + "$RequestWithContextAndHandlerAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and( + takesArgument(2, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + ApacheHttpClientInstrumentationModule.class.getName() + + "$RequestWithHostAndHandlerAdvice"); + + transformers.put( + isMethod() + .and(named("execute")) + .and(not(isAbstract())) + .and(takesArguments(4)) + .and(takesArgument(0, named("org.apache.hc.core5.http.HttpHost"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.ClassicHttpRequest"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.protocol.HttpContext"))) + .and( + takesArgument(3, named("org.apache.hc.core5.http.io.HttpClientResponseHandler"))), + ApacheHttpClientInstrumentationModule.class.getName() + + "$RequestWithHostAndContextAndHandlerAdvice"); + + return transformers; + } + } + + public static class RequestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } + + public static class RequestWithHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Argument(value = 1, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = new WrappingStatusSettingResponseHandler(context, parentContext, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } + + public static class RequestWithContextAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) ClassicHttpRequest request, + @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = new WrappingStatusSettingResponseHandler(context, parentContext, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } + + public static class RequestWithHostAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, host, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } + + public static class RequestWithHostAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Argument(value = 2, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, host, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = new WrappingStatusSettingResponseHandler(context, parentContext, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } + + public static class RequestWithHostAndContextAndHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) HttpHost host, + @Advice.Argument(1) ClassicHttpRequest request, + @Advice.Argument(value = 3, readOnly = false) HttpClientResponseHandler handler, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + if (!tracer().shouldStartSpan(parentContext)) { + return; + } + + context = tracer().startSpan(parentContext, host, request); + scope = context.makeCurrent(); + + // Wrap the handler so we capture the status code + if (handler != null) { + handler = new WrappingStatusSettingResponseHandler(context, parentContext, handler); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Return Object result, + @Advice.Thrown Throwable throwable, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + + scope.close(); + ApacheHttpClientHelper.doMethodExit(context, result, throwable); + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTracer.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTracer.java new file mode 100644 index 0000000000..989a7d6d0e --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/ApacheHttpClientTracer.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer; +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpMessage; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolVersion; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ApacheHttpClientTracer + extends HttpClientTracer { + + private static final ApacheHttpClientTracer TRACER = new ApacheHttpClientTracer(); + + public static ApacheHttpClientTracer tracer() { + return TRACER; + } + + public Context startSpan(Context parentContext, HttpHost host, ClassicHttpRequest request) { + return startSpan(parentContext, new RequestWithHost(host, request)); + } + + public Context startSpan(Context parentContext, ClassicHttpRequest request) { + return startSpan(parentContext, request, request); + } + + @Override + protected String method(ClassicHttpRequest httpRequest) { + return httpRequest.getMethod(); + } + + @Override + protected @Nullable String flavor(ClassicHttpRequest request) { + ProtocolVersion protocolVersion = request.getVersion(); + if (protocolVersion == null) { + protocolVersion = HttpVersion.HTTP_1_1; + } + return protocolVersion.toString(); + } + + @Override + protected URI url(ClassicHttpRequest request) throws URISyntaxException { + return request.getUri(); + } + + @Override + protected Integer status(HttpResponse httpResponse) { + return httpResponse.getCode(); + } + + @Override + protected String requestHeader(ClassicHttpRequest request, String name) { + return header(request, name); + } + + @Override + protected String responseHeader(HttpResponse response, String name) { + return header(response, name); + } + + @Override + protected TextMapPropagator.Setter getSetter() { + return HttpHeadersInjectAdapter.SETTER; + } + + private static String header(HttpMessage message, String name) { + Header header = message.getFirstHeader(name); + return header != null ? header.getValue() : null; + } + + @Override + protected String getInstrumentationName() { + return "io.opentelemetry.javaagent.apache-httpclient"; + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeadersInjectAdapter.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeadersInjectAdapter.java new file mode 100644 index 0000000000..dabbe9af65 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/HttpHeadersInjectAdapter.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import org.apache.hc.core5.http.ClassicHttpRequest; + +class HttpHeadersInjectAdapter implements TextMapPropagator.Setter { + + public static final HttpHeadersInjectAdapter SETTER = new HttpHeadersInjectAdapter(); + + @Override + public void set(ClassicHttpRequest carrier, String key, String value) { + carrier.addHeader(key, value); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java new file mode 100644 index 0000000000..dfff2b91e3 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/RequestWithHost.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import java.net.URI; +import java.net.URISyntaxException; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.HttpRequestWrapper; + +public class RequestWithHost extends HttpRequestWrapper implements ClassicHttpRequest { + + private final ClassicHttpRequest httpRequest; + private final URI uri; + + public RequestWithHost(HttpHost httpHost, ClassicHttpRequest httpRequest) { + super(httpRequest); + + this.httpRequest = httpRequest; + + URI calculatedUri; + try { + // combine requested uri with host info + URI requestUri = httpRequest.getUri(); + calculatedUri = + new URI( + httpHost.getSchemeName(), + null, + httpHost.getHostName(), + httpHost.getPort(), + requestUri.getPath(), + requestUri.getQuery(), + requestUri.getFragment()); + } catch (URISyntaxException e) { + calculatedUri = null; + } + uri = calculatedUri; + } + + @Override + public URI getUri() throws URISyntaxException { + if (uri != null) { + return uri; + } + return super.getUri(); + } + + @Override + public HttpEntity getEntity() { + return httpRequest.getEntity(); + } + + @Override + public void setEntity(HttpEntity entity) { + httpRequest.setEntity(entity); + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java new file mode 100644 index 0000000000..10e146f76d --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpclient/v5_0/WrappingStatusSettingResponseHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0; + +import static io.opentelemetry.javaagent.instrumentation.apachehttpclient.v5_0.ApacheHttpClientTracer.tracer; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import java.io.IOException; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +public class WrappingStatusSettingResponseHandler implements HttpClientResponseHandler { + final Context context; + final Context parentContext; + final HttpClientResponseHandler handler; + + public WrappingStatusSettingResponseHandler( + Context context, Context parentContext, HttpClientResponseHandler handler) { + this.context = context; + this.parentContext = parentContext; + this.handler = handler; + } + + @Override + public T handleResponse(ClassicHttpResponse response) throws IOException, HttpException { + tracer().end(context, response); + // ending the span before executing the callback handler (and scoping the callback handler to + // the parent context), even though we are inside of a synchronous http client callback + // underneath HttpClient.execute(..), in order to not attribute other CLIENT span timings that + // may be performed in the callback handler to the http client span (and so we don't end up with + // nested CLIENT spans, which we currently suppress) + try (Scope ignored = parentContext.makeCurrent()) { + return handler.handleResponse(response); + } + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientResponseHandlerTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientResponseHandlerTest.groovy new file mode 100644 index 0000000000..ee4c2341a5 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientResponseHandlerTest.groovy @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.TimeUnit +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase +import org.apache.hc.client5.http.config.RequestConfig +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.ClassicHttpResponse +import org.apache.hc.core5.http.HttpHost +import org.apache.hc.core5.http.HttpRequest +import org.apache.hc.core5.http.io.HttpClientResponseHandler +import org.apache.hc.core5.http.message.BasicHeader +import org.apache.hc.core5.http.protocol.BasicHttpContext +import spock.lang.AutoCleanup +import spock.lang.Shared + +abstract class ApacheHttpClientResponseHandlerTest extends HttpClientTest implements AgentTestTrait { + + @Shared + @AutoCleanup + def client + + @Shared + def handler = new HttpClientResponseHandler() { + @Override + Integer handleResponse(ClassicHttpResponse response) { + return response.code + } + } + + def setupSpec() { + HttpClientBuilder builder = HttpClients.custom() + builder.setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build()) + + client = builder.build() + } + + abstract int executeRequest(T request) + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + def request = new HttpUriRequestBase(method, uri) + headers.entrySet().each { + request.addHeader(new BasicHeader(it.key, it.value)) + } + + def status = executeRequest(request) + + // handler execution is included within the client span, so we can't call the callback there. + callback?.call() + + return status + } +} + +class ApacheClientHandlerRequest extends ApacheHttpClientResponseHandlerTest { + + @Override + int executeRequest(ClassicHttpRequest request) { + return client.execute(request, handler) + } +} + +class ApacheClientContextHandlerRequest extends ApacheHttpClientResponseHandlerTest { + + @Override + int executeRequest(ClassicHttpRequest request) { + return client.execute(request, new BasicHttpContext(), handler) + } +} + +class ApacheClientHostHandlerRequest extends ApacheHttpClientResponseHandlerTest { + + @Override + int executeRequest(ClassicHttpRequest request) { + URI uri = request.getUri() + return client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request, handler) + } +} + +class ApacheClientHostAndContextHandlerRequest extends ApacheHttpClientResponseHandlerTest { + + @Override + int executeRequest(ClassicHttpRequest request) { + URI uri = request.getUri() + return client.execute(new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()), request, new BasicHttpContext(), handler) + } +} diff --git a/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy new file mode 100644 index 0000000000..acb93daa99 --- /dev/null +++ b/instrumentation/apache-httpclient/apache-httpclient-5.0/javaagent/src/test/groovy/ApacheHttpClientTest.groovy @@ -0,0 +1,216 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.opentelemetry.instrumentation.test.AgentTestTrait +import io.opentelemetry.instrumentation.test.base.HttpClientTest +import java.util.concurrent.TimeUnit +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase +import org.apache.hc.client5.http.config.RequestConfig +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.ClassicHttpResponse +import org.apache.hc.core5.http.HttpHost +import org.apache.hc.core5.http.HttpRequest +import org.apache.hc.core5.http.message.BasicClassicHttpRequest +import org.apache.hc.core5.http.message.BasicHeader +import org.apache.hc.core5.http.protocol.BasicHttpContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Timeout + +abstract class ApacheHttpClientTest extends HttpClientTest implements AgentTestTrait { + @Shared + @AutoCleanup + def client + + def setupSpec() { + HttpClientBuilder builder = HttpClients.custom() + builder.setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build()) + + client = builder.build() + } + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + def request = createRequest(method, uri) + headers.entrySet().each { + request.addHeader(new BasicHeader(it.key, it.value)) + } + + def response = executeRequest(request, uri, callback) + response.close() // Make sure the connection is closed. + + return response.code + } + + abstract T createRequest(String method, URI uri) + + abstract ClassicHttpResponse executeRequest(T request, URI uri, Closure callback) + + static String fullPathFromURI(URI uri) { + StringBuilder builder = new StringBuilder() + if (uri.getPath() != null) { + builder.append(uri.getPath()) + } + + if (uri.getQuery() != null) { + builder.append('?') + builder.append(uri.getQuery()) + } + + if (uri.getFragment() != null) { + builder.append('#') + builder.append(uri.getFragment()) + } + return builder.toString() + } +} + +@Timeout(5) +class ApacheClientHostRequest extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + def response = client.execute(new HttpHost(uri.getHost(), uri.getPort()), request) + callback?.call() + return response + } + + @Override + boolean testRemoteConnection() { + return false + } +} + +@Timeout(5) +class ApacheClientHostRequestContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + def response = client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, new BasicHttpContext()) + callback?.call() + return response + } + + @Override + boolean testRemoteConnection() { + return false + } +} + +@Timeout(5) +class ApacheClientHostRequestResponseHandler extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, { + callback?.call() + return it + }) + } + + @Override + boolean testRemoteConnection() { + return false + } +} + +@Timeout(5) +class ApacheClientHostRequestResponseHandlerContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new BasicClassicHttpRequest(method, fullPathFromURI(uri)) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + return client.execute(new HttpHost(uri.getHost(), uri.getPort()), request, new BasicHttpContext(), { + callback?.call() + return it + }) + } + + @Override + boolean testRemoteConnection() { + return false + } +} + +@Timeout(5) +class ApacheClientUriRequest extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + def response = client.execute(request) + callback?.call() + return response + } +} + +@Timeout(5) +class ApacheClientUriRequestContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + def response = client.execute(request, new BasicHttpContext()) + callback?.call() + return response + } +} + +@Timeout(5) +class ApacheClientUriRequestResponseHandler extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + return client.execute(request, { + callback?.call() + it + }) + } +} + +@Timeout(5) +class ApacheClientUriRequestResponseHandlerContext extends ApacheHttpClientTest { + @Override + ClassicHttpRequest createRequest(String method, URI uri) { + return new HttpUriRequestBase(method, uri) + } + + @Override + ClassicHttpResponse executeRequest(ClassicHttpRequest request, URI uri, Closure callback) { + return client.execute(request, { + callback?.call() + it + }) + } +} diff --git a/settings.gradle b/settings.gradle index 60539eb906..54c22b18f1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -60,6 +60,7 @@ include ':instrumentation:apache-camel-2.20:javaagent-unittests' include ':instrumentation:apache-httpasyncclient-4.1:javaagent' include ':instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent' include ':instrumentation:apache-httpclient:apache-httpclient-4.0:javaagent' +include ':instrumentation:apache-httpclient:apache-httpclient-5.0:javaagent' include ':instrumentation:armeria-1.3:javaagent' include ':instrumentation:armeria-1.3:library' include ':instrumentation:armeria-1.3:testing'