Skip to content

Commit 3883f27

Browse files
authored
Apache httpclient wrapping free body instrumentation (#140)
* Apache client wrapper free instrumentation Signed-off-by: Pavol Loffay <[email protected]> * Fix method called once Signed-off-by: Pavol Loffay <[email protected]> * Working but context is broken for AWS Signed-off-by: Pavol Loffay <[email protected]> * Working Signed-off-by: Pavol Loffay <[email protected]> * latest changes Signed-off-by: Pavol Loffay <[email protected]> * Fix muzzle Signed-off-by: Pavol Loffay <[email protected]> * Use root context Signed-off-by: Pavol Loffay <[email protected]> * disble readAll by default Signed-off-by: Pavol Loffay <[email protected]> * renaming Signed-off-by: Pavol Loffay <[email protected]> * Remove objects from map when done Signed-off-by: Pavol Loffay <[email protected]> * Small refactor Signed-off-by: Pavol Loffay <[email protected]> * Use concurrent weak map Signed-off-by: Pavol Loffay <[email protected]> * more progress now instrument output stream Signed-off-by: Pavol Loffay <[email protected]> * Working request body Signed-off-by: Pavol Loffay <[email protected]> * Add more tests Signed-off-by: Pavol Loffay <[email protected]> * simplify tests Signed-off-by: Pavol Loffay <[email protected]> * simplify Signed-off-by: Pavol Loffay <[email protected]> * cleanup Signed-off-by: Pavol Loffay <[email protected]> * Fixed Signed-off-by: Pavol Loffay <[email protected]> * Create string directly from byteArray buffer Signed-off-by: Pavol Loffay <[email protected]> * Shade concurrent map Signed-off-by: Pavol Loffay <[email protected]> * Rename package Signed-off-by: Pavol Loffay <[email protected]> * Fixes related to package definitions Signed-off-by: Pavol Loffay <[email protected]> * Fix the verification error Signed-off-by: Pavol Loffay <[email protected]> * Add data capture config Signed-off-by: Pavol Loffay <[email protected]> * Fix shading Signed-off-by: Pavol Loffay <[email protected]> * Rename Signed-off-by: Pavol Loffay <[email protected]> * Add readAllBytes Signed-off-by: Pavol Loffay <[email protected]> * Add readNBytes Signed-off-by: Pavol Loffay <[email protected]> * Add link Signed-off-by: Pavol Loffay <[email protected]> * Fix after OTEL 0.11.0 Signed-off-by: Pavol Loffay <[email protected]> * Fix Signed-off-by: Pavol Loffay <[email protected]>
1 parent 0d716d2 commit 3883f27

File tree

20 files changed

+1987
-0
lines changed

20 files changed

+1987
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
plugins {
2+
`java-library`
3+
id("net.bytebuddy.byte-buddy")
4+
id("io.opentelemetry.instrumentation.auto-instrumentation")
5+
muzzle
6+
}
7+
8+
muzzle {
9+
// TODO this check fails, but it passes in OTEL https://github.com/hypertrace/javaagent/issues/144
10+
// fail {
11+
// group = "commons-httpclient"
12+
// module = "commons-httpclient"
13+
// versions = "[,4.0)"
14+
// skipVersions.add("3.1-jenkins-1")
15+
// }
16+
pass {
17+
group = "org.apache.httpcomponents"
18+
module = "httpclient"
19+
versions = "[4.0,)"
20+
assertInverse = true
21+
}
22+
pass {
23+
// We want to support the dropwizard clients too.
24+
group = "io.dropwizard"
25+
module = "dropwizard-client"
26+
versions = "(,)"
27+
assertInverse = true
28+
}
29+
}
30+
31+
afterEvaluate{
32+
io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project,
33+
sourceSets.main.get(),
34+
"io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin",
35+
project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath
36+
).configure()
37+
}
38+
39+
dependencies {
40+
implementation("org.apache.httpcomponents:httpclient:4.0")
41+
api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-apache-httpclient-4.0:0.11.0")
42+
43+
testImplementation(project(":testing-common"))
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
* Copyright The Hypertrace Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.instrumentation.hypertrace.apachehttpclient.v4_0;
18+
19+
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
20+
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
21+
import static net.bytebuddy.matcher.ElementMatchers.is;
22+
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
23+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
24+
import static net.bytebuddy.matcher.ElementMatchers.named;
25+
import static net.bytebuddy.matcher.ElementMatchers.not;
26+
import static net.bytebuddy.matcher.ElementMatchers.returns;
27+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
28+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
29+
30+
import com.google.auto.service.AutoService;
31+
import io.opentelemetry.api.trace.Span;
32+
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
33+
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
34+
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
35+
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
36+
import java.io.ByteArrayOutputStream;
37+
import java.io.InputStream;
38+
import java.io.OutputStream;
39+
import java.io.UnsupportedEncodingException;
40+
import java.nio.charset.Charset;
41+
import java.util.Arrays;
42+
import java.util.HashMap;
43+
import java.util.List;
44+
import java.util.Map;
45+
import net.bytebuddy.asm.Advice;
46+
import net.bytebuddy.description.method.MethodDescription;
47+
import net.bytebuddy.description.type.TypeDescription;
48+
import net.bytebuddy.matcher.ElementMatcher;
49+
import org.apache.http.Header;
50+
import org.apache.http.HttpEntity;
51+
import org.apache.http.HttpMessage;
52+
import org.apache.http.HttpResponse;
53+
import org.hypertrace.agent.config.Config.AgentConfig;
54+
import org.hypertrace.agent.core.ContentEncodingUtils;
55+
import org.hypertrace.agent.core.ContentLengthUtils;
56+
import org.hypertrace.agent.core.ContentTypeUtils;
57+
import org.hypertrace.agent.core.GlobalObjectRegistry;
58+
import org.hypertrace.agent.core.GlobalObjectRegistry.SpanAndBuffer;
59+
import org.hypertrace.agent.core.HypertraceConfig;
60+
import org.hypertrace.agent.core.HypertraceSemanticAttributes;
61+
62+
@AutoService(InstrumentationModule.class)
63+
public class ApacheClientInstrumentationModule extends InstrumentationModule {
64+
65+
public ApacheClientInstrumentationModule() {
66+
super(ApacheHttpClientInstrumentationName.PRIMARY, ApacheHttpClientInstrumentationName.OTHER);
67+
}
68+
69+
@Override
70+
public int getOrder() {
71+
return 1;
72+
}
73+
74+
@Override
75+
public List<TypeInstrumentation> typeInstrumentations() {
76+
return Arrays.asList(new HttpEntityInstrumentation(), new ApacheClientInstrumentation());
77+
}
78+
79+
static class ApacheClientInstrumentation implements TypeInstrumentation {
80+
81+
@Override
82+
public ElementMatcher<TypeDescription> typeMatcher() {
83+
return implementsInterface(named("org.apache.http.client.HttpClient"));
84+
}
85+
86+
@Override
87+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
88+
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
89+
90+
// instrument response
91+
transformers.put(
92+
isMethod().and(named("execute")).and(not(isAbstract())),
93+
HttpClient_ExecuteAdvice_response.class.getName());
94+
95+
// instrument request
96+
transformers.put(
97+
isMethod()
98+
.and(named("execute"))
99+
.and(not(isAbstract()))
100+
.and(takesArgument(0, hasSuperType(named("org.apache.http.HttpMessage")))),
101+
HttpClient_ExecuteAdvice_request0.class.getName());
102+
transformers.put(
103+
isMethod()
104+
.and(named("execute"))
105+
.and(not(isAbstract()))
106+
.and(takesArgument(1, hasSuperType(named("org.apache.http.HttpMessage")))),
107+
HttpClient_ExecuteAdvice_request1.class.getName());
108+
109+
return transformers;
110+
}
111+
}
112+
113+
static class HttpClient_ExecuteAdvice_request0 {
114+
@Advice.OnMethodEnter(suppress = Throwable.class)
115+
public static boolean enter(@Advice.Argument(0) HttpMessage request) {
116+
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpMessage.class);
117+
if (callDepth > 0) {
118+
return false;
119+
}
120+
ApacheHttpClientUtils.traceRequest(request);
121+
return true;
122+
}
123+
124+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
125+
public static void exit(
126+
@Advice.Enter boolean returnFromEnter, @Advice.Thrown Throwable throwable) {
127+
if (returnFromEnter) {
128+
CallDepthThreadLocalMap.reset(HttpMessage.class);
129+
}
130+
}
131+
}
132+
133+
static class HttpClient_ExecuteAdvice_request1 {
134+
@Advice.OnMethodEnter(suppress = Throwable.class)
135+
public static boolean enter(@Advice.Argument(1) HttpMessage request) {
136+
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpMessage.class);
137+
if (callDepth > 0) {
138+
return false;
139+
}
140+
ApacheHttpClientUtils.traceRequest(request);
141+
return true;
142+
}
143+
144+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
145+
public static void exit(
146+
@Advice.Enter boolean returnFromEnter, @Advice.Thrown Throwable throwable) {
147+
if (returnFromEnter) {
148+
CallDepthThreadLocalMap.reset(HttpMessage.class);
149+
}
150+
}
151+
}
152+
153+
static class HttpClient_ExecuteAdvice_response {
154+
@Advice.OnMethodEnter(suppress = Throwable.class)
155+
public static boolean enter() {
156+
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpResponse.class);
157+
if (callDepth > 0) {
158+
return false;
159+
}
160+
return true;
161+
}
162+
163+
@Advice.OnMethodExit(suppress = Throwable.class)
164+
public static void exit(@Advice.Return Object response, @Advice.Enter boolean returnFromEnter) {
165+
if (!returnFromEnter) {
166+
return;
167+
}
168+
169+
CallDepthThreadLocalMap.reset(HttpResponse.class);
170+
if (response instanceof HttpResponse) {
171+
HttpResponse httpResponse = (HttpResponse) response;
172+
Span currentSpan = Java8BytecodeBridge.currentSpan();
173+
AgentConfig agentConfig = HypertraceConfig.get();
174+
if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) {
175+
ApacheHttpClientUtils.addResponseHeaders(currentSpan, httpResponse.headerIterator());
176+
}
177+
178+
if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) {
179+
HttpEntity entity = httpResponse.getEntity();
180+
ApacheHttpClientUtils.traceEntity(
181+
currentSpan, HypertraceSemanticAttributes.HTTP_RESPONSE_BODY.getKey(), entity);
182+
}
183+
}
184+
}
185+
}
186+
187+
static class HttpEntityInstrumentation implements TypeInstrumentation {
188+
@Override
189+
public ElementMatcher<? super TypeDescription> typeMatcher() {
190+
return implementsInterface(named("org.apache.http.HttpEntity"));
191+
}
192+
193+
@Override
194+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
195+
Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>();
196+
197+
// instrumentation for request body along with OutputStream instrumentation
198+
transformers.put(
199+
named("writeTo").and(takesArguments(1)).and(takesArgument(0, is(OutputStream.class))),
200+
HttpEntity_WriteToAdvice.class.getName());
201+
202+
// instrumentation for response body along with InputStream instrumentation
203+
transformers.put(
204+
named("getContent").and(takesArguments(0)).and(returns(InputStream.class)),
205+
HttpEntity_GetContentAdvice.class.getName());
206+
return transformers;
207+
}
208+
}
209+
210+
static class HttpEntity_GetContentAdvice {
211+
212+
@Advice.OnMethodExit(suppress = Throwable.class)
213+
public static void exit(@Advice.This HttpEntity thizz, @Advice.Return InputStream inputStream) {
214+
// here the Span.current() is finished for response entities
215+
Span clientSpan = ApacheHttpClientObjectRegistry.httpEntityToSpanMap.remove(thizz);
216+
// HttpEntity might be wrapped multiple times
217+
// this ensures that the advice runs only for the most outer one
218+
// the returned inputStream is put into globally accessible map
219+
// The InputStream instrumentation then checks if the input stream is in the map and only
220+
// then intercepts the reads.
221+
if (clientSpan == null) {
222+
return;
223+
}
224+
225+
Header contentType = thizz.getContentType();
226+
if (contentType == null || !ContentTypeUtils.shouldCapture(contentType.getValue())) {
227+
return;
228+
}
229+
230+
long contentSize = thizz.getContentLength();
231+
if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
232+
contentSize = ContentLengthUtils.DEFAULT;
233+
}
234+
235+
String encoding =
236+
thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
237+
Charset charset = ContentEncodingUtils.toCharset(encoding);
238+
SpanAndBuffer spanAndBuffer =
239+
new SpanAndBuffer(
240+
clientSpan,
241+
new ByteArrayOutputStream((int) contentSize),
242+
HypertraceSemanticAttributes.HTTP_RESPONSE_BODY,
243+
charset);
244+
GlobalObjectRegistry.inputStreamToSpanAndBufferMap.put(inputStream, spanAndBuffer);
245+
}
246+
}
247+
248+
static class HttpEntity_WriteToAdvice {
249+
@Advice.OnMethodEnter(suppress = Throwable.class)
250+
public static void enter(
251+
@Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
252+
if (!ApacheHttpClientObjectRegistry.httpEntityToSpanMap.containsKey(thizz)) {
253+
return;
254+
}
255+
256+
long contentSize = thizz.getContentLength();
257+
if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
258+
contentSize = ContentLengthUtils.DEFAULT;
259+
}
260+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream((int) contentSize);
261+
262+
GlobalObjectRegistry.outputStreamToBufferMap.put(outputStream, byteArrayOutputStream);
263+
}
264+
265+
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
266+
public static void exit(
267+
@Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
268+
Span clientSpan = ApacheHttpClientObjectRegistry.httpEntityToSpanMap.remove(thizz);
269+
if (clientSpan == null) {
270+
return;
271+
}
272+
273+
String encoding =
274+
thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
275+
Charset charset = ContentEncodingUtils.toCharset(encoding);
276+
277+
ByteArrayOutputStream bufferedOutStream =
278+
GlobalObjectRegistry.outputStreamToBufferMap.remove(outputStream);
279+
try {
280+
String requestBody = bufferedOutStream.toString(charset.name());
281+
System.out.printf("Captured request body via outputstream: %s\n", requestBody);
282+
clientSpan.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, requestBody);
283+
} catch (UnsupportedEncodingException e) {
284+
// should not happen, the charset has been parsed before
285+
}
286+
}
287+
}
288+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright The Hypertrace Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.instrumentation.hypertrace.apachehttpclient.v4_0;
18+
19+
public class ApacheHttpClientInstrumentationName {
20+
public static final String PRIMARY = "httpclient";
21+
public static final String[] OTHER = {
22+
"apache-httpclient",
23+
"apache-http-client",
24+
"ht",
25+
"httpclient-ht",
26+
"apache-httpclient-ht",
27+
"apache-http-client-ht"
28+
};
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright The Hypertrace Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.opentelemetry.instrumentation.hypertrace.apachehttpclient.v4_0;
18+
19+
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
20+
import io.opentelemetry.api.trace.Span;
21+
import org.apache.http.HttpEntity;
22+
23+
public class ApacheHttpClientObjectRegistry {
24+
25+
private ApacheHttpClientObjectRegistry() {}
26+
27+
public static final WeakConcurrentMap<HttpEntity, Span> httpEntityToSpanMap =
28+
new WeakConcurrentMap<>(false);
29+
}

0 commit comments

Comments
 (0)