Skip to content

Commit

Permalink
Apachehttpclient v5 (#3419)
Browse files Browse the repository at this point in the history
* added apachehttpclient common module

* added apachehttpclient5 sync instrumentation

* added apachehttpclient async impl, covered with tests

* added comments for apache http client works with urlpath that contains userinfo

* set isTestHttpCallWithUserInfoEnabled with false value in ApacheHttpClientInstrumentationTest 5

* handled case with recycling when client closed before executing

* added httpclient5 plugin to agent builds pom. Run tests, generated license headers

* added entry to changelog

* updated supported technologies

* added comment

* added test that checks feignClient

* minor polish

* updated configuration.asciidoc

* minor polish according to the review comments

* updated failedWithoutException call for apachehttpclient 5. recycle AsyncRequestProducerWrapper in case when delegate is not null. Added tests that breaks behavior when asyncClientHelper.recycle(this); called multiple times when delegate is already null in ASyncRequestProductWrapper

* updated docs

* renamed class names for consistency

* fixes according to comments

* wrapped with try/finally block. removed static import

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
videnkz and mergify[bot] committed Dec 5, 2023
1 parent d1aaa52 commit eb349d3
Show file tree
Hide file tree
Showing 32 changed files with 1,599 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions apm-agent-builds/pom.xml
Expand Up @@ -52,6 +52,11 @@
<artifactId>apm-apache-httpclient4-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-apache-httpclient5-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-api-plugin</artifactId>
Expand Down
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>apm-apache-httpclient</artifactId>
<groupId>co.elastic.apm</groupId>
<version>1.44.1-SNAPSHOT</version>
</parent>

<artifactId>apm-apache-httpclient-common</artifactId>
<name>${project.groupId}:${project.artifactId}</name>

<properties>
<apm-agent-parent.base.dir>${project.basedir}/../../..</apm-agent-parent.base.dir>
</properties>

</project>
@@ -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 <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE,
HeaderAccessor extends TextHeaderSetter<REQUEST> &
TextHeaderGetter<REQUEST>> Object startSpan(final Tracer tracer,
final ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> 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 <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> void endSpan(ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> 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();
}
}
}
@@ -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 <PRODUCER, WRAPPER extends PRODUCER, CALLBACK, CALLBACK_WRAPPER extends CALLBACK, CONTEXT> Object[] startSpan(
Tracer tracer, ApacheHttpClientAsyncHelper<PRODUCER, WRAPPER, CALLBACK, CALLBACK_WRAPPER, CONTEXT> 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 <PRODUCER, WRAPPER extends PRODUCER, CALLBACK, CALLBACK_WRAPPER extends CALLBACK, CONTEXT> void endSpan(
ApacheHttpClientAsyncHelper<PRODUCER, WRAPPER, CALLBACK, CALLBACK_WRAPPER, CONTEXT> 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);
}
}
}
}
@@ -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<REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> {
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);
}
@@ -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<AsyncProducer, AsyncProducerWrapper extends AsyncProducer, FutureCallback, FutureCallbackWrapper extends FutureCallback, HttpContext> {

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

}
Expand Up @@ -20,6 +20,11 @@
this dependency duplicates the transitive one we get from 'httpasyncclient'
but keeping it explicit avoids relying on transitive dependency
-->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-apache-httpclient-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
Expand Down
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -79,8 +75,8 @@ public ElementMatcher<? super MethodDescription> 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),
Expand All @@ -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);
}
}
}

0 comments on commit eb349d3

Please sign in to comment.