diff --git a/README.md b/README.md index ca172835b..ffc07cd0e 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ and adds following capabilities: List of supported frameworks with additional capabilities: | Library/Framework | Versions | |--------------------------------------------------------------------------------------------------------|-----------------| +| [Apache HttpClient](https://hc.apache.org/index.html) | 4.0+ | +| [gRPC](https://github.com/grpc/grpc-java) | 1.5+ to [1.32.0](https://github.com/hypertrace/javaagent/issues/70) | +| [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 2.0+ | +| [OkHttp](https://github.com/square/okhttp/) | 3.0+ | | [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.3+ | | [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | -| [gRPC](https://github.com/grpc/grpc-java) | 1.5+ | -| [OkHttp](https://github.com/square/okhttp/) | 3.0+ | ### Adding custom filter implementation @@ -57,7 +59,7 @@ The following instrumentation names disable only Hypertrace instrumentations, no * `ht` - all Hypertrace instrumentations * `servlet-ht` - Servlet, Spark Web * `okhttp-ht` - Okhttp -* `grpc-ht` - Okhttp +* `grpc-ht` - gRPC The Hypertrace instrumentations use also the core OpenTelemetry instrumentation names so for example `-Dotel.instrumentation.servlet.enabled=false` disables all servlet instrumentations including core diff --git a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/InputStreamUtils.java b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/InputStreamUtils.java index 9a8bcb133..df08103f6 100644 --- a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/InputStreamUtils.java +++ b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/InputStreamUtils.java @@ -28,6 +28,7 @@ import java.nio.charset.Charset; import org.hypertrace.agent.core.GlobalObjectRegistry; import org.hypertrace.agent.core.GlobalObjectRegistry.SpanAndBuffer; +import org.hypertrace.agent.core.HypertraceSemanticAttributes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,7 +51,7 @@ public static void addAttribute(Span span, AttributeKey attributeKey, St span.setAttribute(attributeKey, value); } else { TRACER - .spanBuilder("additional-data") + .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) .setParent(Context.root().with(span)) .setAttribute(attributeKey, value) .startSpan() diff --git a/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java b/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java index 9eb8b2593..939e4bddd 100644 --- a/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java +++ b/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java @@ -105,7 +105,8 @@ public void getJson() throws IOException, TimeoutException, InterruptedException @Test public void postUrlEncoded() throws IOException, TimeoutException, InterruptedException { List nvps = new ArrayList<>(); - nvps.add(new BasicNameValuePair("code", "22")); + nvps.add(new BasicNameValuePair("key1", "value1")); + nvps.add(new BasicNameValuePair("key2", "value2")); HttpPost postRequest = new HttpPost(); postRequest.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); @@ -126,7 +127,8 @@ public void postUrlEncoded() throws IOException, TimeoutException, InterruptedEx .getAttributes() .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header"))); Assertions.assertEquals( - "code=22", clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + "key1=value1&key2=value2", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); Assertions.assertNull( clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); } diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts index edf25b150..15ac6067c 100644 --- a/instrumentation/build.gradle.kts +++ b/instrumentation/build.gradle.kts @@ -37,6 +37,7 @@ dependencies{ implementation(project(":instrumentation:grpc-1.5")) implementation(project(":instrumentation:okhttp:okhttp-3.0")) implementation(project(":instrumentation:apache-httpclient-4.0")) + implementation(project(":instrumentation:jaxrs-client-2.0")) implementation(project(":otel-extensions")) } diff --git a/instrumentation/jaxrs-client-2.0/build.gradle.kts b/instrumentation/jaxrs-client-2.0/build.gradle.kts new file mode 100644 index 000000000..905a84af0 --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + `java-library` + id("net.bytebuddy.byte-buddy") + id("io.opentelemetry.instrumentation.auto-instrumentation") + muzzle +} + +muzzle { + pass { + group = "javax.ws.rs" + module = "javax.ws.rs-api" + versions = "[2.0,)" + } + pass { + // We want to support the dropwizard clients too. + group = "io.dropwizard" + module = "dropwizard-client" + versions = "[0.8.0,)" + assertInverse = true + } +} + +afterEvaluate{ + io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project, + sourceSets.main.get(), + "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin", + project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath + ).configure() +} + +val versions: Map by extra + +dependencies { + api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-jaxrs-client-2.0-common:${versions["opentelemetry_java_agent"]}") + + compileOnly("javax.ws.rs:javax.ws.rs-api:2.0.1") + + testImplementation(project(":testing-common")) + testImplementation("org.glassfish.jersey.core:jersey-client:2.27") + testImplementation("org.glassfish.jersey.inject:jersey-hk2:2.27") +} diff --git a/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyCaptureFilter.java b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyCaptureFilter.java new file mode 100644 index 000000000..44fcaf99c --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyCaptureFilter.java @@ -0,0 +1,135 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed 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 io.opentelemetry.instrumentation.hypertrace.jaxrs.v2_0; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.ClientTracingFilter; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.ContentTypeUtils; +import org.hypertrace.agent.core.HypertraceConfig; +import org.hypertrace.agent.core.HypertraceSemanticAttributes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JaxrsClientBodyCaptureFilter implements ClientRequestFilter, ClientResponseFilter { + + private static final Logger log = LoggerFactory.getLogger(JaxrsClientBodyCaptureFilter.class); + + @Override + public void filter(ClientRequestContext requestContext) { + Object spanObj = requestContext.getProperty(ClientTracingFilter.SPAN_PROPERTY_NAME); + if (!(spanObj instanceof Span)) { + return; + } + + Span currentSpan = (Span) spanObj; + AgentConfig agentConfig = HypertraceConfig.get(); + + try { + if (agentConfig.getDataCapture().getHttpHeaders().getRequest().getValue()) { + captureHeaders( + currentSpan, + HypertraceSemanticAttributes::httpRequestHeader, + requestContext.getStringHeaders()); + } + if (requestContext.hasEntity() + && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { + MediaType mediaType = requestContext.getMediaType(); + if (mediaType == null || !ContentTypeUtils.shouldCapture(mediaType.toString())) { + return; + } + + Object entity = requestContext.getEntity(); + if (entity != null) { + if (entity instanceof Form) { + Form form = (Form) entity; + String content = getUrlEncodedContent(form); + currentSpan.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, content); + } else { + currentSpan.setAttribute( + HypertraceSemanticAttributes.HTTP_REQUEST_BODY, entity.toString()); + } + } + } + requestContext.getEntity(); + } catch (Exception ex) { + log.error("Exception while getting request entity or headers", ex); + } + } + + @Override + public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) { + Object spanObj = requestContext.getProperty(ClientTracingFilter.SPAN_PROPERTY_NAME); + if (!(spanObj instanceof Span)) { + return; + } + + Span currentSpan = (Span) spanObj; + AgentConfig agentConfig = HypertraceConfig.get(); + + try { + if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) { + captureHeaders( + currentSpan, + HypertraceSemanticAttributes::httpResponseHeader, + responseContext.getHeaders()); + } + } catch (Exception ex) { + log.error("Exception while getting response entity or headers", ex); + } + } + + private static String getUrlEncodedContent(Form form) { + MultivaluedMap formMap = form.asMap(); + StringBuilder sb = new StringBuilder(); + if (formMap != null) { + for (Map.Entry> entry : formMap.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + for (String value : entry.getValue()) { + sb.append(entry.getKey()); + sb.append("="); + sb.append(value); + } + } + } + return sb.toString(); + } + + private static void captureHeaders( + Span span, + Function> keySupplier, + MultivaluedMap headers) { + for (Map.Entry> entry : headers.entrySet()) { + for (Object value : entry.getValue()) { + span.setAttribute(keySupplier.apply(entry.getKey()), value.toString()); + } + } + } +} diff --git a/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationModule.java b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationModule.java new file mode 100644 index 000000000..1901cbde7 --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationModule.java @@ -0,0 +1,88 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed 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 io.opentelemetry.instrumentation.hypertrace.jaxrs.v2_0; + +import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.hasInterface; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.ws.rs.client.Client; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.bytecode.assign.Assigner; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class JaxrsClientBodyInstrumentationModule extends InstrumentationModule { + + @Override + public int getOrder() { + return 1; + } + + public JaxrsClientBodyInstrumentationModule() { + super(JaxrsClientBodyInstrumentationName.PRIMARY, JaxrsClientBodyInstrumentationName.OTHER); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new JaxrsClientBuilderInstrumentation()); + } + + class JaxrsClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("javax.ws.rs.client.ClientBuilder"); + } + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("javax.ws.rs.client.ClientBuilder")); + } + + @Override + public Map, String> transformers() { + return singletonMap( + named("build").and(returns(hasInterface(named("javax.ws.rs.client.Client")))), + ClientBuilder_build_Advice.class.getName()); + } + } + + static class ClientBuilder_build_Advice { + @Advice.OnMethodExit + public static void registerFeature( + @Advice.Return(typing = Assigner.Typing.DYNAMIC) Client client) { + // Register on the generated client instead of the builder + // The build() can be called multiple times and is not thread safe + // A client is only created once + // Use lowest priority to run after OTEL filter that controls lifecycle of span + client.register(JaxrsClientBodyCaptureFilter.class, Integer.MIN_VALUE); + client.register(JaxrsClientEntityInterceptor.class); + } + } +} diff --git a/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationName.java b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationName.java new file mode 100644 index 000000000..158d61f78 --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationName.java @@ -0,0 +1,24 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed 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 io.opentelemetry.instrumentation.hypertrace.jaxrs.v2_0; + +public class JaxrsClientBodyInstrumentationName { + public static final String PRIMARY = "jaxrs-client"; + public static final String[] OTHER = { + "jaxrs-client-2.0", "ht", "jaxrs-client-ht", "jaxrs-client-2.0-ht" + }; +} diff --git a/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientEntityInterceptor.java b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientEntityInterceptor.java new file mode 100644 index 000000000..dde6e4555 --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/src/main/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientEntityInterceptor.java @@ -0,0 +1,78 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed 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 io.opentelemetry.instrumentation.hypertrace.jaxrs.v2_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.jaxrsclient.v2_0.ClientTracingFilter; +import java.io.IOException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.ContentTypeUtils; +import org.hypertrace.agent.core.HypertraceConfig; +import org.hypertrace.agent.core.HypertraceSemanticAttributes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JaxrsClientEntityInterceptor implements ReaderInterceptor { + + private static final Logger log = LoggerFactory.getLogger(JaxrsClientEntityInterceptor.class); + + private static final Tracer TRACER = + OpenTelemetry.getGlobalTracer("org.hypertrace.java.jaxrs.client"); + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) + throws IOException, WebApplicationException { + + Object entity = context.proceed(); + + Object spanObj = context.getProperty(ClientTracingFilter.SPAN_PROPERTY_NAME); + if (!(spanObj instanceof Span)) { + log.error( + "Span object is not present in the context properties, response object will not be captured"); + return entity; + } + Span currentSpan = (Span) spanObj; + + MediaType mediaType = context.getMediaType(); + AgentConfig agentConfig = HypertraceConfig.get(); + if (mediaType == null + || !ContentTypeUtils.shouldCapture(mediaType.toString()) + || !agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { + return entity; + } + + if (currentSpan.isRecording()) { + currentSpan.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, entity.toString()); + } else { + TRACER + .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) + .setParent(Context.root().with(currentSpan)) + .setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, entity.toString()) + .startSpan() + .end(); + } + + return entity; + } +} diff --git a/instrumentation/jaxrs-client-2.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationTest.java b/instrumentation/jaxrs-client-2.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationTest.java new file mode 100644 index 000000000..177297c15 --- /dev/null +++ b/instrumentation/jaxrs-client-2.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/jaxrs/v2_0/JaxrsClientBodyInstrumentationTest.java @@ -0,0 +1,157 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed 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 io.opentelemetry.instrumentation.hypertrace.jaxrs.v2_0; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.TimeoutException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import org.hypertrace.agent.core.HypertraceSemanticAttributes; +import org.hypertrace.agent.testing.AbstractInstrumenterTest; +import org.hypertrace.agent.testing.TestHttpServer; +import org.hypertrace.agent.testing.TestHttpServer.GetJsonHandler; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class JaxrsClientBodyInstrumentationTest extends AbstractInstrumenterTest { + + private static final String JSON = "{\"id\":1,\"name\":\"John\"}"; + private static final TestHttpServer testHttpServer = new TestHttpServer(); + + @BeforeAll + public static void startServer() throws Exception { + testHttpServer.start(); + } + + @AfterAll + public static void closeServer() throws Exception { + testHttpServer.close(); + } + + @Test + public void getJson() throws TimeoutException, InterruptedException { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + Client client = clientBuilder.build(); + + Response response = + client + .target(String.format("http://localhost:%d/get_json", testHttpServer.port())) + .request() + .header("test-request-header", "test-header-value") + .get(); + Assertions.assertEquals(200, response.getStatus()); + // read entity has to happen before response.close() + String entity = response.readEntity(String.class); + Assertions.assertEquals(GetJsonHandler.RESPONSE_BODY, entity); + Assertions.assertEquals(false, Span.current().isRecording()); + response.close(); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + Assertions.assertEquals(2, traces.get(0).size()); + SpanData clientSpan = traces.get(0).get(0); + + Assertions.assertEquals( + "test-value", + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header"))); + Assertions.assertEquals( + "test-header-value", + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader("test-request-header"))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + SpanData responseBodySpan = traces.get(0).get(1); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + responseBodySpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void postJson() throws TimeoutException, InterruptedException { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + Client client = clientBuilder.build(); + + Response response = + client + .target(String.format("http://localhost:%d/post", testHttpServer.port())) + .request() + .header("test-request-header", "test-header-value") + .post(Entity.entity(JSON, MediaType.APPLICATION_JSON_TYPE)); + Assertions.assertEquals(204, response.getStatus()); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + Assertions.assertEquals(1, traces.get(0).size()); + SpanData clientSpan = traces.get(0).get(0); + + Assertions.assertEquals( + "test-value", + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header"))); + Assertions.assertEquals( + JSON, clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void postUrlEncoded() throws TimeoutException, InterruptedException { + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + Client client = clientBuilder.build(); + + WebTarget webTarget = + client.target(String.format("http://localhost:%d/post", testHttpServer.port())); + MultivaluedMap formData = new MultivaluedHashMap<>(); + formData.add("key1", "value1"); + formData.add("key2", "value2"); + Response response = webTarget.request().post(Entity.form(formData)); + Assertions.assertEquals(204, response.getStatus()); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + Assertions.assertEquals(1, traces.get(0).size()); + SpanData clientSpan = traces.get(0).get(0); + + Assertions.assertEquals( + "test-value", + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header"))); + Assertions.assertEquals( + "key1=value1&key2=value2", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/okhttp/okhttp-3.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/okhttp/v3_0/OkHttpTracingInterceptorTest.java b/instrumentation/okhttp/okhttp-3.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/okhttp/v3_0/OkHttpTracingInterceptorTest.java index 4545ec44d..d19f3737a 100644 --- a/instrumentation/okhttp/okhttp-3.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/okhttp/v3_0/OkHttpTracingInterceptorTest.java +++ b/instrumentation/okhttp/okhttp-3.0/src/test/java/io/opentelemetry/instrumentation/hypertrace/okhttp/v3_0/OkHttpTracingInterceptorTest.java @@ -113,7 +113,7 @@ public void getJson() throws Exception { @Test public void postUrlEncoded() throws Exception { - FormBody formBody = new FormBody.Builder().add("foo", "bar").build(); + FormBody formBody = new FormBody.Builder().add("key1", "value1").add("key2", "value2").build(); Request request = new Builder() .url(String.format("http://localhost:%d/post", testHttpServer.port())) @@ -134,7 +134,8 @@ public void postUrlEncoded() throws Exception { .getAttributes() .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header"))); Assertions.assertEquals( - "foo=bar", clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + "key1=value1&key2=value2", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); Assertions.assertNull( clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); } diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/HypertraceSemanticAttributes.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/HypertraceSemanticAttributes.java index 391cd13af..972c60f4c 100644 --- a/javaagent-core/src/main/java/org/hypertrace/agent/core/HypertraceSemanticAttributes.java +++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/HypertraceSemanticAttributes.java @@ -21,6 +21,14 @@ public class HypertraceSemanticAttributes { private HypertraceSemanticAttributes() {} + /** + * Span name used for span that carries additional data (e.g. attributes) that should belong to + * its parent. The parent span cannot carry additional data because it has been already finished. + * This usually happens when capturing response body in RPC client instrumentations - the body is + * read after the client span is finished. + */ + public static final String ADDITIONAL_DATA_SPAN_NAME = "additional-data"; + public static AttributeKey httpRequestHeader(String header) { return AttributeKey.stringKey("http.request.header." + header); } diff --git a/settings.gradle.kts b/settings.gradle.kts index caeafcfad..f74f4b1a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,3 +39,5 @@ findProject(":instrumentation:okhttp:okhttp-3.0")?.name = "okhttp-3.0" include("filter-custom-opa") include("otel-extensions") include("testing-bootstrap") +include("instrumentation:jaxrs-client-2.0") +findProject(":instrumentation:jaxrs-client-2.0")?.name = "jaxrs-client-2.0"