Skip to content

Commit eb349d3

Browse files
Apachehttpclient v5 (#3419)
* 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>
1 parent d1aaa52 commit eb349d3

File tree

32 files changed

+1599
-90
lines changed

32 files changed

+1599
-90
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes:
3636
* Added support for OpenTelemetry annotations - `WithSpan` and `SpanAttribute` - {pull}3406[#3406]
3737
* 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]
3838
* Added support for Spring 6.1 / Spring-Boot 3.2 - {pull}3440[#3440]
39+
* Add support for Apache HTTP client 5.x - {pull}3419[#3419]
3940
4041
[[release-notes-1.x]]
4142
=== Java Agent version 1.x

apm-agent-builds/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
<artifactId>apm-apache-httpclient4-plugin</artifactId>
5353
<version>${project.version}</version>
5454
</dependency>
55+
<dependency>
56+
<groupId>${project.groupId}</groupId>
57+
<artifactId>apm-apache-httpclient5-plugin</artifactId>
58+
<version>${project.version}</version>
59+
</dependency>
5560
<dependency>
5661
<groupId>${project.groupId}</groupId>
5762
<artifactId>apm-api-plugin</artifactId>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>apm-apache-httpclient</artifactId>
9+
<groupId>co.elastic.apm</groupId>
10+
<version>1.44.1-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>apm-apache-httpclient-common</artifactId>
14+
<name>${project.groupId}:${project.artifactId}</name>
15+
16+
<properties>
17+
<apm-agent-parent.base.dir>${project.basedir}/../../..</apm-agent-parent.base.dir>
18+
</properties>
19+
20+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.httpclient.common;
20+
21+
22+
import co.elastic.apm.agent.httpclient.HttpClientHelper;
23+
import co.elastic.apm.agent.tracer.ElasticContext;
24+
import co.elastic.apm.agent.tracer.Outcome;
25+
import co.elastic.apm.agent.tracer.Span;
26+
import co.elastic.apm.agent.tracer.Tracer;
27+
import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter;
28+
import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter;
29+
30+
import java.net.URISyntaxException;
31+
32+
public abstract class AbstractApacheHttpClientAdvice {
33+
34+
public static <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE,
35+
HeaderAccessor extends TextHeaderSetter<REQUEST> &
36+
TextHeaderGetter<REQUEST>> Object startSpan(final Tracer tracer,
37+
final ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> adapter,
38+
final WRAPPER request,
39+
final HTTPHOST httpHost,
40+
final HeaderAccessor headerAccessor) throws URISyntaxException {
41+
ElasticContext<?> elasticContext = tracer.currentContext();
42+
Span<?> span = null;
43+
if (elasticContext.getSpan() != null) {
44+
span = HttpClientHelper.startHttpClientSpan(elasticContext, adapter.getMethod(request), adapter.getUri(request), adapter.getHostName(httpHost));
45+
if (span != null) {
46+
span.activate();
47+
}
48+
}
49+
tracer.currentContext().propagateContext(request, headerAccessor, headerAccessor);
50+
return span;
51+
}
52+
53+
public static <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> void endSpan(ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> adapter,
54+
Object spanObj,
55+
Throwable t,
56+
RESPONSE response) {
57+
Span<?> span = (Span<?>) spanObj;
58+
if (span == null) {
59+
return;
60+
}
61+
try {
62+
if (response != null && adapter.isNotNullStatusLine(response)) {
63+
int statusCode = adapter.getResponseCode(response);
64+
span.getContext().getHttp().withStatusCode(statusCode);
65+
}
66+
span.captureException(t);
67+
} finally {
68+
// in case of circular redirect, we get an exception but status code won't be available without response
69+
// thus we have to deal with span outcome directly
70+
if (adapter.isCircularRedirectException(t)) {
71+
span.withOutcome(Outcome.FAILURE);
72+
}
73+
span.deactivate().end();
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.httpclient.common;
20+
21+
22+
import co.elastic.apm.agent.httpclient.HttpClientHelper;
23+
import co.elastic.apm.agent.tracer.ElasticContext;
24+
import co.elastic.apm.agent.tracer.Span;
25+
import co.elastic.apm.agent.tracer.Tracer;
26+
27+
public abstract class AbstractApacheHttpClientAsyncAdvice {
28+
29+
public static <PRODUCER, WRAPPER extends PRODUCER, CALLBACK, CALLBACK_WRAPPER extends CALLBACK, CONTEXT> Object[] startSpan(
30+
Tracer tracer, ApacheHttpClientAsyncHelper<PRODUCER, WRAPPER, CALLBACK, CALLBACK_WRAPPER, CONTEXT> asyncHelper,
31+
PRODUCER asyncRequestProducer, CONTEXT context, CALLBACK futureCallback) {
32+
33+
ElasticContext<?> parentContext = tracer.currentContext();
34+
if (parentContext.isEmpty()) {
35+
// performance optimization, no need to wrap if we have nothing to propagate
36+
// empty context means also we will not create an exit span
37+
return null;
38+
}
39+
CALLBACK wrappedFutureCallback = futureCallback;
40+
ElasticContext<?> activeContext = tracer.currentContext();
41+
Span<?> span = activeContext.createExitSpan();
42+
if (span != null) {
43+
span.withType(HttpClientHelper.EXTERNAL_TYPE)
44+
.withSubtype(HttpClientHelper.HTTP_SUBTYPE)
45+
.withSync(false)
46+
.activate();
47+
wrappedFutureCallback = asyncHelper.wrapFutureCallback(futureCallback, context, span);
48+
}
49+
PRODUCER wrappedProducer = asyncHelper.wrapRequestProducer(asyncRequestProducer, span, tracer.currentContext());
50+
return new Object[]{wrappedProducer, wrappedFutureCallback, span};
51+
}
52+
53+
public static <PRODUCER, WRAPPER extends PRODUCER, CALLBACK, CALLBACK_WRAPPER extends CALLBACK, CONTEXT> void endSpan(
54+
ApacheHttpClientAsyncHelper<PRODUCER, WRAPPER, CALLBACK, CALLBACK_WRAPPER, CONTEXT> asyncHelper, Object[] enter, Throwable t) {
55+
Span<?> span = enter != null ? (Span<?>) enter[2] : null;
56+
if (span != null) {
57+
// Deactivate in this thread
58+
span.deactivate();
59+
// End the span if the method terminated with an exception.
60+
// The exception means that the listener who normally does the ending will not be invoked.
61+
WRAPPER wrapper = (WRAPPER) enter[0];
62+
if (t != null) {
63+
CALLBACK_WRAPPER cb = (CALLBACK_WRAPPER) enter[1];
64+
// only for apachehttpclient_v4
65+
asyncHelper.failedBeforeRequestStarted(cb, t);
66+
67+
asyncHelper.recycle(wrapper);
68+
}
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.httpclient.common;
20+
21+
22+
import java.net.URI;
23+
import java.net.URISyntaxException;
24+
25+
public interface ApacheHttpClientApiAdapter<REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> {
26+
String getMethod(WRAPPER request);
27+
28+
URI getUri(WRAPPER request) throws URISyntaxException;
29+
30+
CharSequence getHostName(HTTPHOST httpHost);
31+
32+
int getResponseCode(RESPONSE response);
33+
34+
boolean isCircularRedirectException(Throwable t);
35+
36+
boolean isNotNullStatusLine(RESPONSE response);
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.apm.agent.httpclient.common;
20+
21+
22+
import co.elastic.apm.agent.tracer.ElasticContext;
23+
import co.elastic.apm.agent.tracer.Span;
24+
25+
public interface ApacheHttpClientAsyncHelper<AsyncProducer, AsyncProducerWrapper extends AsyncProducer, FutureCallback, FutureCallbackWrapper extends FutureCallback, HttpContext> {
26+
27+
AsyncProducerWrapper wrapRequestProducer(AsyncProducer asyncRequestProducer, Span<?> span, ElasticContext<?> toPropagate);
28+
29+
FutureCallbackWrapper wrapFutureCallback(FutureCallback futureCallback, HttpContext httpContext, Span<?> span);
30+
31+
void failedBeforeRequestStarted(FutureCallbackWrapper cb, Throwable t);
32+
33+
void recycle(AsyncProducerWrapper requestProducerWrapper);
34+
35+
}

apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
this dependency duplicates the transitive one we get from 'httpasyncclient'
2121
but keeping it explicit avoids relying on transitive dependency
2222
-->
23+
<dependency>
24+
<groupId>${project.groupId}</groupId>
25+
<artifactId>apm-apache-httpclient-common</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
2328
<dependency>
2429
<groupId>org.apache.httpcomponents</groupId>
2530
<artifactId>httpclient</artifactId>

apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient4-plugin/src/main/java/co/elastic/apm/agent/httpclient/v4/ApacheHttpAsyncClientInstrumentation.java

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,8 @@
1818
*/
1919
package co.elastic.apm.agent.httpclient.v4;
2020

21-
import co.elastic.apm.agent.httpclient.HttpClientHelper;
22-
import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpAsyncClientHelper;
23-
import co.elastic.apm.agent.httpclient.v4.helper.FutureCallbackWrapper;
24-
import co.elastic.apm.agent.httpclient.v4.helper.HttpAsyncRequestProducerWrapper;
25-
import co.elastic.apm.agent.tracer.ElasticContext;
26-
import co.elastic.apm.agent.tracer.Span;
21+
import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAsyncAdvice;
22+
import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpClient4AsyncHelper;
2723
import net.bytebuddy.asm.Advice;
2824
import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument;
2925
import net.bytebuddy.description.NamedElement;
@@ -50,7 +46,7 @@ public class ApacheHttpAsyncClientInstrumentation extends BaseApacheHttpClientIn
5046

5147
@Override
5248
public String getAdviceClassName() {
53-
return "co.elastic.apm.agent.httpclient.v4.ApacheHttpAsyncClientInstrumentation$ApacheHttpAsyncClientAdvice";
49+
return "co.elastic.apm.agent.httpclient.v4.ApacheHttpAsyncClientInstrumentation$ApacheHttpClient4AsyncAdvice";
5450
}
5551

5652
@Override
@@ -79,8 +75,8 @@ public ElementMatcher<? super MethodDescription> getMethodMatcher() {
7975
.and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback")));
8076
}
8177

82-
public static class ApacheHttpAsyncClientAdvice {
83-
private static ApacheHttpAsyncClientHelper asyncHelper = new ApacheHttpAsyncClientHelper();
78+
public static class ApacheHttpClient4AsyncAdvice extends AbstractApacheHttpClientAsyncAdvice {
79+
private static ApacheHttpClient4AsyncHelper asyncHelper = new ApacheHttpClient4AsyncHelper();
8480

8581
@Advice.AssignReturned.ToArguments({
8682
@ToArgument(index = 0, value = 0, typing = DYNAMIC),
@@ -91,43 +87,13 @@ public static class ApacheHttpAsyncClientAdvice {
9187
public static Object[] onBeforeExecute(@Advice.Argument(value = 0) HttpAsyncRequestProducer requestProducer,
9288
@Advice.Argument(2) HttpContext context,
9389
@Advice.Argument(value = 3) FutureCallback<?> futureCallback) {
94-
95-
ElasticContext<?> parentContext = tracer.currentContext();
96-
if (parentContext.isEmpty()) {
97-
// performance optimization, no need to wrap if we have nothing to propagate
98-
// empty context means also we will not create an exit span
99-
return null;
100-
}
101-
FutureCallback<?> wrappedFutureCallback = futureCallback;
102-
ElasticContext<?> activeContext = tracer.currentContext();
103-
Span<?> span = activeContext.createExitSpan();
104-
if (span != null) {
105-
span.withType(HttpClientHelper.EXTERNAL_TYPE)
106-
.withSubtype(HttpClientHelper.HTTP_SUBTYPE)
107-
.withSync(false)
108-
.activate();
109-
wrappedFutureCallback = asyncHelper.wrapFutureCallback(futureCallback, context, span);
110-
}
111-
HttpAsyncRequestProducer wrappedProducer = asyncHelper.wrapRequestProducer(requestProducer, span, tracer.currentContext());
112-
return new Object[]{wrappedProducer, wrappedFutureCallback, span};
90+
return startSpan(tracer, asyncHelper, requestProducer, context, futureCallback);
11391
}
11492

11593
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false)
11694
public static void onAfterExecute(@Advice.Enter @Nullable Object[] enter,
11795
@Advice.Thrown @Nullable Throwable t) {
118-
Span<?> span = enter != null ? (Span<?>) enter[2] : null;
119-
if (span != null) {
120-
// Deactivate in this thread
121-
span.deactivate();
122-
// End the span if the method terminated with an exception.
123-
// The exception means that the listener who normally does the ending will not be invoked.
124-
if (t != null) {
125-
HttpAsyncRequestProducerWrapper wrapper = (HttpAsyncRequestProducerWrapper) enter[0];
126-
FutureCallbackWrapper<?> cb = (FutureCallbackWrapper<?>) enter[1];
127-
cb.failedWithoutExecution(t);
128-
asyncHelper.recycle(wrapper);
129-
}
130-
}
96+
endSpan(asyncHelper, enter, t);
13197
}
13298
}
13399
}

0 commit comments

Comments
 (0)