diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java index 67fb84eee..9fda52903 100644 --- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java +++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java @@ -19,6 +19,7 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; @@ -26,6 +27,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.nio.charset.Charset; import org.hypertrace.agent.core.instrumentation.HypertraceCallDepthThreadLocalMap; import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; @@ -42,6 +45,23 @@ private InputStreamUtils() {} private static final Tracer TRACER = GlobalOpenTelemetry.get().getTracer("org.hypertrace.java.inputstream"); + private static Method getAttribute = null; + + static { + try { + getAttribute = + Class.forName("io.opentelemetry.sdk.trace.SdkSpan") + .getDeclaredMethod("getAttribute", AttributeKey.class); + } catch (NoSuchMethodException e) { + log.error("getAttribute method not found in SdkSpan class", e); + } catch (ClassNotFoundException e) { + log.error("SdkSpan class not found", e); + } + if (getAttribute != null) { + getAttribute.setAccessible(true); + } + } + /** * Adds an attribute to span. If the span is ended it adds the attributed to a newly created * child. @@ -50,12 +70,34 @@ public static void addAttribute(Span span, AttributeKey attributeKey, St if (span.isRecording()) { span.setAttribute(attributeKey, value); } else { - TRACER - .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) - .setParent(Context.root().with(span)) - .setAttribute(attributeKey, value) - .startSpan() - .end(); + SpanBuilder spanBuilder = + TRACER + .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) + .setParent(Context.root().with(span)) + .setAttribute(attributeKey, value); + + // Also add content type if present + if (getAttribute != null + && span.getClass().getName().equals("io.opentelemetry.sdk.trace.SdkSpan")) { + try { + Object reqContentType = + getAttribute.invoke( + span, HypertraceSemanticAttributes.HTTP_REQUEST_HEADER_CONTENT_TYPE); + if (reqContentType != null) { + spanBuilder.setAttribute("http.request.header.content-type", (String) reqContentType); + } + Object resContentType = + getAttribute.invoke( + span, HypertraceSemanticAttributes.HTTP_RESPONSE_HEADER_CONTENT_TYPE); + if (resContentType != null) { + spanBuilder.setAttribute("http.response.header.content-type", (String) resContentType); + } + } catch (IllegalAccessException | InvocationTargetException e) { + // ignore and continue + log.debug("Could not invoke getAttribute on SdkSpan", e); + } + } + spanBuilder.startSpan().end(); } } diff --git a/instrumentation/java-streams/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModuleTest.java b/instrumentation/java-streams/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModuleTest.java index 4ded7ef00..c86e60865 100644 --- a/instrumentation/java-streams/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModuleTest.java +++ b/instrumentation/java-streams/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModuleTest.java @@ -111,4 +111,51 @@ private void read(InputStream inputStream, Runnable read, String expected) { SpanData spanData = trace.get(0); Assertions.assertEquals(expected, spanData.getAttributes().get(ATTRIBUTE_KEY)); } + + @Test + public void readAfterSpanEnd() { + + InputStream inputStream = new ByteArrayInputStream(STR.getBytes()); + + Span span = + TEST_TRACER + .spanBuilder("test-span") + .setAttribute("http.request.header.content-type", "application/json") + .setAttribute("http.response.header.content-type", "application/xml") + .startSpan(); + + Runnable read = + () -> { + while (true) { + try { + if (inputStream.read(new byte[10], 0, 10) == -1) break; + span.end(); + } catch (IOException e) { + e.printStackTrace(); + } + ; + } + }; + + BoundedByteArrayOutputStream buffer = + BoundedBuffersFactory.createStream(StandardCharsets.ISO_8859_1); + ContextAccessor.addToInputStreamContext( + inputStream, new SpanAndBuffer(span, buffer, ATTRIBUTE_KEY, StandardCharsets.ISO_8859_1)); + + read.run(); + + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + + List trace = traces.get(0); + Assertions.assertEquals(2, trace.size()); + SpanData spanData = trace.get(1); + Assertions.assertEquals(STR, spanData.getAttributes().get(ATTRIBUTE_KEY)); + Assertions.assertEquals( + "application/json", + spanData.getAttributes().get(AttributeKey.stringKey("http.request.header.content-type"))); + Assertions.assertEquals( + "application/xml", + spanData.getAttributes().get(AttributeKey.stringKey("http.response.header.content-type"))); + } } diff --git a/instrumentation/vertx/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java b/instrumentation/vertx/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java index 6cb72287d..06adf1f4a 100644 --- a/instrumentation/vertx/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java +++ b/instrumentation/vertx/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java @@ -17,18 +17,43 @@ package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ResponseBodyWrappingHandler implements Handler { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("io.opentelemetry.javaagent.vertx-core-3.0"); + private static final Logger log = LoggerFactory.getLogger(ResponseBodyWrappingHandler.class); + + private static Method getAttribute = null; + + static { + try { + getAttribute = + Class.forName("io.opentelemetry.sdk.trace.SdkSpan") + .getDeclaredMethod("getAttribute", AttributeKey.class); + } catch (NoSuchMethodException e) { + log.error("getAttribute method not found in SdkSpan class", e); + } catch (ClassNotFoundException e) { + log.error("SdkSpan class not found", e); + } + if (getAttribute != null) { + getAttribute.setAccessible(true); + } + } + private final Handler wrapped; private final Span span; @@ -43,12 +68,29 @@ public void handle(Buffer event) { if (span.isRecording()) { span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, responseBody); } else { - tracer - .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) - .setParent(Context.root().with(span)) - .setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, responseBody) - .startSpan() - .end(); + SpanBuilder spanBuilder = + tracer + .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) + .setParent(Context.root().with(span)) + .setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, responseBody); + + // Also add content type if present + if (getAttribute != null + && span.getClass().getName().equals("io.opentelemetry.sdk.trace.SdkSpan")) { + try { + Object resContentType = + getAttribute.invoke( + span, HypertraceSemanticAttributes.HTTP_RESPONSE_HEADER_CONTENT_TYPE); + if (resContentType != null) { + spanBuilder.setAttribute("http.response.header.content-type", (String) resContentType); + } + } catch (IllegalAccessException | InvocationTargetException e) { + // ignore and continue + log.debug("Could not invoke getAttribute on SdkSpan", e); + } + } + + spanBuilder.startSpan().end(); } wrapped.handle(event); diff --git a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/HypertraceSemanticAttributes.java b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/HypertraceSemanticAttributes.java index 5e94c8cd7..20b5fd171 100644 --- a/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/HypertraceSemanticAttributes.java +++ b/javaagent-core/src/main/java/org/hypertrace/agent/core/instrumentation/HypertraceSemanticAttributes.java @@ -45,6 +45,12 @@ public static AttributeKey httpResponseHeader(String header) { public static final AttributeKey HTTP_REQUEST_SESSION_ID = AttributeKey.stringKey("http.request.session_id"); + public static final AttributeKey HTTP_REQUEST_HEADER_CONTENT_TYPE = + AttributeKey.stringKey("http.request.header.content-type"); + + public static final AttributeKey HTTP_RESPONSE_HEADER_CONTENT_TYPE = + AttributeKey.stringKey("http.response.header.content-type"); + public static final AttributeKey RPC_REQUEST_BODY = AttributeKey.stringKey("rpc.request.body"); public static final AttributeKey RPC_RESPONSE_BODY = diff --git a/testing-common/src/testFixtures/java/org/hypertrace/agent/testing/AbstractHttpClientTest.java b/testing-common/src/testFixtures/java/org/hypertrace/agent/testing/AbstractHttpClientTest.java index 4e8bb258b..51764b192 100644 --- a/testing-common/src/testFixtures/java/org/hypertrace/agent/testing/AbstractHttpClientTest.java +++ b/testing-common/src/testFixtures/java/org/hypertrace/agent/testing/AbstractHttpClientTest.java @@ -16,6 +16,7 @@ package org.hypertrace.agent.testing; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.BufferedReader; import java.io.IOException; @@ -118,6 +119,10 @@ public void postJson_echo() Assertions.assertEquals(2, traces.get(0).size()); SpanData responseBodySpan = traces.get(0).get(1); assertBodies(clientSpan, responseBodySpan, body, body); + Assertions.assertNotNull( + responseBodySpan + .getAttributes() + .get(AttributeKey.stringKey("http.response.header.content-type"))); } else { Assertions.assertEquals(1, traces.get(0).size()); assertRequestAndResponseBody(clientSpan, body, body);