Skip to content

Commit 935c770

Browse files
authored
Add JAX-RS 2 client body capture (#159)
* Add JAX-RS 2 client body capture Signed-off-by: Pavol Loffay <[email protected]> * Add to readme Signed-off-by: Pavol Loffay <[email protected]> * typo Signed-off-by: Pavol Loffay <[email protected]> * Form Signed-off-by: Pavol Loffay <[email protected]> * Update readme Signed-off-by: Pavol Loffay <[email protected]> * Fix Signed-off-by: Pavol Loffay <[email protected]> * unify across clients Signed-off-by: Pavol Loffay <[email protected]> * Use the right content Signed-off-by: Pavol Loffay <[email protected]> * use static and remove junk Signed-off-by: Pavol Loffay <[email protected]>
1 parent f4fa189 commit 935c770

File tree

13 files changed

+548
-8
lines changed

13 files changed

+548
-8
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ and adds following capabilities:
1414
List of supported frameworks with additional capabilities:
1515
| Library/Framework | Versions |
1616
|--------------------------------------------------------------------------------------------------------|-----------------|
17+
| [Apache HttpClient](https://hc.apache.org/index.html) | 4.0+ |
18+
| [gRPC](https://github.com/grpc/grpc-java) | 1.5+ to [1.32.0](https://github.com/hypertrace/javaagent/issues/70) |
19+
| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 2.0+ |
20+
| [OkHttp](https://github.com/square/okhttp/) | 3.0+ |
1721
| [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.3+ |
1822
| [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ |
19-
| [gRPC](https://github.com/grpc/grpc-java) | 1.5+ |
20-
| [OkHttp](https://github.com/square/okhttp/) | 3.0+ |
2123

2224
### Adding custom filter implementation
2325

@@ -57,7 +59,7 @@ The following instrumentation names disable only Hypertrace instrumentations, no
5759
* `ht` - all Hypertrace instrumentations
5860
* `servlet-ht` - Servlet, Spark Web
5961
* `okhttp-ht` - Okhttp
60-
* `grpc-ht` - Okhttp
62+
* `grpc-ht` - gRPC
6163

6264
The Hypertrace instrumentations use also the core OpenTelemetry instrumentation names so for example
6365
`-Dotel.instrumentation.servlet.enabled=false` disables all servlet instrumentations including core

instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/InputStreamUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.nio.charset.Charset;
2929
import org.hypertrace.agent.core.GlobalObjectRegistry;
3030
import org.hypertrace.agent.core.GlobalObjectRegistry.SpanAndBuffer;
31+
import org.hypertrace.agent.core.HypertraceSemanticAttributes;
3132
import org.slf4j.Logger;
3233
import org.slf4j.LoggerFactory;
3334

@@ -50,7 +51,7 @@ public static void addAttribute(Span span, AttributeKey<String> attributeKey, St
5051
span.setAttribute(attributeKey, value);
5152
} else {
5253
TRACER
53-
.spanBuilder("additional-data")
54+
.spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME)
5455
.setParent(Context.root().with(span))
5556
.setAttribute(attributeKey, value)
5657
.startSpan()

instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public void getJson() throws IOException, TimeoutException, InterruptedException
105105
@Test
106106
public void postUrlEncoded() throws IOException, TimeoutException, InterruptedException {
107107
List<NameValuePair> nvps = new ArrayList<>();
108-
nvps.add(new BasicNameValuePair("code", "22"));
108+
nvps.add(new BasicNameValuePair("key1", "value1"));
109+
nvps.add(new BasicNameValuePair("key2", "value2"));
109110

110111
HttpPost postRequest = new HttpPost();
111112
postRequest.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
@@ -126,7 +127,8 @@ public void postUrlEncoded() throws IOException, TimeoutException, InterruptedEx
126127
.getAttributes()
127128
.get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header")));
128129
Assertions.assertEquals(
129-
"code=22", clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
130+
"key1=value1&key2=value2",
131+
clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
130132
Assertions.assertNull(
131133
clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
132134
}

instrumentation/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies{
3737
implementation(project(":instrumentation:grpc-1.5"))
3838
implementation(project(":instrumentation:okhttp:okhttp-3.0"))
3939
implementation(project(":instrumentation:apache-httpclient-4.0"))
40+
implementation(project(":instrumentation:jaxrs-client-2.0"))
4041
implementation(project(":otel-extensions"))
4142
}
4243

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
pass {
10+
group = "javax.ws.rs"
11+
module = "javax.ws.rs-api"
12+
versions = "[2.0,)"
13+
}
14+
pass {
15+
// We want to support the dropwizard clients too.
16+
group = "io.dropwizard"
17+
module = "dropwizard-client"
18+
versions = "[0.8.0,)"
19+
assertInverse = true
20+
}
21+
}
22+
23+
afterEvaluate{
24+
io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project,
25+
sourceSets.main.get(),
26+
"io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin",
27+
project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath
28+
).configure()
29+
}
30+
31+
val versions: Map<String, String> by extra
32+
33+
dependencies {
34+
api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-jaxrs-client-2.0-common:${versions["opentelemetry_java_agent"]}")
35+
36+
compileOnly("javax.ws.rs:javax.ws.rs-api:2.0.1")
37+
38+
testImplementation(project(":testing-common"))
39+
testImplementation("org.glassfish.jersey.core:jersey-client:2.27")
40+
testImplementation("org.glassfish.jersey.inject:jersey-hk2:2.27")
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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.jaxrs.v2_0;
18+
19+
import io.opentelemetry.api.common.AttributeKey;
20+
import io.opentelemetry.api.trace.Span;
21+
import io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.ClientTracingFilter;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.function.Function;
25+
import javax.ws.rs.client.ClientRequestContext;
26+
import javax.ws.rs.client.ClientRequestFilter;
27+
import javax.ws.rs.client.ClientResponseContext;
28+
import javax.ws.rs.client.ClientResponseFilter;
29+
import javax.ws.rs.core.Form;
30+
import javax.ws.rs.core.MediaType;
31+
import javax.ws.rs.core.MultivaluedMap;
32+
import org.hypertrace.agent.config.Config.AgentConfig;
33+
import org.hypertrace.agent.core.ContentTypeUtils;
34+
import org.hypertrace.agent.core.HypertraceConfig;
35+
import org.hypertrace.agent.core.HypertraceSemanticAttributes;
36+
import org.slf4j.Logger;
37+
import org.slf4j.LoggerFactory;
38+
39+
public class JaxrsClientBodyCaptureFilter implements ClientRequestFilter, ClientResponseFilter {
40+
41+
private static final Logger log = LoggerFactory.getLogger(JaxrsClientBodyCaptureFilter.class);
42+
43+
@Override
44+
public void filter(ClientRequestContext requestContext) {
45+
Object spanObj = requestContext.getProperty(ClientTracingFilter.SPAN_PROPERTY_NAME);
46+
if (!(spanObj instanceof Span)) {
47+
return;
48+
}
49+
50+
Span currentSpan = (Span) spanObj;
51+
AgentConfig agentConfig = HypertraceConfig.get();
52+
53+
try {
54+
if (agentConfig.getDataCapture().getHttpHeaders().getRequest().getValue()) {
55+
captureHeaders(
56+
currentSpan,
57+
HypertraceSemanticAttributes::httpRequestHeader,
58+
requestContext.getStringHeaders());
59+
}
60+
if (requestContext.hasEntity()
61+
&& agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) {
62+
MediaType mediaType = requestContext.getMediaType();
63+
if (mediaType == null || !ContentTypeUtils.shouldCapture(mediaType.toString())) {
64+
return;
65+
}
66+
67+
Object entity = requestContext.getEntity();
68+
if (entity != null) {
69+
if (entity instanceof Form) {
70+
Form form = (Form) entity;
71+
String content = getUrlEncodedContent(form);
72+
currentSpan.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, content);
73+
} else {
74+
currentSpan.setAttribute(
75+
HypertraceSemanticAttributes.HTTP_REQUEST_BODY, entity.toString());
76+
}
77+
}
78+
}
79+
requestContext.getEntity();
80+
} catch (Exception ex) {
81+
log.error("Exception while getting request entity or headers", ex);
82+
}
83+
}
84+
85+
@Override
86+
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) {
87+
Object spanObj = requestContext.getProperty(ClientTracingFilter.SPAN_PROPERTY_NAME);
88+
if (!(spanObj instanceof Span)) {
89+
return;
90+
}
91+
92+
Span currentSpan = (Span) spanObj;
93+
AgentConfig agentConfig = HypertraceConfig.get();
94+
95+
try {
96+
if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) {
97+
captureHeaders(
98+
currentSpan,
99+
HypertraceSemanticAttributes::httpResponseHeader,
100+
responseContext.getHeaders());
101+
}
102+
} catch (Exception ex) {
103+
log.error("Exception while getting response entity or headers", ex);
104+
}
105+
}
106+
107+
private static String getUrlEncodedContent(Form form) {
108+
MultivaluedMap<String, String> formMap = form.asMap();
109+
StringBuilder sb = new StringBuilder();
110+
if (formMap != null) {
111+
for (Map.Entry<String, List<String>> entry : formMap.entrySet()) {
112+
if (sb.length() > 0) {
113+
sb.append("&");
114+
}
115+
for (String value : entry.getValue()) {
116+
sb.append(entry.getKey());
117+
sb.append("=");
118+
sb.append(value);
119+
}
120+
}
121+
}
122+
return sb.toString();
123+
}
124+
125+
private static void captureHeaders(
126+
Span span,
127+
Function<String, AttributeKey<String>> keySupplier,
128+
MultivaluedMap<String, String> headers) {
129+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
130+
for (Object value : entry.getValue()) {
131+
span.setAttribute(keySupplier.apply(entry.getKey()), value.toString());
132+
}
133+
}
134+
}
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.jaxrs.v2_0;
18+
19+
import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
20+
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass;
21+
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.hasInterface;
22+
import static java.util.Collections.singletonMap;
23+
import static net.bytebuddy.matcher.ElementMatchers.named;
24+
import static net.bytebuddy.matcher.ElementMatchers.returns;
25+
26+
import com.google.auto.service.AutoService;
27+
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
28+
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Map;
32+
import javax.ws.rs.client.Client;
33+
import net.bytebuddy.asm.Advice;
34+
import net.bytebuddy.description.method.MethodDescription;
35+
import net.bytebuddy.description.type.TypeDescription;
36+
import net.bytebuddy.implementation.bytecode.assign.Assigner;
37+
import net.bytebuddy.matcher.ElementMatcher;
38+
39+
@AutoService(InstrumentationModule.class)
40+
public class JaxrsClientBodyInstrumentationModule extends InstrumentationModule {
41+
42+
@Override
43+
public int getOrder() {
44+
return 1;
45+
}
46+
47+
public JaxrsClientBodyInstrumentationModule() {
48+
super(JaxrsClientBodyInstrumentationName.PRIMARY, JaxrsClientBodyInstrumentationName.OTHER);
49+
}
50+
51+
@Override
52+
public List<TypeInstrumentation> typeInstrumentations() {
53+
return Collections.singletonList(new JaxrsClientBuilderInstrumentation());
54+
}
55+
56+
class JaxrsClientBuilderInstrumentation implements TypeInstrumentation {
57+
58+
@Override
59+
public ElementMatcher<ClassLoader> classLoaderOptimization() {
60+
return hasClassesNamed("javax.ws.rs.client.ClientBuilder");
61+
}
62+
63+
@Override
64+
public ElementMatcher<TypeDescription> typeMatcher() {
65+
return extendsClass(named("javax.ws.rs.client.ClientBuilder"));
66+
}
67+
68+
@Override
69+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
70+
return singletonMap(
71+
named("build").and(returns(hasInterface(named("javax.ws.rs.client.Client")))),
72+
ClientBuilder_build_Advice.class.getName());
73+
}
74+
}
75+
76+
static class ClientBuilder_build_Advice {
77+
@Advice.OnMethodExit
78+
public static void registerFeature(
79+
@Advice.Return(typing = Assigner.Typing.DYNAMIC) Client client) {
80+
// Register on the generated client instead of the builder
81+
// The build() can be called multiple times and is not thread safe
82+
// A client is only created once
83+
// Use lowest priority to run after OTEL filter that controls lifecycle of span
84+
client.register(JaxrsClientBodyCaptureFilter.class, Integer.MIN_VALUE);
85+
client.register(JaxrsClientEntityInterceptor.class);
86+
}
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.jaxrs.v2_0;
18+
19+
public class JaxrsClientBodyInstrumentationName {
20+
public static final String PRIMARY = "jaxrs-client";
21+
public static final String[] OTHER = {
22+
"jaxrs-client-2.0", "ht", "jaxrs-client-ht", "jaxrs-client-2.0-ht"
23+
};
24+
}

0 commit comments

Comments
 (0)