Skip to content

Commit f3b2835

Browse files
authored
Add support for apache http client v5 body capturing (#3767)
1 parent 50981bb commit f3b2835

23 files changed

+472
-153
lines changed

apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/AbstractApacheHttpClientAdvice.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020

2121

2222
import co.elastic.apm.agent.httpclient.HttpClientHelper;
23-
import co.elastic.apm.agent.tracer.TraceState;
2423
import co.elastic.apm.agent.tracer.Outcome;
2524
import co.elastic.apm.agent.tracer.Span;
25+
import co.elastic.apm.agent.tracer.TraceState;
2626
import co.elastic.apm.agent.tracer.Tracer;
2727
import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter;
2828
import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter;
@@ -34,11 +34,11 @@ public abstract class AbstractApacheHttpClientAdvice {
3434

3535
public static <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE,
3636
HeaderAccessor extends TextHeaderSetter<REQUEST> &
37-
TextHeaderGetter<REQUEST>> Span<?> startSpan(final Tracer tracer,
38-
final ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> adapter,
39-
final WRAPPER request,
40-
@Nullable final HTTPHOST httpHost,
41-
final HeaderAccessor headerAccessor) throws URISyntaxException {
37+
TextHeaderGetter<REQUEST>, HTTPENTITY> Span<?> startSpan(final Tracer tracer,
38+
final ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE, HTTPENTITY> adapter,
39+
final WRAPPER request,
40+
@Nullable final HTTPHOST httpHost,
41+
final HeaderAccessor headerAccessor) throws URISyntaxException {
4242
TraceState<?> traceState = tracer.currentContext();
4343
Span<?> span = null;
4444
if (traceState.getSpan() != null) {
@@ -51,10 +51,11 @@ TextHeaderGetter<REQUEST>> Span<?> startSpan(final Tracer tracer,
5151
return span;
5252
}
5353

54-
public static <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> void endSpan(ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE> adapter,
55-
Object spanObj,
56-
Throwable t,
57-
RESPONSE response) {
54+
public static <REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE, HTTPENTITY>
55+
void endSpan(ApacheHttpClientApiAdapter<REQUEST, WRAPPER, HTTPHOST, RESPONSE, HTTPENTITY> adapter,
56+
Object spanObj,
57+
Throwable t,
58+
RESPONSE response) {
5859
Span<?> span = (Span<?>) spanObj;
5960
if (span == null) {
6061
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.RequestBodyRecordingInputStream;
23+
import co.elastic.apm.agent.httpclient.RequestBodyRecordingOutputStream;
24+
import co.elastic.apm.agent.sdk.logging.Logger;
25+
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
26+
import co.elastic.apm.agent.tracer.Span;
27+
28+
import java.io.InputStream;
29+
import java.io.OutputStream;
30+
31+
public abstract class AbstractApacheHttpRequestBodyCaptureAdvice {
32+
33+
private static final Logger logger = LoggerFactory.getLogger(AbstractApacheHttpRequestBodyCaptureAdvice.class);
34+
35+
public static <HTTPENTITY> InputStream maybeCaptureRequestBodyInputStream(HTTPENTITY thiz, InputStream requestBody) {
36+
Span<?> clientSpan = RequestBodyCaptureRegistry.removeSpanFor(thiz);
37+
if (clientSpan != null) {
38+
logger.debug("Wrapping input stream for request body capture for HttpEntity {} ({}) for span {}", thiz.getClass().getName(), System.identityHashCode(thiz), clientSpan);
39+
return new RequestBodyRecordingInputStream(requestBody, clientSpan);
40+
}
41+
return requestBody;
42+
}
43+
44+
public static <HTTPENTITY> OutputStream maybeCaptureRequestBodyOutputStream(HTTPENTITY thiz, OutputStream requestBody) {
45+
Span<?> clientSpan = RequestBodyCaptureRegistry.removeSpanFor(thiz);
46+
if (clientSpan != null) {
47+
logger.debug("Wrapping output stream for request body capture for HttpEntity {} ({}) for span {}", thiz.getClass().getName(), System.identityHashCode(thiz), clientSpan);
48+
return new RequestBodyRecordingOutputStream(requestBody, clientSpan);
49+
}
50+
return requestBody;
51+
}
52+
53+
public static void releaseRequestBodyOutputStream(OutputStream maybeWrapped) {
54+
if (maybeWrapped instanceof RequestBodyRecordingOutputStream) {
55+
((RequestBodyRecordingOutputStream) maybeWrapped).releaseSpan();
56+
}
57+
}
58+
}

apm-agent-plugins/apm-apache-httpclient/apm-apache-httpclient-common/src/main/java/co/elastic/apm/agent/httpclient/common/ApacheHttpClientApiAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import java.net.URI;
2424
import java.net.URISyntaxException;
2525

26-
public interface ApacheHttpClientApiAdapter<REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE> {
26+
public interface ApacheHttpClientApiAdapter<REQUEST, WRAPPER extends REQUEST, HTTPHOST, RESPONSE, HTTPENTITY> extends ApacheHttpClientEntityAccessor<REQUEST, HTTPENTITY> {
2727
String getMethod(WRAPPER request);
2828

2929
URI getUri(WRAPPER request) throws URISyntaxException;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
import javax.annotation.Nullable;
22+
23+
public interface ApacheHttpClientEntityAccessor<REQUEST, HTTPENTITY> {
24+
@Nullable
25+
HTTPENTITY getRequestEntity(REQUEST request);
26+
27+
@Nullable
28+
byte[] getSimpleBodyBytes(REQUEST request);
29+
}
Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
package co.elastic.apm.agent.httpclient.v4.helper;
19+
package co.elastic.apm.agent.httpclient.common;
2020

2121
import co.elastic.apm.agent.httpclient.HttpClientHelper;
2222
import co.elastic.apm.agent.sdk.logging.Logger;
@@ -25,10 +25,8 @@
2525
import co.elastic.apm.agent.tracer.AbstractSpan;
2626
import co.elastic.apm.agent.tracer.GlobalTracer;
2727
import co.elastic.apm.agent.tracer.Span;
28+
import co.elastic.apm.agent.tracer.dispatch.TextHeaderGetter;
2829
import co.elastic.apm.agent.tracer.reference.ReferenceCountedMap;
29-
import org.apache.http.HttpEntity;
30-
import org.apache.http.HttpEntityEnclosingRequest;
31-
import org.apache.http.HttpRequest;
3230

3331
import javax.annotation.Nullable;
3432

@@ -52,25 +50,33 @@ public static Span<?> removeSpanFor(Object entity) {
5250
}
5351

5452

55-
public static void potentiallyCaptureRequestBody(HttpRequest request, @Nullable AbstractSpan<?> span) {
56-
if (HttpClientHelper.startRequestBodyCapture(span, request, RequestHeaderAccessor.INSTANCE)) {
57-
if (request instanceof HttpEntityEnclosingRequest) {
58-
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
59-
if (entity != null) {
60-
logger.debug("Enabling request capture for entity {}() for span {}", entity.getClass().getName(), System.identityHashCode(entity), span);
61-
MapHolder.captureBodyFor(entity, (Span<?>) span);
53+
public static <REQUEST, HTTPENTITY> void potentiallyCaptureRequestBody(
54+
REQUEST request,
55+
@Nullable AbstractSpan<?> abstractSpan,
56+
ApacheHttpClientEntityAccessor<REQUEST, HTTPENTITY> adapter,
57+
TextHeaderGetter<REQUEST> headerGetter
58+
) {
59+
if (HttpClientHelper.startRequestBodyCapture(abstractSpan, request, headerGetter)) {
60+
Span<?> span = (Span<?>) abstractSpan;
61+
byte[] simpleBytes = adapter.getSimpleBodyBytes(request);
62+
if (simpleBytes != null) {
63+
logger.debug("Captured simple request body for span {}", abstractSpan);
64+
span.getContext().getHttp().getRequestBody().append(simpleBytes, 0, simpleBytes.length);
65+
} else {
66+
HTTPENTITY httpEntity = adapter.getRequestEntity(request);
67+
if (httpEntity != null) {
68+
logger.debug("Enabling request capture for entity {}() for span {}", httpEntity.getClass().getName(), System.identityHashCode(httpEntity), abstractSpan);
69+
MapHolder.captureBodyFor(httpEntity, span);
6270
} else {
63-
logger.debug("HttpEntity is null for span {}", span);
71+
logger.debug("Not capturing request body because HttpEntity is null for span {}", abstractSpan);
6472
}
65-
} else {
66-
logger.debug("Not capturing request body because {} is not an HttpEntityEnclosingRequest", request.getClass().getName());
6773
}
6874
}
6975
}
7076

7177
@Nullable
72-
public static Span<?> removeSpanFor(HttpEntity entity) {
73-
return MapHolder.removeSpanFor(entity);
78+
public static Span<?> removeSpanFor(Object httpEntity) {
79+
return MapHolder.removeSpanFor(httpEntity);
7480
}
7581

7682
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
package co.elastic.apm.agent.httpclient.v4;
2020

2121
import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpClientAdvice;
22+
import co.elastic.apm.agent.httpclient.common.RequestBodyCaptureRegistry;
2223
import co.elastic.apm.agent.httpclient.v4.helper.ApacheHttpClient4ApiAdapter;
23-
import co.elastic.apm.agent.httpclient.v4.helper.RequestBodyCaptureRegistry;
2424
import co.elastic.apm.agent.httpclient.v4.helper.RequestHeaderAccessor;
2525
import co.elastic.apm.agent.sdk.logging.Logger;
2626
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
@@ -60,7 +60,7 @@ public static class ApacheHttpClient4Advice extends AbstractApacheHttpClientAdvi
6060
public static Object onBeforeExecute(@Advice.Argument(0) HttpRoute route,
6161
@Advice.Argument(1) HttpRequestWrapper request) throws URISyntaxException {
6262
Span<?> span = startSpan(tracer, adapter, request, route.getTargetHost(), RequestHeaderAccessor.INSTANCE);
63-
RequestBodyCaptureRegistry.potentiallyCaptureRequestBody(request, tracer.getActive());
63+
RequestBodyCaptureRegistry.potentiallyCaptureRequestBody(request, tracer.getActive(), adapter, RequestHeaderAccessor.INSTANCE);
6464
return span;
6565
}
6666

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

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,49 +18,25 @@
1818
*/
1919
package co.elastic.apm.agent.httpclient.v4;
2020

21-
import co.elastic.apm.agent.httpclient.RequestBodyRecordingInputStream;
22-
import co.elastic.apm.agent.httpclient.RequestBodyRecordingOutputStream;
23-
import co.elastic.apm.agent.httpclient.v4.helper.RequestBodyCaptureRegistry;
24-
import co.elastic.apm.agent.sdk.logging.Logger;
25-
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
26-
import co.elastic.apm.agent.tracer.Span;
21+
import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpRequestBodyCaptureAdvice;
2722
import net.bytebuddy.asm.Advice;
28-
import net.bytebuddy.description.NamedElement;
2923
import net.bytebuddy.description.method.MethodDescription;
30-
import net.bytebuddy.description.type.TypeDescription;
3124
import net.bytebuddy.matcher.ElementMatcher;
3225
import org.apache.http.HttpEntity;
3326

3427
import java.io.InputStream;
35-
import java.io.OutputStream;
36-
import java.net.URISyntaxException;
3728

38-
import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass;
39-
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
40-
import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader;
41-
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
42-
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
4329
import static net.bytebuddy.matcher.ElementMatchers.named;
44-
import static net.bytebuddy.matcher.ElementMatchers.not;
45-
import static net.bytebuddy.matcher.ElementMatchers.returns;
46-
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
4730
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
4831

49-
public class ApacheHttpEntityGetContentInstrumentation extends BaseApacheHttpClientInstrumentation {
32+
public class ApacheHttpEntityGetContentInstrumentation extends BaseApacheHttpEntityInstrumentation {
5033

51-
public static class ApacheHttpEntityGetContentAdvice {
52-
53-
private static final Logger logger = LoggerFactory.getLogger(ApacheHttpEntityGetContentAdvice.class);
34+
public static class ApacheHttpEntityGetContentAdvice extends AbstractApacheHttpRequestBodyCaptureAdvice {
5435

5536
@Advice.OnMethodExit(suppress = Throwable.class, inline = false)
5637
@Advice.AssignReturned.ToReturned
57-
public static InputStream onExit(@Advice.This HttpEntity thiz, @Advice.Return InputStream content) throws URISyntaxException {
58-
Span<?> clientSpan = RequestBodyCaptureRegistry.removeSpanFor(thiz);
59-
if (clientSpan != null) {
60-
logger.debug("Wrapping input stream for request body capture for HttpEntity {} ({}) for span {}", thiz.getClass().getName(), System.identityHashCode(thiz), clientSpan);
61-
return new RequestBodyRecordingInputStream(content, clientSpan);
62-
}
63-
return content;
38+
public static InputStream onExit(@Advice.This HttpEntity thiz, @Advice.Return InputStream content) {
39+
return maybeCaptureRequestBodyInputStream(thiz, content);
6440
}
6541
}
6642

@@ -69,22 +45,6 @@ public String getAdviceClassName() {
6945
return "co.elastic.apm.agent.httpclient.v4.ApacheHttpEntityGetContentInstrumentation$ApacheHttpEntityGetContentAdvice";
7046
}
7147

72-
@Override
73-
public ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
74-
return not(isBootstrapClassLoader())
75-
.and(classLoaderCanLoadClass("org.apache.http.HttpEntity"));
76-
}
77-
78-
@Override
79-
public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() {
80-
return nameStartsWith("org.apache.http").and(nameContains("Entity"));
81-
}
82-
83-
@Override
84-
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
85-
return hasSuperType(named("org.apache.http.HttpEntity"));
86-
}
87-
8848
@Override
8949
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
9050
return named("getContent")

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

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

21-
import co.elastic.apm.agent.httpclient.RequestBodyRecordingOutputStream;
22-
import co.elastic.apm.agent.httpclient.v4.helper.RequestBodyCaptureRegistry;
23-
import co.elastic.apm.agent.sdk.logging.Logger;
24-
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
25-
import co.elastic.apm.agent.tracer.Span;
21+
import co.elastic.apm.agent.httpclient.common.AbstractApacheHttpRequestBodyCaptureAdvice;
2622
import net.bytebuddy.asm.Advice;
27-
import net.bytebuddy.description.NamedElement;
2823
import net.bytebuddy.description.method.MethodDescription;
29-
import net.bytebuddy.description.type.TypeDescription;
3024
import net.bytebuddy.matcher.ElementMatcher;
3125
import org.apache.http.HttpEntity;
3226

3327
import java.io.OutputStream;
34-
import java.net.URISyntaxException;
3528

36-
import static co.elastic.apm.agent.sdk.bytebuddy.CustomElementMatchers.classLoaderCanLoadClass;
37-
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
38-
import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader;
39-
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
40-
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
4129
import static net.bytebuddy.matcher.ElementMatchers.named;
42-
import static net.bytebuddy.matcher.ElementMatchers.not;
4330
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
4431
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
4532

46-
public class ApacheHttpEntityWriteToInstrumentation extends BaseApacheHttpClientInstrumentation {
33+
public class ApacheHttpEntityWriteToInstrumentation extends BaseApacheHttpEntityInstrumentation {
4734

48-
public static class ApacheHttpEntityWriteToAdvice {
49-
50-
private static final Logger logger = LoggerFactory.getLogger(ApacheHttpEntityWriteToAdvice.class);
35+
public static class ApacheHttpEntityWriteToAdvice extends AbstractApacheHttpRequestBodyCaptureAdvice {
5136

5237
@Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
5338
@Advice.AssignReturned.ToArguments(@Advice.AssignReturned.ToArguments.ToArgument(0))
54-
public static OutputStream onEnter(@Advice.This HttpEntity thiz, @Advice.Argument(0) OutputStream drain) throws URISyntaxException {
55-
Span<?> clientSpan = RequestBodyCaptureRegistry.removeSpanFor(thiz);
56-
if (clientSpan != null) {
57-
logger.debug("Wrapping output stream for request body capture for HttpEntity {} ({}) for span {}", thiz.getClass().getName(), System.identityHashCode(thiz), clientSpan);
58-
return new RequestBodyRecordingOutputStream(drain, clientSpan);
59-
}
60-
return drain;
39+
public static OutputStream onEnter(@Advice.This HttpEntity thiz, @Advice.Argument(0) OutputStream drain) {
40+
return maybeCaptureRequestBodyOutputStream(thiz, drain);
6141
}
6242

6343
@Advice.OnMethodExit(suppress = Throwable.class, inline = false)
64-
public static void onExit(@Advice.Enter OutputStream potentiallyWrappedStream) throws URISyntaxException {
65-
if (potentiallyWrappedStream instanceof RequestBodyRecordingOutputStream) {
66-
((RequestBodyRecordingOutputStream) potentiallyWrappedStream).releaseSpan();
67-
}
44+
public static void onExit(@Advice.Enter OutputStream potentiallyWrappedStream) {
45+
releaseRequestBodyOutputStream(potentiallyWrappedStream);
6846
}
6947
}
7048

@@ -73,22 +51,6 @@ public String getAdviceClassName() {
7351
return "co.elastic.apm.agent.httpclient.v4.ApacheHttpEntityWriteToInstrumentation$ApacheHttpEntityWriteToAdvice";
7452
}
7553

76-
@Override
77-
public ElementMatcher.Junction<ClassLoader> getClassLoaderMatcher() {
78-
return not(isBootstrapClassLoader())
79-
.and(classLoaderCanLoadClass("org.apache.http.HttpEntity"));
80-
}
81-
82-
@Override
83-
public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() {
84-
return nameStartsWith("org.apache.http").and(nameContains("Entity"));
85-
}
86-
87-
@Override
88-
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
89-
return hasSuperType(named("org.apache.http.HttpEntity"));
90-
}
91-
9254
@Override
9355
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
9456
return named("writeTo")

0 commit comments

Comments
 (0)