diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a8f0a5f382..5952e1ed81 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -36,6 +36,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes: * Added support for OpenTelemetry annotations - `WithSpan` and `SpanAttribute` - {pull}3406[#3406] * Only automatically apply redacted exceptions for Corretto JVM 17-20. Outside that, user should use capture_exception_details=false to workaround the JVM race-condition bug if it gets triggered: {pull}3438[#3438] * Added support for Spring 6.1 / Spring-Boot 3.2 - {pull}3440[#3440] +* Add support for Apache HTTP client 5.x - {pull}3419[#3419] [[release-notes-1.x]] === Java Agent version 1.x diff --git a/apm-agent-builds/pom.xml b/apm-agent-builds/pom.xml index 590d9218aa..fe524e77fa 100644 --- a/apm-agent-builds/pom.xml +++ b/apm-agent-builds/pom.xml @@ -52,6 +52,11 @@ apm-apache-httpclient4-plugin ${project.version} + + ${project.groupId} + apm-apache-httpclient5-plugin + ${project.version} + ${project.groupId} apm-api-plugin diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/pom.xml b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/pom.xml new file mode 100644 index 0000000000..c9d5fd8fc0 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + + apm-apache-httpclient + co.elastic.apm + 1.44.1-SNAPSHOT + + + apm-apache-httpclient-common + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + + + diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAdvice.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAdvice.java new file mode 100644 index 0000000000..fce3555a6d --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAdvice.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.common; + + +import co.elastic.apm.agent.httpclient.HttpClientHelper; +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.Outcome; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.Tracer; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; + +import java.net.URISyntaxException; + +public abstract class AbstractApacheHttpClientAdvice { + + public static & + TextHeaderGetter> Object startSpan(final Tracer tracer, + final ApacheHttpClientApiAdapter adapter, + final WRAPPER request, + final HTTPHOST httpHost, + final HeaderAccessor headerAccessor) throws URISyntaxException { + ElasticContext elasticContext = tracer.currentContext(); + Span span = null; + if (elasticContext.getSpan() != null) { + span = HttpClientHelper.startHttpClientSpan(elasticContext, adapter.getMethod(request), adapter.getUri(request), adapter.getHostName(httpHost)); + if (span != null) { + span.activate(); + } + } + tracer.currentContext().propagateContext(request, headerAccessor, headerAccessor); + return span; + } + + public static void endSpan(ApacheHttpClientApiAdapter adapter, + Object spanObj, + Throwable t, + RESPONSE response) { + Span span = (Span) spanObj; + if (span == null) { + return; + } + try { + if (response != null && adapter.isNotNullStatusLine(response)) { + int statusCode = adapter.getResponseCode(response); + span.getContext().getHttp().withStatusCode(statusCode); + } + span.captureException(t); + } finally { + // in case of circular redirect, we get an exception but status code won't be available without response + // thus we have to deal with span outcome directly + if (adapter.isCircularRedirectException(t)) { + span.withOutcome(Outcome.FAILURE); + } + span.deactivate().end(); + } + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAsyncAdvice.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAsyncAdvice.java new file mode 100644 index 0000000000..08aab1d912 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAsyncAdvice.java @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.common; + + +import co.elastic.apm.agent.httpclient.HttpClientHelper; +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.Tracer; + +public abstract class AbstractApacheHttpClientAsyncAdvice { + + public static Object[] startSpan( + Tracer tracer, ApacheHttpClientAsyncHelper asyncHelper, + PRODUCER asyncRequestProducer, CONTEXT context, CALLBACK futureCallback) { + + ElasticContext parentContext = tracer.currentContext(); + if (parentContext.isEmpty()) { + // performance optimization, no need to wrap if we have nothing to propagate + // empty context means also we will not create an exit span + return null; + } + CALLBACK wrappedFutureCallback = futureCallback; + ElasticContext activeContext = tracer.currentContext(); + Span span = activeContext.createExitSpan(); + if (span != null) { + span.withType(HttpClientHelper.EXTERNAL_TYPE) + .withSubtype(HttpClientHelper.HTTP_SUBTYPE) + .withSync(false) + .activate(); + wrappedFutureCallback = asyncHelper.wrapFutureCallback(futureCallback, context, span); + } + PRODUCER wrappedProducer = asyncHelper.wrapRequestProducer(asyncRequestProducer, span, tracer.currentContext()); + return new Object[]{wrappedProducer, wrappedFutureCallback, span}; + } + + public static void endSpan( + ApacheHttpClientAsyncHelper asyncHelper, Object[] enter, Throwable t) { + Span span = enter != null ? (Span) enter[2] : null; + if (span != null) { + // Deactivate in this thread + span.deactivate(); + // End the span if the method terminated with an exception. + // The exception means that the listener who normally does the ending will not be invoked. + WRAPPER wrapper = (WRAPPER) enter[0]; + if (t != null) { + CALLBACK_WRAPPER cb = (CALLBACK_WRAPPER) enter[1]; + // only for apachehttpclient_v4 + asyncHelper.failedBeforeRequestStarted(cb, t); + + asyncHelper.recycle(wrapper); + } + } + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientApiAdapter.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientApiAdapter.java new file mode 100644 index 0000000000..4363cc3e17 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientApiAdapter.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.common; + + +import java.net.URI; +import java.net.URISyntaxException; + +public interface ApacheHttpClientApiAdapter { + String getMethod(WRAPPER request); + + URI getUri(WRAPPER request) throws URISyntaxException; + + CharSequence getHostName(HTTPHOST httpHost); + + int getResponseCode(RESPONSE response); + + boolean isCircularRedirectException(Throwable t); + + boolean isNotNullStatusLine(RESPONSE response); +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientAsyncHelper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientAsyncHelper.java new file mode 100644 index 0000000000..7f213dcc84 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientAsyncHelper.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.common; + + +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.Span; + +public interface ApacheHttpClientAsyncHelper { + + AsyncProducerWrapper wrapRequestProducer(AsyncProducer asyncRequestProducer, Span span, ElasticContext toPropagate); + + FutureCallbackWrapper wrapFutureCallback(FutureCallback futureCallback, HttpContext httpContext, Span span); + + void failedBeforeRequestStarted(FutureCallbackWrapper cb, Throwable t); + + void recycle(AsyncProducerWrapper requestProducerWrapper); + +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/pom.xml b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/pom.xml index b3db50451f..e63e0b269f 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/pom.xml +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/pom.xml @@ -20,6 +20,11 @@ this dependency duplicates the transitive one we get from 'httpasyncclient' but keeping it explicit avoids relying on transitive dependency --> + + ${project.groupId} + apm-apache-httpclient-common + ${project.version} + org.apache.httpcomponents httpclient diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentation.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentation.java index aaa928face..97d908ec1d 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentation.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentation.java @@ -18,12 +18,8 @@ */ package co.elastic.apm.agent.httpclient.v4; -import co.elastic.apm.agent.httpclient.HttpClientHelper; -import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpAsyncClientHelper; -import co.elastic.apm.agent.httpclient.v4.helper.FutureCallbackWrapper; -import co.elastic.apm.agent.httpclient.v4.helper.HttpAsyncRequestProducerWrapper; -import co.elastic.apm.agent.tracer.ElasticContext; -import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAsyncAdvice; +import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpClient4AsyncHelper; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.NamedElement; @@ -50,7 +46,7 @@ public class ApacheHttpAsyncClientInstrumentation extends BaseApacheHttpClientIn @Override public String getAdviceClassName() { - return "co.elastic.apm.agent.httpclient.v4.ApacheHttpAsyncClientInstrumentation$ApacheHttpAsyncClientAdvice"; + return "co.elastic.apm.agent.httpclient.v4.ApacheHttpAsyncClientInstrumentation$ApacheHttpClient4AsyncAdvice"; } @Override @@ -79,8 +75,8 @@ public ElementMatcher getMethodMatcher() { .and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))); } - public static class ApacheHttpAsyncClientAdvice { - private static ApacheHttpAsyncClientHelper asyncHelper = new ApacheHttpAsyncClientHelper(); + public static class ApacheHttpClient4AsyncAdvice extends AbstractApacheHttpClientAsyncAdvice { + private static ApacheHttpClient4AsyncHelper asyncHelper = new ApacheHttpClient4AsyncHelper(); @Advice.AssignReturned.ToArguments({ @ToArgument(index = 0, value = 0, typing = DYNAMIC), @@ -91,43 +87,13 @@ public static class ApacheHttpAsyncClientAdvice { public static Object[] onBeforeExecute(@Advice.Argument(value = 0) HttpAsyncRequestProducer requestProducer, @Advice.Argument(2) HttpContext context, @Advice.Argument(value = 3) FutureCallback futureCallback) { - - ElasticContext parentContext = tracer.currentContext(); - if (parentContext.isEmpty()) { - // performance optimization, no need to wrap if we have nothing to propagate - // empty context means also we will not create an exit span - return null; - } - FutureCallback wrappedFutureCallback = futureCallback; - ElasticContext activeContext = tracer.currentContext(); - Span span = activeContext.createExitSpan(); - if (span != null) { - span.withType(HttpClientHelper.EXTERNAL_TYPE) - .withSubtype(HttpClientHelper.HTTP_SUBTYPE) - .withSync(false) - .activate(); - wrappedFutureCallback = asyncHelper.wrapFutureCallback(futureCallback, context, span); - } - HttpAsyncRequestProducer wrappedProducer = asyncHelper.wrapRequestProducer(requestProducer, span, tracer.currentContext()); - return new Object[]{wrappedProducer, wrappedFutureCallback, span}; + return startSpan(tracer, asyncHelper, requestProducer, context, futureCallback); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) public static void onAfterExecute(@Advice.Enter @Nullable Object[] enter, @Advice.Thrown @Nullable Throwable t) { - Span span = enter != null ? (Span) enter[2] : null; - if (span != null) { - // Deactivate in this thread - span.deactivate(); - // End the span if the method terminated with an exception. - // The exception means that the listener who normally does the ending will not be invoked. - if (t != null) { - HttpAsyncRequestProducerWrapper wrapper = (HttpAsyncRequestProducerWrapper) enter[0]; - FutureCallbackWrapper cb = (FutureCallbackWrapper) enter[1]; - cb.failedWithoutExecution(t); - asyncHelper.recycle(wrapper); - } - } + endSpan(asyncHelper, enter, t); } } } diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpClientInstrumentation.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpClientInstrumentation.java index cf90d28c96..1d60900984 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpClientInstrumentation.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpClientInstrumentation.java @@ -18,22 +18,20 @@ */ package co.elastic.apm.agent.httpclient.v4; -import co.elastic.apm.agent.httpclient.HttpClientHelper; +import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAdvice; +import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpClient4ApiAdapter; import co.elastic.apm.agent.httpclient.v4.helper.RequestHeaderAccessor; -import co.elastic.apm.agent.tracer.ElasticContext; -import co.elastic.apm.agent.tracer.Outcome; -import co.elastic.apm.agent.tracer.Span; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import org.apache.http.client.CircularRedirectException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpRequestWrapper; import org.apache.http.conn.routing.HttpRoute; import javax.annotation.Nullable; +import java.net.URISyntaxException; import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass; import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; @@ -48,54 +46,27 @@ @SuppressWarnings("Duplicates") public class ApacheHttpClientInstrumentation extends BaseApacheHttpClientInstrumentation { - public static class ApacheHttpClientAdvice { + public static class ApacheHttpClient4Advice extends AbstractApacheHttpClientAdvice { + private static final ApacheHttpClient4ApiAdapter adapter = ApacheHttpClient4ApiAdapter.get(); + @Nullable @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) public static Object onBeforeExecute(@Advice.Argument(0) HttpRoute route, - @Advice.Argument(1) HttpRequestWrapper request) { - ElasticContext activeContext = tracer.currentContext(); - Span span = null; - if (activeContext.getSpan() != null) { - span = HttpClientHelper.startHttpClientSpan(activeContext, request.getMethod(), request.getURI(), route.getTargetHost().getHostName()); - - if (span != null) { - span.activate(); - } - } - - tracer.currentContext().propagateContext(request, RequestHeaderAccessor.INSTANCE, RequestHeaderAccessor.INSTANCE); - return span; + @Advice.Argument(1) HttpRequestWrapper request) throws URISyntaxException { + return startSpan(tracer, adapter, request, route.getTargetHost(), RequestHeaderAccessor.INSTANCE); } @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) public static void onAfterExecute(@Advice.Return @Nullable CloseableHttpResponse response, @Advice.Enter @Nullable Object spanObj, @Advice.Thrown @Nullable Throwable t) { - Span span = (Span) spanObj; - if (span == null) { - return; - } - try { - if (response != null && response.getStatusLine() != null) { - int statusCode = response.getStatusLine().getStatusCode(); - span.getContext().getHttp().withStatusCode(statusCode); - } - span.captureException(t); - } finally { - // in case of circular redirect, we get an exception but status code won't be available without response - // thus we have to deal with span outcome directly - if (t instanceof CircularRedirectException) { - span.withOutcome(Outcome.FAILURE); - } - - span.deactivate().end(); - } + endSpan(adapter, spanObj, t, response); } } @Override public String getAdviceClassName() { - return "co.elastic.apm.agent.httpclient.v4.ApacheHttpClientInstrumentation$ApacheHttpClientAdvice"; + return "co.elastic.apm.agent.httpclient.v4.ApacheHttpClientInstrumentation$ApacheHttpClient4Advice"; } @Override diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4ApiAdapter.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4ApiAdapter.java new file mode 100644 index 0000000000..0b3c0ad4a4 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4ApiAdapter.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v4.helper; + + +import co.elastic.apm.agent.httpclient.common.ApacheHttpClientApiAdapter; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.StatusLine; +import org.apache.http.client.CircularRedirectException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpRequestWrapper; + +import java.net.URI; + +public class ApacheHttpClient4ApiAdapter implements ApacheHttpClientApiAdapter { + private static final ApacheHttpClient4ApiAdapter INSTANCE = new ApacheHttpClient4ApiAdapter(); + + private ApacheHttpClient4ApiAdapter() { + } + + public static ApacheHttpClient4ApiAdapter get() { + return INSTANCE; + } + + @Override + public String getMethod(HttpRequestWrapper request) { + return request.getMethod(); + } + + @Override + public URI getUri(HttpRequestWrapper request) { + return request.getURI(); + } + + @Override + public CharSequence getHostName(HttpHost httpHost) { + return httpHost.getHostName(); + } + + @Override + public int getResponseCode(CloseableHttpResponse closeableHttpResponse) { + final StatusLine statusLine = closeableHttpResponse.getStatusLine(); + if (statusLine == null) { + return 0; + } + return statusLine.getStatusCode(); + } + + @Override + public boolean isCircularRedirectException(Throwable t) { + return t instanceof CircularRedirectException; + } + + @Override + public boolean isNotNullStatusLine(CloseableHttpResponse o) { + return null != o.getStatusLine(); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpAsyncClientHelper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4AsyncHelper.java similarity index 79% rename from apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpAsyncClientHelper.java rename to apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4AsyncHelper.java index f9c9cb1e81..ed4ef3c8cb 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpAsyncClientHelper.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/ApacheHttpClient4AsyncHelper.java @@ -18,6 +18,7 @@ */ package co.elastic.apm.agent.httpclient.v4.helper; +import co.elastic.apm.agent.httpclient.common.ApacheHttpClientAsyncHelper; import co.elastic.apm.agent.tracer.ElasticContext; import co.elastic.apm.agent.tracer.GlobalTracer; import co.elastic.apm.agent.tracer.Span; @@ -31,7 +32,7 @@ import javax.annotation.Nullable; -public class ApacheHttpAsyncClientHelper { +public class ApacheHttpClient4AsyncHelper implements ApacheHttpClientAsyncHelper, HttpContext> { private static final int MAX_POOLED_ELEMENTS = 256; @@ -39,7 +40,7 @@ public class ApacheHttpAsyncClientHelper { private final ObjectPool requestProducerWrapperObjectPool; private final ObjectPool> futureCallbackWrapperObjectPool; - public ApacheHttpAsyncClientHelper() { + public ApacheHttpClient4AsyncHelper() { tracer = GlobalTracer.get(); ObjectPoolFactory factory = tracer.getObjectPoolFactory(); requestProducerWrapperObjectPool = factory.createRecyclableObjectPool(MAX_POOLED_ELEMENTS, new RequestProducerWrapperAllocator()); @@ -53,14 +54,14 @@ public Tracer getTracer() { private class RequestProducerWrapperAllocator implements Allocator { @Override public HttpAsyncRequestProducerWrapper createInstance() { - return new HttpAsyncRequestProducerWrapper(ApacheHttpAsyncClientHelper.this); + return new HttpAsyncRequestProducerWrapper(ApacheHttpClient4AsyncHelper.this); } } private class FutureCallbackWrapperAllocator implements Allocator> { @Override public FutureCallbackWrapper createInstance() { - return new FutureCallbackWrapper(ApacheHttpAsyncClientHelper.this); + return new FutureCallbackWrapper<>(ApacheHttpClient4AsyncHelper.this); } } @@ -69,10 +70,17 @@ public HttpAsyncRequestProducerWrapper wrapRequestProducer(HttpAsyncRequestProdu return requestProducerWrapperObjectPool.createInstance().with(requestProducer, span, toPropagate); } - public FutureCallbackWrapper wrapFutureCallback(FutureCallback futureCallback, HttpContext context, Span span) { - return ((FutureCallbackWrapper) futureCallbackWrapperObjectPool.createInstance()).with(futureCallback, context, span); + @Override + public FutureCallbackWrapper wrapFutureCallback(FutureCallback futureCallback, HttpContext context, Span span) { + return futureCallbackWrapperObjectPool.createInstance().with(futureCallback, context, span); } + @Override + public void failedBeforeRequestStarted(FutureCallbackWrapper cb, Throwable t) { + cb.failedWithoutExecution(t); + } + + @Override public void recycle(HttpAsyncRequestProducerWrapper requestProducerWrapper) { requestProducerWrapperObjectPool.recycle(requestProducerWrapper); } diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/FutureCallbackWrapper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/FutureCallbackWrapper.java index ed90963baa..9b54c428a0 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/FutureCallbackWrapper.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/FutureCallbackWrapper.java @@ -30,14 +30,14 @@ import javax.annotation.Nullable; public class FutureCallbackWrapper implements FutureCallback, Recyclable { - private final ApacheHttpAsyncClientHelper helper; + private final ApacheHttpClient4AsyncHelper helper; @Nullable private FutureCallback delegate; @Nullable private HttpContext context; private volatile Span span; - FutureCallbackWrapper(ApacheHttpAsyncClientHelper helper) { + FutureCallbackWrapper(ApacheHttpClient4AsyncHelper helper) { this.helper = helper; } diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/HttpAsyncRequestProducerWrapper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/HttpAsyncRequestProducerWrapper.java index 9daf9fa975..4d749ffce7 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/HttpAsyncRequestProducerWrapper.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/helper/HttpAsyncRequestProducerWrapper.java @@ -35,7 +35,7 @@ import java.io.IOException; public class HttpAsyncRequestProducerWrapper implements HttpAsyncRequestProducer, Recyclable { - private final ApacheHttpAsyncClientHelper asyncClientHelper; + private final ApacheHttpClient4AsyncHelper asyncClientHelper; private volatile HttpAsyncRequestProducer delegate; @Nullable @@ -44,7 +44,7 @@ public class HttpAsyncRequestProducerWrapper implements HttpAsyncRequestProducer @Nullable private Span span; - HttpAsyncRequestProducerWrapper(ApacheHttpAsyncClientHelper helper) { + HttpAsyncRequestProducerWrapper(ApacheHttpClient4AsyncHelper helper) { this.asyncClientHelper = helper; } diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/test/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentationTest.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/test/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentationTest.java index 060021ac3c..9a62a4bd9c 100644 --- a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/test/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentationTest.java +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/test/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentationTest.java @@ -19,11 +19,14 @@ package co.elastic.apm.agent.httpclient.v4; import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.tracer.Outcome; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.concurrent.FutureCallback; +import org.apache.http.conn.UnsupportedSchemeException; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.assertj.core.api.Assertions; @@ -104,4 +107,23 @@ public void testSpanFinishOnEarlyException() throws Exception { Assertions.assertThat(reporter.getSpans()).hasSize(1); } + @Test + public void testSpanWithIllegalProtocol() throws Exception { + reporter.disableCheckServiceTarget(); + reporter.disableCheckDestinationAddress(); + try { + String illegalProtocol = "ottp"; + String url = getBaseUrl().replaceAll("http", illegalProtocol) + "/"; + assertThatThrownBy(() -> performGet(url)).cause().isInstanceOf(UnsupportedSchemeException.class); + } finally { + setUp(); + reporter.resetChecks(); + } + Span firstSpan = reporter.getFirstSpan(500); + assertThat(firstSpan).isNotNull(); + assertThat(firstSpan.getOutcome()).isEqualTo(Outcome.FAILURE); + Assertions.assertThat(reporter.getSpans()).hasSize(1); + } + + } diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/pom.xml b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/pom.xml new file mode 100644 index 0000000000..eeaf564549 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + co.elastic.apm + apm-apache-httpclient + 1.44.1-SNAPSHOT + + + apm-apache-httpclient5-plugin + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + 5.2.1 + + + + + ${project.groupId} + apm-apache-httpclient-common + ${project.version} + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient.version} + provided + + + + + diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClient5Instrumentation.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClient5Instrumentation.java new file mode 100644 index 0000000000..0848be4d92 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClient5Instrumentation.java @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5; + +import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAsyncAdvice; +import co.elastic.apm.agent.httpclient.v5.helper.ApacheHttpClient5AsyncHelper; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.protocol.HttpContext; + +import javax.annotation.Nullable; + +import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass; +import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC; +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +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; + +public class ApacheHttpAsyncClient5Instrumentation extends BaseApacheHttpClient5Instrumentation { + + @Override + public String getAdviceClassName() { + return "co.elastic.apm.agent.httpclient.v5.ApacheHttpAsyncClient5Instrumentation$ApacheHttpClient5AsyncAdvice"; + } + + @Override + public ElementMatcher.Junction getClassLoaderMatcher() { + return not(isBootstrapClassLoader()).and(classLoaderCanLoadClass("org.apache.hc.client5.http.async.HttpAsyncClient")); + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameContains("HttpAsyncClient"); + } + + @Override + public ElementMatcher getTypeMatcher() { + return hasSuperType(named("org.apache.hc.client5.http.async.HttpAsyncClient")); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("execute").and(takesArguments(4)) + .and(takesArgument(0, named("org.apache.hc.core5.http.nio.AsyncRequestProducer"))) + .and(takesArgument(1, named("org.apache.hc.core5.http.nio.AsyncResponseConsumer"))) + .and(takesArgument(2, named("org.apache.hc.core5.http.protocol.HttpContext"))) + .and(takesArgument(3, named("org.apache.hc.core5.concurrent.FutureCallback"))); + } + + public static class ApacheHttpClient5AsyncAdvice extends AbstractApacheHttpClientAsyncAdvice { + + private static ApacheHttpClient5AsyncHelper asyncHelper = new ApacheHttpClient5AsyncHelper(); + + @Nullable + @Advice.AssignReturned.ToArguments({ + @Advice.AssignReturned.ToArguments.ToArgument(index = 0, value = 0, typing = DYNAMIC), + @Advice.AssignReturned.ToArguments.ToArgument(index = 1, value = 3, typing = DYNAMIC) + }) + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static Object[] onBeforeExecute(@Advice.Argument(value = 0) AsyncRequestProducer asyncRequestProducer, @Advice.Argument(value = 2) HttpContext context, @Advice.Argument(value = 3) FutureCallback futureCallback) { + return startSpan(tracer, asyncHelper, asyncRequestProducer, context, futureCallback); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.Enter @Nullable Object[] enter, @Advice.Thrown @Nullable Throwable t) { + endSpan(asyncHelper, enter, t); + } + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClient5Instrumentation.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClient5Instrumentation.java new file mode 100644 index 0000000000..0f03d73ab8 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClient5Instrumentation.java @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5; + + +import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAdvice; +import co.elastic.apm.agent.httpclient.v5.helper.ApacheHttpClient5ApiAdapter; +import co.elastic.apm.agent.httpclient.v5.helper.RequestHeaderAccessor; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; + +import javax.annotation.Nullable; +import java.net.URISyntaxException; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class ApacheHttpClient5Instrumentation extends BaseApacheHttpClient5Instrumentation { + + public static class ApacheHttpClient5Advice extends AbstractApacheHttpClientAdvice { + private static final ApacheHttpClient5ApiAdapter adapter = ApacheHttpClient5ApiAdapter.get(); + + @Nullable + @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) + public static Object onBeforeExecute(@Advice.Argument(0) HttpHost httpHost, + @Advice.Argument(1) ClassicHttpRequest request) throws URISyntaxException { + return startSpan(tracer, adapter, request, httpHost, RequestHeaderAccessor.INSTANCE); + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) + public static void onAfterExecute(@Advice.Return @Nullable CloseableHttpResponse response, + @Advice.Enter @Nullable Object spanObj, + @Advice.Thrown @Nullable Throwable t) { + endSpan(adapter, spanObj, t, response); + } + } + + @Override + public String getAdviceClassName() { + return "co.elastic.apm.agent.httpclient.v5.ApacheHttpClient5Instrumentation$ApacheHttpClient5Advice"; + } + + @Override + public ElementMatcher getTypeMatcher() { + return hasSuperType(named("org.apache.hc.client5.http.impl.classic.CloseableHttpClient")); + } + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameContains("HttpClient"); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("doExecute") + .and(takesArguments(3)) + .and(returns(hasSuperType(named("org.apache.hc.client5.http.impl.classic.CloseableHttpResponse")))) + .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"))); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/BaseApacheHttpClient5Instrumentation.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/BaseApacheHttpClient5Instrumentation.java new file mode 100644 index 0000000000..4909dd412e --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/BaseApacheHttpClient5Instrumentation.java @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5; + +import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; +import co.elastic.apm.agent.tracer.GlobalTracer; +import co.elastic.apm.agent.tracer.Tracer; + +import java.util.Arrays; +import java.util.Collection; + +public abstract class BaseApacheHttpClient5Instrumentation extends ElasticApmInstrumentation { + + static final Tracer tracer = GlobalTracer.get(); + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList("http-client", "apache-httpclient"); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5ApiAdapter.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5ApiAdapter.java new file mode 100644 index 0000000000..2bff7de991 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5ApiAdapter.java @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + +import co.elastic.apm.agent.httpclient.common.ApacheHttpClientApiAdapter; +import org.apache.hc.client5.http.CircularRedirectException; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; + +import java.net.URI; +import java.net.URISyntaxException; + +public class ApacheHttpClient5ApiAdapter implements ApacheHttpClientApiAdapter { + private static final ApacheHttpClient5ApiAdapter INSTANCE = new ApacheHttpClient5ApiAdapter(); + + private ApacheHttpClient5ApiAdapter() { + } + + public static ApacheHttpClient5ApiAdapter get() { + return INSTANCE; + } + + @Override + public String getMethod(ClassicHttpRequest request) { + return request.getMethod(); + } + + @Override + public URI getUri(ClassicHttpRequest request) throws URISyntaxException { + return request.getUri(); + } + + @Override + public CharSequence getHostName(HttpHost httpHost) { + return httpHost.getHostName(); + } + + @Override + public int getResponseCode(CloseableHttpResponse closeableHttpResponse) { + return closeableHttpResponse.getCode(); + } + + @Override + public boolean isCircularRedirectException(Throwable t) { + return t instanceof CircularRedirectException; + } + + @Override + public boolean isNotNullStatusLine(CloseableHttpResponse closeableHttpResponse) { + // HTTP response messages in HttpClient 5.x no longer have a status line. + return true; + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5AsyncHelper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5AsyncHelper.java new file mode 100644 index 0000000000..9c2a4462f5 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/ApacheHttpClient5AsyncHelper.java @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + + +import co.elastic.apm.agent.httpclient.common.ApacheHttpClientAsyncHelper; +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.GlobalTracer; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.Tracer; +import co.elastic.apm.agent.tracer.pooling.Allocator; +import co.elastic.apm.agent.tracer.pooling.ObjectPool; +import co.elastic.apm.agent.tracer.pooling.ObjectPoolFactory; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.protocol.HttpContext; + +import javax.annotation.Nullable; + +public class ApacheHttpClient5AsyncHelper implements ApacheHttpClientAsyncHelper, HttpContext> { + + private static final int MAX_POOLED_ELEMENTS = 256; + + private final ObjectPool requestProducerWrapperObjectPool; + private final ObjectPool> futureCallbackWrapperObjectPool; + private final ObjectPool requestChannelWrapperObjectPool; + + public ApacheHttpClient5AsyncHelper() { + Tracer tracer = GlobalTracer.get(); + + ObjectPoolFactory factory = tracer.getObjectPoolFactory(); + requestProducerWrapperObjectPool = factory.createRecyclableObjectPool(MAX_POOLED_ELEMENTS, new RequestProducerWrapperAllocator()); + futureCallbackWrapperObjectPool = factory.createRecyclableObjectPool(MAX_POOLED_ELEMENTS, new FutureCallbackWrapperAllocator()); + requestChannelWrapperObjectPool = factory.createRecyclableObjectPool(MAX_POOLED_ELEMENTS, new RequestChannelWrapperAllocator()); + } + + private class RequestProducerWrapperAllocator implements Allocator { + @Override + public AsyncRequestProducerWrapper createInstance() { + return new AsyncRequestProducerWrapper(ApacheHttpClient5AsyncHelper.this); + } + } + + private class FutureCallbackWrapperAllocator implements Allocator> { + @Override + public FutureCallbackWrapper createInstance() { + return new FutureCallbackWrapper<>(ApacheHttpClient5AsyncHelper.this); + } + } + + private static class RequestChannelWrapperAllocator implements Allocator { + @Override + public RequestChannelWrapper createInstance() { + return new RequestChannelWrapper(); + } + } + + public AsyncRequestProducerWrapper wrapRequestProducer(AsyncRequestProducer requestProducer, @Nullable Span span, + @Nullable ElasticContext toPropagate) { + return requestProducerWrapperObjectPool.createInstance().with(requestProducer, span, toPropagate); + } + + @Override + public FutureCallbackWrapper wrapFutureCallback(FutureCallback futureCallback, HttpContext context, Span span) { + return futureCallbackWrapperObjectPool.createInstance().with(futureCallback, context, span); + } + + @Override + public void failedBeforeRequestStarted(FutureCallbackWrapper cb, Throwable t) { + cb.failedWithoutExecution(t); + } + + public RequestChannelWrapper wrapRequestChannel(RequestChannel requestChannel, @Nullable Span span, @Nullable ElasticContext toPropagate) { + return requestChannelWrapperObjectPool.createInstance().with(requestChannel, span, toPropagate); + } + + public void recycle(AsyncRequestProducerWrapper requestProducerWrapper) { + requestProducerWrapperObjectPool.recycle(requestProducerWrapper); + } + + void recycle(FutureCallbackWrapper futureCallbackWrapper) { + futureCallbackWrapperObjectPool.recycle(futureCallbackWrapper); + } + + void recycle(RequestChannelWrapper requestChannelWrapper) { + requestChannelWrapperObjectPool.recycle(requestChannelWrapper); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/AsyncRequestProducerWrapper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/AsyncRequestProducerWrapper.java new file mode 100644 index 0000000000..9000796fda --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/AsyncRequestProducerWrapper.java @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + + +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.protocol.HttpContext; + +import javax.annotation.Nullable; +import java.io.IOException; + +public class AsyncRequestProducerWrapper implements AsyncRequestProducer, Recyclable { + + private final ApacheHttpClient5AsyncHelper asyncClientHelper; + private volatile AsyncRequestProducer delegate; + + @Nullable + private ElasticContext toPropagate; + + @Nullable + private Span span; + + AsyncRequestProducerWrapper(ApacheHttpClient5AsyncHelper helper) { + this.asyncClientHelper = helper; + } + + public AsyncRequestProducerWrapper with(AsyncRequestProducer delegate, @Nullable Span span, + ElasticContext toPropagate) { + this.span = span; + toPropagate.incrementReferences(); + this.toPropagate = toPropagate; + this.delegate = delegate; + return this; + } + + /** + * Here we should catch {@link IllegalStateException} in cases + * when {@link CloseableHttpAsyncClient#close()} executed. + */ + @Override + public void sendRequest(RequestChannel requestChannel, HttpContext httpContext) throws HttpException, IOException { + RequestChannelWrapper wrappedRequestChannel = asyncClientHelper.wrapRequestChannel(requestChannel, span, toPropagate); + boolean isNotNullWrappedRequestChannel = null != wrappedRequestChannel; + try { + delegate.sendRequest(isNotNullWrappedRequestChannel ? wrappedRequestChannel : requestChannel, httpContext); + } catch (HttpException | IOException | IllegalStateException e) { + asyncClientHelper.recycle(this); + throw e; + } finally { + if (isNotNullWrappedRequestChannel) { + asyncClientHelper.recycle(wrappedRequestChannel); + } + } + } + + @Override + public boolean isRepeatable() { + return delegate.isRepeatable(); + } + + @Override + public void failed(Exception e) { + delegate.failed(e); + } + + @Override + public int available() { + return delegate.available(); + } + + @Override + public void produce(DataStreamChannel dataStreamChannel) throws IOException { + delegate.produce(dataStreamChannel); + } + + @Override + public void releaseResources() { + if (delegate != null) { + delegate.releaseResources(); + asyncClientHelper.recycle(this); + } + } + + @Override + public void resetState() { + span = null; + if (toPropagate != null) { + toPropagate.decrementReferences(); + toPropagate = null; + } + delegate = null; + } + +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/FutureCallbackWrapper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/FutureCallbackWrapper.java new file mode 100644 index 0000000000..498d66e6d1 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/FutureCallbackWrapper.java @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + + +import co.elastic.apm.agent.tracer.Outcome; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.protocol.HttpCoreContext; + +import javax.annotation.Nullable; + +class FutureCallbackWrapper implements FutureCallback, Recyclable { + private final ApacheHttpClient5AsyncHelper helper; + @Nullable + private FutureCallback delegate; + @Nullable + private HttpContext context; + private volatile Span span; + + FutureCallbackWrapper(ApacheHttpClient5AsyncHelper helper) { + this.helper = helper; + } + + FutureCallbackWrapper with(@Nullable FutureCallback delegate, @Nullable HttpContext context, Span span) { + this.delegate = delegate; + this.context = context; + // write to volatile field last + this.span = span; + return this; + } + + @Override + public void completed(T result) { + try { + finishSpan(null); + } finally { + try { + if (delegate != null) { + delegate.completed(result); + } + } finally { + helper.recycle(this); + } + } + } + + @Override + public void failed(Exception ex) { + try { + finishSpan(ex); + } finally { + try { + if (delegate != null) { + delegate.failed(ex); + } + } finally { + helper.recycle(this); + } + } + } + + public void failedWithoutExecution(Throwable ex) { + try { + final Span localSpan = span; + localSpan.captureException(ex).end(); + } finally { + helper.recycle(this); + } + } + + @Override + public void cancelled() { + try { + finishSpan(null); + } finally { + try { + if (delegate != null) { + delegate.cancelled(); + } + } finally { + helper.recycle(this); + } + } + } + + private void finishSpan(@Nullable Exception e) { + // start by reading the volatile field + final Span localSpan = span; + try { + if (context != null) { + Object responseObject = context.getAttribute(HttpCoreContext.HTTP_RESPONSE); + if (responseObject instanceof HttpResponse) { + int statusCode = ((HttpResponse) responseObject).getCode(); + span.getContext().getHttp().withStatusCode(statusCode); + } + } + localSpan.captureException(e); + + if (e != null) { + localSpan.withOutcome(Outcome.FAILURE); + } + } finally { + localSpan.end(); + } + } + + @Override + public void resetState() { + delegate = null; + context = null; + // write to volatile field last + span = null; + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestChannelWrapper.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestChannelWrapper.java new file mode 100644 index 0000000000..9b9844f37a --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestChannelWrapper.java @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + + +import co.elastic.apm.agent.httpclient.HttpClientHelper; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.tracer.ElasticContext; +import co.elastic.apm.agent.tracer.Span; +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.protocol.HttpContext; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.URI; + +public class RequestChannelWrapper implements RequestChannel, Recyclable { + private static final Logger logger = LoggerFactory.getLogger(RequestChannelWrapper.class); + + private volatile RequestChannel delegate; + + @Nullable + private ElasticContext toPropagate; + + @Nullable + private Span span; + + public RequestChannelWrapper() { + } + + public RequestChannelWrapper with(RequestChannel delegate, + @Nullable Span span, + ElasticContext toPropagate) { + this.span = span; + toPropagate.incrementReferences(); + this.toPropagate = toPropagate; + this.delegate = delegate; + return this; + } + + @Override + public void resetState() { + span = null; + if (toPropagate != null) { + toPropagate.decrementReferences(); + toPropagate = null; + } + delegate = null; + } + + @Override + public void sendRequest(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException { + try { + if (toPropagate == null) { + throw new IllegalStateException("sendRequest was called before 'with'!"); + } + + if (httpRequest != null) { + if (span != null) { + String host = null; + String protocol = null; + int port = -1; + URI httpRequestURI = null; + try { + httpRequestURI = httpRequest.getUri(); + if (httpRequestURI != null) { + host = httpRequestURI.getHost(); + port = httpRequestURI.getPort(); + protocol = httpRequestURI.getScheme(); + } + } catch (Exception e) { + logger.warn("Failed to obtain Apache HttpClient 5 destination info, null httpRequestURI", e); + } + String method = httpRequest.getMethod(); + span.withName(method).appendToName(" "); + if (host != null) { + span.appendToName(host); + } + span.getContext().getHttp().withMethod(method).withUrl(httpRequest.getRequestUri()); + HttpClientHelper.setDestinationServiceDetails(span, protocol, host, port); + } + + toPropagate.propagateContext(httpRequest, RequestHeaderAccessor.INSTANCE, RequestHeaderAccessor.INSTANCE); + } + + toPropagate.decrementReferences(); + toPropagate = null; + } finally { + delegate.sendRequest(httpRequest, entityDetails, httpContext); + } + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestHeaderAccessor.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestHeaderAccessor.java new file mode 100644 index 0000000000..c547eb8d1b --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/java/co/elastic/apm/agent/httpclient/v5/helper/RequestHeaderAccessor.java @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5.helper; + +import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter; +import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpRequest; + +import javax.annotation.Nullable; + +public class RequestHeaderAccessor implements TextHeaderGetter, TextHeaderSetter { + + public static final RequestHeaderAccessor INSTANCE = new RequestHeaderAccessor(); + + @Nullable + @Override + public String getFirstHeader(String headerName, HttpRequest request) { + Header header = request.getFirstHeader(headerName); + if (header == null) { + return null; + } + return header.getValue(); + } + + @Override + public void forEach(String headerName, HttpRequest carrier, S state, HeaderConsumer consumer) { + Header[] headers = carrier.getHeaders(headerName); + if (headers == null) { + return; + } + for (Header header : headers) { + consumer.accept(header.getValue(), state); + } + } + + @Override + public void setHeader(String headerName, String headerValue, HttpRequest request) { + request.setHeader(headerName, headerValue); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..04dd6ced97 --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1,2 @@ +co.elastic.apm.agent.httpclient.v5.ApacheHttpClient5Instrumentation +co.elastic.apm.agent.httpclient.v5.ApacheHttpAsyncClient5Instrumentation diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClientInstrumentationTest.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClientInstrumentationTest.java new file mode 100644 index 0000000000..4b0ccc72eb --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpAsyncClientInstrumentationTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5; + +import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.tracer.Outcome; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.net.URIAuthority; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import static co.elastic.apm.agent.testutils.assertions.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ApacheHttpAsyncClientInstrumentationTest extends AbstractHttpClientInstrumentationTest { + + private static CloseableHttpAsyncClient client; + + @BeforeClass + public static void setUp() { + client = HttpAsyncClients.createDefault(); + client.start(); + } + + @AfterClass + public static void tearDown() throws IOException { + client.close(); + } + + @Override + protected void performGet(String path) throws Exception { + final CompletableFuture responseFuture = new CompletableFuture<>(); + SimpleHttpRequest req = SimpleRequestBuilder.get().setPath(path) + .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setCircularRedirectsAllowed(true) + .build(); + HttpClientContext httpClientContext = HttpClientContext.create(); + httpClientContext.setRequestConfig(requestConfig); + client.execute(req, httpClientContext, new FutureCallback() { + @Override + public void completed(SimpleHttpResponse simpleHttpResponse) { + responseFuture.complete(simpleHttpResponse); + } + + @Override + public void failed(Exception e) { + responseFuture.completeExceptionally(e); + } + + @Override + public void cancelled() { + responseFuture.cancel(true); + } + }); + + responseFuture.get(); + } + + + @Test + public void testSpanFinishOnEarlyException() throws Exception { + + client.close(); //this forces execute to immediately exit with an exception + + reporter.disableCheckServiceTarget(); + reporter.disableCheckDestinationAddress(); + try { + assertThatThrownBy(() -> performGet(getBaseUrl() + "/")).cause().isInstanceOf(IllegalStateException.class); + } finally { + //Reset state for other tests + setUp(); + reporter.resetChecks(); + } + assertThat(reporter.getFirstSpan(500)).isNotNull(); + assertThat(reporter.getSpans()).hasSize(1); + } + + @Test + public void testSpanFinishWithIllegalProtocol() throws Exception { + reporter.disableCheckServiceTarget(); + reporter.disableCheckDestinationAddress(); + String url = getBaseUrl().replaceAll("http", "ottp") + "/"; + performGet(url); + + Span firstSpan = reporter.getFirstSpan(500); + assertThat(firstSpan).isNotNull(); + assertThat(firstSpan.getOutcome()).isEqualTo(Outcome.FAILURE); + assertThat(firstSpan.getNameAsString()).isEqualTo("GET localhost"); + assertThat(reporter.getSpans()).hasSize(1); + } + + @Test + public void testSpanFinishWithIllegalUrl() throws Exception { + reporter.disableCheckServiceTarget(); + reporter.disableCheckDestinationAddress(); + String url = getBaseUrl().replaceAll("http:/", "") + "/"; + + try { + assertThatThrownBy(() -> performGet(url)).cause().isInstanceOf(ProtocolException.class); + } finally { + //Reset state for other tests + setUp(); + reporter.resetChecks(); + } + + Span firstSpan = reporter.getFirstSpan(500); + assertThat(firstSpan).isNotNull(); + assertThat(firstSpan.getOutcome()).isEqualTo(Outcome.FAILURE); + assertThat(firstSpan.getNameAsString()).isEqualTo("GET "); + assertThat(reporter.getSpans()).hasSize(1); + } + + /** + * Difference between sync and async requests is that + * In async requests you need {@link SimpleRequestBuilder#setAuthority(URIAuthority)} explicitly + * And in this case exception will be thrown from {@link org.apache.hc.client5.http.impl.async.AsyncProtocolExec#execute} + *

+ * SimpleHttpRequest req = SimpleRequestBuilder.get().setPath(path) + * .setScheme("http") + * .setAuthority(new URIAuthority(uri.getUserInfo(), uri.getHost(), uri.getPort())) + * .build(); + */ + @Override + public boolean isTestHttpCallWithUserInfoEnabled() { + return true; + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java new file mode 100644 index 0000000000..7ed5a2163c --- /dev/null +++ b/apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient5-plugin/src/test/java/co/elastic/apm/agent/httpclient/v5/ApacheHttpClientInstrumentationTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.httpclient.v5; + + +import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest; +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; + +public class ApacheHttpClientInstrumentationTest extends AbstractHttpClientInstrumentationTest { + + private static CloseableHttpClient client; + + @BeforeClass + public static void setUp() { + client = HttpClients.createDefault(); + } + + @AfterClass + public static void close() throws IOException { + client.close(); + } + + /** + * RFC 7230: treat presence of userinfo in authority component in request URI as an HTTP protocol violation. + * + * Uses {@link org.apache.hc.core5.http.message.BasicHttpRequest#setUri} to fill {@link org.apache.hc.core5.net.URIAuthority} + * + * Assertions on authority in {@link org.apache.hc.client5.http.impl.classic.ProtocolExec#execute} + */ + @Override + public boolean isTestHttpCallWithUserInfoEnabled() { + return false; + } + + @Override + protected void performGet(String path) throws Exception { + HttpClientResponseHandler responseHandler = response -> { + int status = response.getCode(); + if (status >= 200 && status < 300) { + HttpEntity entity = response.getEntity(); + String res = entity != null ? EntityUtils.toString(entity) : null; + return res; + } else { + throw new ClientProtocolException("Unexpected response status: " + status); + } + }; + String response = client.execute(new HttpGet(path), responseHandler); + } +} diff --git a/apm-agent-plugins/apm-apache-httpclient/pom.xml b/apm-agent-plugins/apm-apache-httpclient/pom.xml index 8fccd3bf88..003a279727 100644 --- a/apm-agent-plugins/apm-apache-httpclient/pom.xml +++ b/apm-agent-plugins/apm-apache-httpclient/pom.xml @@ -18,7 +18,9 @@ apm-apache-httpclient3-plugin + apm-apache-httpclient-common apm-apache-httpclient4-plugin + apm-apache-httpclient5-plugin diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml index 816a10a8df..4f53239439 100644 --- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml +++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml @@ -14,6 +14,7 @@ ${project.basedir}/../../.. true + 13.0 @@ -70,6 +71,31 @@ ${project.version} test + + ${project.groupId} + apm-apache-httpclient5-plugin + ${project.version} + test + + + io.github.openfeign + feign-core + ${feign.version} + test + + + io.github.openfeign + feign-hc5 + ${feign.version} + test + + + io.github.openfeign + feign-jaxrs2 + ${feign.version} + test + + diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/FeignClientTest.java b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/FeignClientTest.java new file mode 100644 index 0000000000..ae9ebf00d1 --- /dev/null +++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/FeignClientTest.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.apm.agent.resttemplate; + +import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.context.Http; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.Transaction; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import feign.Feign; +import feign.hc5.ApacheHttp5Client; +import feign.jaxrs.JAXRSContract; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +public class FeignClientTest extends AbstractInstrumentationTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort(), false); + + private Transaction rootTransaction; + + private String baseUrl = null; + + @Before + public void before() { + baseUrl = String.format("http://127.0.0.1:%d/", wireMockRule.port()); + + wireMockRule.stubFor(any(urlEqualTo("/")) + .willReturn(okJson("{}") + .withStatus(200))); + + rootTransaction = tracer.startRootTransaction(getClass().getClassLoader()); + rootTransaction.withName("parent of http span") + .withType("request") + .activate(); + } + + @After + public void after() { + rootTransaction.deactivate().end(); + reporter.awaitTransactionCount(1); + } + + @Test + public void testThatSpanCreated() { + final JaxRsTestInterface testInterface = buildTestInterface(); + + testInterface.getRoot(); + + reporter.awaitSpanCount(1); + + Span span = reporter.getFirstSpan(); + Http http = span.getContext().getHttp(); + + assertThat(span.getNameAsString()).isEqualTo("GET 127.0.0.1"); + assertThat(http.getMethod()).isEqualTo("GET"); + assertThat(http.getUrl().toString()).isNotNull().isEqualTo(baseUrl); + } + + private JaxRsTestInterface buildTestInterface() { + return Feign.builder() + .contract(new JAXRSContract()) + .client(new ApacheHttp5Client(HttpClientBuilder.create().build())) + .target(JaxRsTestInterface.class, baseUrl); + } + + + @Path("/") + public interface JaxRsTestInterface { + + @GET + @Path("/") + String getRoot(); + } +} diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 910b421953..57102e4a7c 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -319,6 +319,7 @@ The spans are named after the schema ` `, for example `GET elastic | | 0.7.0 (4.3+) 1.8.0 (4.0+) + 1.45.0 (5.0+) |Apache HttpClient (Legacy) |3.0+