diff --git a/README.md b/README.md index a1176a435..34dc9c2d4 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ List of supported frameworks with additional capabilities: | [Apache HttpClient](https://hc.apache.org/index.html) | 4.0+ | | [gRPC](https://github.com/grpc/grpc-java) | 1.5+ | | [JAX-RS Client](https://javaee.github.io/javaee-spec/javadocs/javax/ws/rs/client/package-summary.html) | 2.0+ | -| [Micronaut](https://micronaut.io/) (via Netty and only server) | 1.0+ | -| [Netty](https://github.com/netty/netty) (only server) | 4.0+ | +| [Micronaut](https://micronaut.io/) (basic support via Netty) | 1.0+ | +| [Netty](https://github.com/netty/netty) | 4.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+ | -| [Spring Webflux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) (only server) | 5.0+ | -| [Vert.x](https://vertx.io) (only server) | 3.0+ | +| [Spring Webflux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.0+ | +| [Vert.x](https://vertx.io) | 3.0+ | ### Adding custom filter implementation diff --git a/instrumentation/micronaut-1.0/build.gradle.kts b/instrumentation/micronaut-1.0/build.gradle.kts index 5b91917ba..b7d405379 100644 --- a/instrumentation/micronaut-1.0/build.gradle.kts +++ b/instrumentation/micronaut-1.0/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { testImplementation("io.micronaut:micronaut-http-server-netty:${micronautVersion}") testImplementation("io.micronaut:micronaut-runtime:${micronautVersion}") testImplementation("io.micronaut:micronaut-inject:${micronautVersion}") + testImplementation("io.micronaut:micronaut-http-client:${micronautVersion}") testAnnotationProcessor("io.micronaut:micronaut-inject-java:${micronautVersion}") } diff --git a/instrumentation/micronaut-1.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/micronaut/MicronautClientInstrumentationTest.java b/instrumentation/micronaut-1.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/micronaut/MicronautClientInstrumentationTest.java new file mode 100644 index 000000000..89ba1ada1 --- /dev/null +++ b/instrumentation/micronaut-1.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/micronaut/MicronautClientInstrumentationTest.java @@ -0,0 +1,131 @@ +/* + * 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.javaagent.instrumentation.hypertrace.micronaut; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.annotation.MicronautTest; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.TimeoutException; +import javax.inject.Inject; +import org.hypertrace.agent.core.instrumentation.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; + +@MicronautTest +public class MicronautClientInstrumentationTest extends AbstractInstrumenterTest { + + private static final String REQUEST_BODY = "hello_foo_bar"; + private static final String REQUEST_HEADER_NAME = "reqheadername"; + private static final String REQUEST_HEADER_VALUE = "reqheadervalue"; + + 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(); + } + + @Inject + @Client("/") + private HttpClient client; + + @Test + public void getJson() throws InterruptedException, TimeoutException { + String retrieve = + client + .toBlocking() + .retrieve( + HttpRequest.GET( + String.format("http://localhost:%d/get_json", testHttpServer.port())) + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE)); + Assertions.assertEquals(GetJsonHandler.RESPONSE_BODY, retrieve); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void post() throws InterruptedException, TimeoutException { + HttpResponse response = + client + .toBlocking() + .exchange( + HttpRequest.POST( + String.format("http://localhost:%d/post", testHttpServer.port()), + REQUEST_BODY) + .header(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .header("Content-Type", "application/json")); + Assertions.assertEquals(204, response.getStatus().getCode()); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertEquals( + REQUEST_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/netty/netty-4.0/build.gradle.kts b/instrumentation/netty/netty-4.0/build.gradle.kts index cd0557797..e58c1e7a3 100644 --- a/instrumentation/netty/netty-4.0/build.gradle.kts +++ b/instrumentation/netty/netty-4.0/build.gradle.kts @@ -47,5 +47,6 @@ dependencies { implementation("io.netty:netty-codec-http:4.0.0.Final") testImplementation(project(":testing-common")) + testImplementation("org.asynchttpclient:async-http-client:2.0.9") } diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/DataCaptureUtils.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/DataCaptureUtils.java similarity index 99% rename from instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/DataCaptureUtils.java rename to instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/DataCaptureUtils.java index 38102a0bd..af6ed1ed2 100644 --- a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/DataCaptureUtils.java +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/DataCaptureUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyChannelPipelineInstrumentation.java index 4d6b246f1..796cdc1a2 100644 --- a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyChannelPipelineInstrumentation.java @@ -25,10 +25,16 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpRequestEncoder; +import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpServerCodec; import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.client.HttpClientRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.client.HttpClientResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.client.HttpClientTracingHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.HttpServerBlockingRequestHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.HttpServerRequestTracingHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.HttpServerResponseTracingHandler; @@ -123,22 +129,36 @@ public static void addHandler( pipeline.addLast( HttpServerBlockingRequestHandler.class.getName(), new HttpServerBlockingRequestHandler()); + } else + // Client pipeline handlers + if (handler instanceof HttpClientCodec) { + pipeline.replace( + io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.HttpClientTracingHandler + .class + .getName(), + HttpClientTracingHandler.class.getName(), + new HttpClientTracingHandler()); + + // add OTEL request handler to start spans + pipeline.addAfter( + HttpClientTracingHandler.class.getName(), + io.opentelemetry.javaagent.instrumentation.netty.v4_0.client + .HttpClientRequestTracingHandler.class + .getName(), + new io.opentelemetry.javaagent.instrumentation.netty.v4_0.client + .HttpClientRequestTracingHandler()); + } else if (handler instanceof HttpRequestEncoder) { + pipeline.addLast( + HttpClientRequestTracingHandler.class.getName(), + new HttpClientRequestTracingHandler()); + } else if (handler instanceof HttpResponseDecoder) { + pipeline.replace( + io.opentelemetry.javaagent.instrumentation.netty.v4_0.client + .HttpClientResponseTracingHandler.class + .getName(), + HttpClientResponseTracingHandler.class.getName(), + new HttpClientResponseTracingHandler()); } - // TODO add client instrumentation - // else - // Client pipeline handlers - // if (handler instanceof HttpClientCodec) { - // pipeline.addLast( - // HttpClientTracingHandler.class.getName(), new HttpClientTracingHandler()); - // } else if (handler instanceof HttpRequestEncoder) { - // pipeline.addLast( - // HttpClientRequestTracingHandler.class.getName(), - // new HttpClientRequestTracingHandler()); - // } else if (handler instanceof HttpResponseDecoder) { - // pipeline.addLast( - // HttpClientResponseTracingHandler.class.getName(), - // new HttpClientResponseTracingHandler()); - // } } catch (IllegalArgumentException e) { // Prevented adding duplicate handlers. } diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientRequestTracingHandler.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientRequestTracingHandler.java new file mode 100644 index 000000000..78cefc1c2 --- /dev/null +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,106 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_0.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.Attribute; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.DataCaptureUtils; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapter { + + private final AgentConfig agentConfig = HypertraceConfig.get(); + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + Channel channel = ctx.channel(); + Context context = + channel + .attr( + io.opentelemetry.javaagent.instrumentation.netty.v4_0.AttributeKeys.CLIENT_CONTEXT) + .get(); + if (context == null) { + ctx.write(msg, prm); + return; + } + Span span = Span.fromContext(context); + + if (msg instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest) msg; + + Map headersMap = headersToMap(httpRequest); + if (agentConfig.getDataCapture().getHttpHeaders().getRequest().getValue()) { + headersMap.forEach((key, value) -> span.setAttribute(key, value)); + } + + CharSequence contentType = DataCaptureUtils.getContentType(httpRequest); + if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue() + && contentType != null + && ContentTypeUtils.shouldCapture(contentType.toString())) { + + CharSequence contentLengthHeader = DataCaptureUtils.getContentLength(httpRequest); + int contentLength = ContentLengthUtils.parseLength(contentLengthHeader); + + String charsetString = ContentTypeUtils.parseCharset(contentType.toString()); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetString); + + // set the buffer to capture response body + // the buffer is used byt captureBody method + Attribute bufferAttr = + ctx.channel().attr(AttributeKeys.REQUEST_BODY_BUFFER); + bufferAttr.set(BoundedBuffersFactory.createStream(contentLength, charset)); + } + } + + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { + DataCaptureUtils.captureBody(span, channel, AttributeKeys.REQUEST_BODY_BUFFER, msg); + } + + ctx.write(msg, prm); + } + + private static Map headersToMap(HttpMessage httpMessage) { + Map map = new HashMap<>(); + for (Map.Entry entry : httpMessage.headers().entries()) { + AttributeKey key = HypertraceSemanticAttributes.httpRequestHeader(entry.getKey()); + map.put(key.getKey(), entry.getValue()); + } + return map; + } +} diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientResponseTracingHandler.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientResponseTracingHandler.java new file mode 100644 index 000000000..bb0e7a9ef --- /dev/null +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,118 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_0.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.Attribute; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.DataCaptureUtils; +import io.opentelemetry.javaagent.instrumentation.netty.v4_0.client.NettyHttpClientTracer; +import java.nio.charset.Charset; +import java.util.Map; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpClientResponseTracingHandler extends ChannelInboundHandlerAdapter { + + private final AgentConfig agentConfig = HypertraceConfig.get(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel channel = ctx.channel(); + Context context = + channel + .attr( + io.opentelemetry.javaagent.instrumentation.netty.v4_0.AttributeKeys.CLIENT_CONTEXT) + .get(); + if (context == null) { + ctx.fireChannelRead(msg); + return; + } + Span span = Span.fromContext(context); + + if (msg instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) msg; + if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) { + captureHeaders(span, httpResponse); + } + + CharSequence contentType = DataCaptureUtils.getContentType(httpResponse); + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && contentType != null + && ContentTypeUtils.shouldCapture(contentType.toString())) { + + CharSequence contentLengthHeader = DataCaptureUtils.getContentLength(httpResponse); + int contentLength = ContentLengthUtils.parseLength(contentLengthHeader); + + String charsetString = ContentTypeUtils.parseCharset(contentType.toString()); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetString); + + // set the buffer to capture response body + // the buffer is used byt captureBody method + Attribute bufferAttr = + ctx.channel().attr(AttributeKeys.RESPONSE_BODY_BUFFER); + bufferAttr.set(BoundedBuffersFactory.createStream(contentLength, charset)); + } + } + + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { + DataCaptureUtils.captureBody(span, ctx.channel(), AttributeKeys.RESPONSE_BODY_BUFFER, msg); + } + + try (Scope ignored = context.makeCurrent()) { + ctx.fireChannelRead(msg); + } catch (Throwable throwable) { + NettyHttpClientTracer.tracer().endExceptionally(context, throwable); + throw throwable; + } + if (msg instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) msg; + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpResponse.getStatus().code()); + span.setStatus(HttpStatusConverter.statusFromHttpStatus(httpResponse.getStatus().code())); + } + if (msg instanceof FullHttpMessage || msg instanceof LastHttpContent) { + span.end(); + } + } + + private static void captureHeaders(Span span, HttpMessage httpMessage) { + for (Map.Entry entry : httpMessage.headers().entries()) { + span.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(entry.getKey()), entry.getValue()); + } + } +} diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientTracingHandler.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientTracingHandler.java new file mode 100644 index 000000000..7d0e80f65 --- /dev/null +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientTracingHandler.java @@ -0,0 +1,28 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_0.client; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpClientTracingHandler + extends CombinedChannelDuplexHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler() { + super(new HttpClientResponseTracingHandler(), new HttpClientRequestTracingHandler()); + } +} diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerRequestTracingHandler.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerRequestTracingHandler.java index 9d02757a6..439a358e6 100644 --- a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerRequestTracingHandler.java +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerRequestTracingHandler.java @@ -16,6 +16,7 @@ package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; +import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -27,6 +28,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.DataCaptureUtils; import io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer; import java.nio.charset.Charset; import java.util.HashMap; @@ -83,7 +85,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } - if (msg instanceof HttpContent + if ((msg instanceof HttpContent || msg instanceof ByteBuf) && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { DataCaptureUtils.captureBody(span, channel, AttributeKeys.REQUEST_BODY_BUFFER, msg); } diff --git a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerResponseTracingHandler.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerResponseTracingHandler.java index 18946643a..1893ce7cc 100644 --- a/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerResponseTracingHandler.java +++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/HttpServerResponseTracingHandler.java @@ -18,6 +18,7 @@ import static io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer.tracer; +import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; @@ -33,6 +34,7 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.DataCaptureUtils; import io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer; import java.nio.charset.Charset; import java.util.Map; @@ -83,7 +85,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { } } - if (msg instanceof HttpContent + if ((msg instanceof HttpContent || msg instanceof ByteBuf) && agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { DataCaptureUtils.captureBody(span, ctx.channel(), AttributeKeys.RESPONSE_BODY_BUFFER, msg); } diff --git a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/Netty40ClientInstrumentationTest.java b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/Netty40ClientInstrumentationTest.java new file mode 100644 index 000000000..681a87e5f --- /dev/null +++ b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/Netty40ClientInstrumentationTest.java @@ -0,0 +1,146 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_0.client; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.asynchttpclient.AsyncCompletionHandler; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.ListenableFuture; +import org.asynchttpclient.Response; +import org.hypertrace.agent.core.instrumentation.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 Netty40ClientInstrumentationTest extends AbstractInstrumenterTest { + + private static final String REQUEST_BODY = "hello_foo_bar"; + private static final String REQUEST_HEADER_NAME = "reqheadername"; + private static final String REQUEST_HEADER_VALUE = "reqheadervalue"; + + private static final TestHttpServer testHttpServer = new TestHttpServer(); + + private final DefaultAsyncHttpClientConfig clientConfig = + new DefaultAsyncHttpClientConfig.Builder().setRequestTimeout(30000).build(); + private final AsyncHttpClient asyncHttpClient = Dsl.asyncHttpClient(clientConfig); + + @BeforeAll + public static void startServer() throws Exception { + testHttpServer.start(); + } + + @AfterAll + public static void closeServer() throws Exception { + testHttpServer.close(); + } + + @Test + public void getJson() throws ExecutionException, InterruptedException, TimeoutException { + ListenableFuture response = + asyncHttpClient + .prepareGet(String.format("http://localhost:%d/get_json", testHttpServer.port())) + .addHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .execute( + new AsyncCompletionHandler() { + @Override + public Object onCompleted(Response response) throws Exception { + return null; + } + }); + + // wait for the result + response.get(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void post() throws ExecutionException, InterruptedException, TimeoutException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(REQUEST_BODY.getBytes()); + + ListenableFuture response = + asyncHttpClient + .preparePost(String.format("http://localhost:%d/post", testHttpServer.port())) + .setBody(inputStream) + .addHeader("Content-Type", "application/json") + .addHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .execute( + new AsyncCompletionHandler() { + @Override + public Object onCompleted(Response response) throws Exception { + return null; + } + }); + + // wait for the result + response.get(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertEquals( + REQUEST_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/AbstractNetty40ServerInstrumentationTest.java b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/AbstractNetty40ServerInstrumentationTest.java similarity index 97% rename from instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/AbstractNetty40ServerInstrumentationTest.java rename to instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/AbstractNetty40ServerInstrumentationTest.java index f413d1b7e..6d3911fb1 100644 --- a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/AbstractNetty40ServerInstrumentationTest.java +++ b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/AbstractNetty40ServerInstrumentationTest.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.NettyTestServer.RESPONSE_BODY; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.NettyTestServer.RESPONSE_HEADER_NAME; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.NettyTestServer.RESPONSE_HEADER_VALUE; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.NettyTestServer.RESPONSE_BODY; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.NettyTestServer.RESPONSE_HEADER_NAME; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server.NettyTestServer.RESPONSE_HEADER_VALUE; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; diff --git a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40HttpServerCodecInstrumentationTest.java b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40HttpServerCodecInstrumentationTest.java similarity index 98% rename from instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40HttpServerCodecInstrumentationTest.java rename to instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40HttpServerCodecInstrumentationTest.java index 92f863a17..b97b0fd78 100644 --- a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40HttpServerCodecInstrumentationTest.java +++ b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40HttpServerCodecInstrumentationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; import io.netty.handler.codec.http.HttpServerCodec; import java.util.Arrays; diff --git a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40ServerInstrumentationTest.java b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40ServerInstrumentationTest.java similarity index 98% rename from instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40ServerInstrumentationTest.java rename to instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40ServerInstrumentationTest.java index b42b48783..99e356427 100644 --- a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/Netty40ServerInstrumentationTest.java +++ b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/Netty40ServerInstrumentationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; diff --git a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyTestServer.java b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/NettyTestServer.java similarity index 99% rename from instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyTestServer.java rename to instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/NettyTestServer.java index d7c0fec01..97dd9124e 100644 --- a/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyTestServer.java +++ b/instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/server/NettyTestServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_0.server; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; diff --git a/instrumentation/netty/netty-4.1/build.gradle.kts b/instrumentation/netty/netty-4.1/build.gradle.kts index 9ec78ec44..a4659b931 100644 --- a/instrumentation/netty/netty-4.1/build.gradle.kts +++ b/instrumentation/netty/netty-4.1/build.gradle.kts @@ -48,5 +48,6 @@ dependencies { testImplementation(project(":testing-common")) testImplementation("io.netty:netty-handler:4.1.0.Final") + testImplementation("org.asynchttpclient:async-http-client:2.1.0") } diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/DataCaptureUtils.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/DataCaptureUtils.java similarity index 99% rename from instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/DataCaptureUtils.java rename to instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/DataCaptureUtils.java index f4fb9363d..3b1825483 100644 --- a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/DataCaptureUtils.java +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/DataCaptureUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyChannelPipelineInstrumentation.java index 9e18a7eb4..273f96f47 100644 --- a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyChannelPipelineInstrumentation.java +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyChannelPipelineInstrumentation.java @@ -25,9 +25,15 @@ import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpRequestEncoder; +import io.netty.handler.codec.http.HttpResponseDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpServerCodec; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.client.HttpClientRequestTracingHandler; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.client.HttpClientResponseTracingHandler; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.client.HttpClientTracingHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.HttpServerBlockingRequestHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.HttpServerRequestTracingHandler; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.HttpServerResponseTracingHandler; @@ -131,6 +137,35 @@ public static void addHandler( pipeline.addLast( HttpServerBlockingRequestHandler.class.getName(), new HttpServerBlockingRequestHandler()); + } else + // Client pipeline handlers + if (handler instanceof HttpClientCodec) { + pipeline.replace( + io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.HttpClientTracingHandler + .class + .getName(), + HttpClientTracingHandler.class.getName(), + new HttpClientTracingHandler()); + + // add OTEL request handler to start spans + pipeline.addAfter( + HttpClientTracingHandler.class.getName(), + io.opentelemetry.javaagent.instrumentation.netty.v4_1.client + .HttpClientRequestTracingHandler.class + .getName(), + new io.opentelemetry.javaagent.instrumentation.netty.v4_1.client + .HttpClientRequestTracingHandler()); + } else if (handler instanceof HttpRequestEncoder) { + pipeline.addLast( + HttpClientRequestTracingHandler.class.getName(), + new HttpClientRequestTracingHandler()); + } else if (handler instanceof HttpResponseDecoder) { + pipeline.replace( + io.opentelemetry.javaagent.instrumentation.netty.v4_1.client + .HttpClientResponseTracingHandler.class + .getName(), + HttpClientResponseTracingHandler.class.getName(), + new HttpClientResponseTracingHandler()); } // TODO add client instrumentation // else diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientRequestTracingHandler.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientRequestTracingHandler.java new file mode 100644 index 000000000..55ed819fc --- /dev/null +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientRequestTracingHandler.java @@ -0,0 +1,106 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_1.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.Attribute; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.DataCaptureUtils; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpClientRequestTracingHandler extends ChannelOutboundHandlerAdapter { + + private final AgentConfig agentConfig = HypertraceConfig.get(); + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { + Channel channel = ctx.channel(); + Context context = + channel + .attr( + io.opentelemetry.javaagent.instrumentation.netty.v4_1.AttributeKeys.CLIENT_CONTEXT) + .get(); + if (context == null) { + ctx.write(msg, prm); + return; + } + Span span = Span.fromContext(context); + + if (msg instanceof HttpRequest) { + HttpRequest httpRequest = (HttpRequest) msg; + + Map headersMap = headersToMap(httpRequest); + if (agentConfig.getDataCapture().getHttpHeaders().getRequest().getValue()) { + headersMap.forEach((key, value) -> span.setAttribute(key, value)); + } + + CharSequence contentType = DataCaptureUtils.getContentType(httpRequest); + if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue() + && contentType != null + && ContentTypeUtils.shouldCapture(contentType.toString())) { + + CharSequence contentLengthHeader = DataCaptureUtils.getContentLength(httpRequest); + int contentLength = ContentLengthUtils.parseLength(contentLengthHeader); + + String charsetString = ContentTypeUtils.parseCharset(contentType.toString()); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetString); + + // set the buffer to capture response body + // the buffer is used byt captureBody method + Attribute bufferAttr = + ctx.channel().attr(AttributeKeys.REQUEST_BODY_BUFFER); + bufferAttr.set(BoundedBuffersFactory.createStream(contentLength, charset)); + } + } + + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { + DataCaptureUtils.captureBody(span, channel, AttributeKeys.REQUEST_BODY_BUFFER, msg); + } + + ctx.write(msg, prm); + } + + private static Map headersToMap(HttpMessage httpMessage) { + Map map = new HashMap<>(); + for (Map.Entry entry : httpMessage.headers().entries()) { + AttributeKey key = HypertraceSemanticAttributes.httpRequestHeader(entry.getKey()); + map.put(key.getKey(), entry.getValue()); + } + return map; + } +} diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientResponseTracingHandler.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientResponseTracingHandler.java new file mode 100644 index 000000000..54bc77988 --- /dev/null +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientResponseTracingHandler.java @@ -0,0 +1,118 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_1.client; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.Attribute; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.attributes.SemanticAttributes; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.DataCaptureUtils; +import io.opentelemetry.javaagent.instrumentation.netty.v4_1.client.NettyHttpClientTracer; +import java.nio.charset.Charset; +import java.util.Map; +import org.hypertrace.agent.config.Config.AgentConfig; +import org.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream; +import org.hypertrace.agent.core.instrumentation.utils.ContentLengthUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeCharsetUtils; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpClientResponseTracingHandler extends ChannelInboundHandlerAdapter { + + private final AgentConfig agentConfig = HypertraceConfig.get(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel channel = ctx.channel(); + Context context = + channel + .attr( + io.opentelemetry.javaagent.instrumentation.netty.v4_1.AttributeKeys.CLIENT_CONTEXT) + .get(); + if (context == null) { + ctx.fireChannelRead(msg); + return; + } + Span span = Span.fromContext(context); + + if (msg instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) msg; + if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) { + captureHeaders(span, httpResponse); + } + + CharSequence contentType = DataCaptureUtils.getContentType(httpResponse); + if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue() + && contentType != null + && ContentTypeUtils.shouldCapture(contentType.toString())) { + + CharSequence contentLengthHeader = DataCaptureUtils.getContentLength(httpResponse); + int contentLength = ContentLengthUtils.parseLength(contentLengthHeader); + + String charsetString = ContentTypeUtils.parseCharset(contentType.toString()); + Charset charset = ContentTypeCharsetUtils.toCharset(charsetString); + + // set the buffer to capture response body + // the buffer is used byt captureBody method + Attribute bufferAttr = + ctx.channel().attr(AttributeKeys.RESPONSE_BODY_BUFFER); + bufferAttr.set(BoundedBuffersFactory.createStream(contentLength, charset)); + } + } + + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { + DataCaptureUtils.captureBody(span, ctx.channel(), AttributeKeys.RESPONSE_BODY_BUFFER, msg); + } + + try (Scope ignored = context.makeCurrent()) { + ctx.fireChannelRead(msg); + } catch (Throwable throwable) { + NettyHttpClientTracer.tracer().endExceptionally(context, throwable); + throw throwable; + } + if (msg instanceof HttpResponse) { + HttpResponse httpResponse = (HttpResponse) msg; + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpResponse.getStatus().code()); + span.setStatus(HttpStatusConverter.statusFromHttpStatus(httpResponse.getStatus().code())); + } + if (msg instanceof FullHttpMessage || msg instanceof LastHttpContent) { + span.end(); + } + } + + private static void captureHeaders(Span span, HttpMessage httpMessage) { + for (Map.Entry entry : httpMessage.headers().entries()) { + span.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(entry.getKey()), entry.getValue()); + } + } +} diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientTracingHandler.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientTracingHandler.java new file mode 100644 index 000000000..fed09a5fd --- /dev/null +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/HttpClientTracingHandler.java @@ -0,0 +1,28 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_1.client; + +import io.netty.channel.CombinedChannelDuplexHandler; + +public class HttpClientTracingHandler + extends CombinedChannelDuplexHandler< + HttpClientResponseTracingHandler, HttpClientRequestTracingHandler> { + + public HttpClientTracingHandler() { + super(new HttpClientResponseTracingHandler(), new HttpClientRequestTracingHandler()); + } +} diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerRequestTracingHandler.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerRequestTracingHandler.java index f077ee7c3..d3bbc582c 100644 --- a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerRequestTracingHandler.java +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerRequestTracingHandler.java @@ -28,6 +28,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.DataCaptureUtils; import io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.NettyHttpServerTracer; import java.nio.charset.Charset; import java.util.HashMap; @@ -84,9 +85,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } } - if (msg instanceof HttpContent - || msg instanceof ByteBuf - && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getRequest().getValue()) { DataCaptureUtils.captureBody(span, channel, AttributeKeys.REQUEST_BODY_BUFFER, msg); } diff --git a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerResponseTracingHandler.java b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerResponseTracingHandler.java index 18641a67e..bdf336410 100644 --- a/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerResponseTracingHandler.java +++ b/instrumentation/netty/netty-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/HttpServerResponseTracingHandler.java @@ -32,6 +32,7 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.tracer.HttpStatusConverter; import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.AttributeKeys; +import io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.DataCaptureUtils; import io.opentelemetry.javaagent.instrumentation.netty.v4_1.server.NettyHttpServerTracer; import java.nio.charset.Charset; import java.util.Map; @@ -82,9 +83,8 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise prm) { } } - if (msg instanceof HttpContent - || msg instanceof ByteBuf - && agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { + if ((msg instanceof HttpContent || msg instanceof ByteBuf) + && agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) { DataCaptureUtils.captureBody(span, ctx.channel(), AttributeKeys.RESPONSE_BODY_BUFFER, msg); } diff --git a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/Netty41ClientInstrumentationTest.java b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/Netty41ClientInstrumentationTest.java new file mode 100644 index 000000000..60e602e9f --- /dev/null +++ b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/client/Netty41ClientInstrumentationTest.java @@ -0,0 +1,146 @@ +/* + * 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.javaagent.instrumentation.hypertrace.netty.v4_1.client; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.asynchttpclient.AsyncCompletionHandler; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.ListenableFuture; +import org.asynchttpclient.Response; +import org.hypertrace.agent.core.instrumentation.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 Netty41ClientInstrumentationTest extends AbstractInstrumenterTest { + + private static final String REQUEST_BODY = "hello_foo_bar"; + private static final String REQUEST_HEADER_NAME = "reqheadername"; + private static final String REQUEST_HEADER_VALUE = "reqheadervalue"; + + private static final TestHttpServer testHttpServer = new TestHttpServer(); + + private final DefaultAsyncHttpClientConfig clientConfig = + new DefaultAsyncHttpClientConfig.Builder().setRequestTimeout(30000).build(); + private final AsyncHttpClient asyncHttpClient = Dsl.asyncHttpClient(clientConfig); + + @BeforeAll + public static void startServer() throws Exception { + testHttpServer.start(); + } + + @AfterAll + public static void closeServer() throws Exception { + testHttpServer.close(); + } + + @Test + public void getJson() throws ExecutionException, InterruptedException, TimeoutException { + ListenableFuture response = + asyncHttpClient + .prepareGet(String.format("http://localhost:%d/get_json", testHttpServer.port())) + .addHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .execute( + new AsyncCompletionHandler() { + @Override + public Object onCompleted(Response response) throws Exception { + return null; + } + }); + + // wait for the result + response.get(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void post() throws ExecutionException, InterruptedException, TimeoutException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(REQUEST_BODY.getBytes()); + + ListenableFuture response = + asyncHttpClient + .preparePost(String.format("http://localhost:%d/post", testHttpServer.port())) + .setBody(inputStream) + .addHeader("Content-Type", "application/json") + .addHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .execute( + new AsyncCompletionHandler() { + @Override + public Object onCompleted(Response response) throws Exception { + return null; + } + }); + + // wait for the result + response.get(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertEquals( + REQUEST_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/AbstractNetty41ServerInstrumentationTest.java b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/AbstractNetty41ServerInstrumentationTest.java similarity index 97% rename from instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/AbstractNetty41ServerInstrumentationTest.java rename to instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/AbstractNetty41ServerInstrumentationTest.java index 733df202c..4179cd176 100644 --- a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/AbstractNetty41ServerInstrumentationTest.java +++ b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/AbstractNetty41ServerInstrumentationTest.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.NettyTestServer.RESPONSE_BODY; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.NettyTestServer.RESPONSE_HEADER_NAME; -import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.NettyTestServer.RESPONSE_HEADER_VALUE; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.NettyTestServer.RESPONSE_BODY; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.NettyTestServer.RESPONSE_HEADER_NAME; +import static io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server.NettyTestServer.RESPONSE_HEADER_VALUE; import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; diff --git a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41HttpServerCodecInstrumentationTest.java b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41HttpServerCodecInstrumentationTest.java similarity index 98% rename from instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41HttpServerCodecInstrumentationTest.java rename to instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41HttpServerCodecInstrumentationTest.java index 70e1549d6..fd4b9644d 100644 --- a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41HttpServerCodecInstrumentationTest.java +++ b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41HttpServerCodecInstrumentationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server; import io.netty.handler.codec.http.HttpServerCodec; import java.util.Arrays; diff --git a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41ServerInstrumentationTest.java b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41ServerInstrumentationTest.java similarity index 98% rename from instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41ServerInstrumentationTest.java rename to instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41ServerInstrumentationTest.java index b66fede27..71a1fb041 100644 --- a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/Netty41ServerInstrumentationTest.java +++ b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/Netty41ServerInstrumentationTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; diff --git a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyTestServer.java b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/NettyTestServer.java similarity index 99% rename from instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyTestServer.java rename to instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/NettyTestServer.java index fed2bb49e..fe6b11c15 100644 --- a/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/NettyTestServer.java +++ b/instrumentation/netty/netty-4.1/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_1/server/NettyTestServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1; +package io.opentelemetry.javaagent.instrumentation.hypertrace.netty.v4_1.server; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; diff --git a/instrumentation/spring/spring-webflux-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/spring/webflux/SpringWebfluxClientTest.java b/instrumentation/spring/spring-webflux-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/spring/webflux/SpringWebfluxClientTest.java new file mode 100644 index 000000000..bec242128 --- /dev/null +++ b/instrumentation/spring/spring-webflux-5.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/spring/webflux/SpringWebfluxClientTest.java @@ -0,0 +1,121 @@ +/* + * 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.javaagent.instrumentation.hypertrace.spring.webflux; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.TimeoutException; +import org.hypertrace.agent.core.instrumentation.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; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClient.RequestHeadersUriSpec; +import reactor.core.publisher.Mono; + +public class SpringWebfluxClientTest extends AbstractInstrumenterTest { + + private static final String REQUEST_BODY = "hello_foo_bar"; + private static final String REQUEST_HEADER_NAME = "reqheadername"; + private static final String REQUEST_HEADER_VALUE = "reqheadervalue"; + + 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 InterruptedException, TimeoutException { + WebClient client = + WebClient.builder() + .baseUrl(String.format("http://localhost:%d/get_json", testHttpServer.port())) + .defaultHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .build(); + RequestHeadersUriSpec requestHeadersUriSpec = client.get(); + Mono stringMono = requestHeadersUriSpec.retrieve().bodyToMono(String.class); + String responseBody = stringMono.block(); + Assertions.assertEquals(GetJsonHandler.RESPONSE_BODY, responseBody); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void post() throws InterruptedException, TimeoutException { + WebClient client = + WebClient.builder() + .baseUrl(String.format("http://localhost:%d/post", testHttpServer.port())) + .defaultHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .defaultHeader("Content-Type", "application/json") + .build(); + client.post().body(Mono.just(REQUEST_BODY), String.class).exchange().block(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertEquals( + REQUEST_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java new file mode 100644 index 000000000..4663557bd --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java @@ -0,0 +1,143 @@ +/* + * 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.javaagent.instrumentation.hypertrace.vertx; + +import io.opentelemetry.sdk.trace.data.SpanData; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import org.hypertrace.agent.core.instrumentation.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 VertxClientInstrumentationTest extends AbstractInstrumenterTest { + + private static final String REQUEST_BODY = "hello_foo_bar"; + private static final String REQUEST_HEADER_NAME = "reqheadername"; + private static final String REQUEST_HEADER_VALUE = "reqheadervalue"; + + private static final TestHttpServer testHttpServer = new TestHttpServer(); + + private static final Vertx vertx = Vertx.vertx(new VertxOptions()); + private final HttpClientOptions clientOptions = new HttpClientOptions(); + private final HttpClient httpClient = vertx.createHttpClient(clientOptions); + + @BeforeAll + public static void startServer() throws Exception { + testHttpServer.start(); + } + + @AfterAll + public static void closeServer() throws Exception { + testHttpServer.close(); + } + + @Test + public void getJson() throws InterruptedException, TimeoutException { + CountDownLatch countDownLatch = new CountDownLatch(1); + httpClient + .request(HttpMethod.GET, testHttpServer.port(), "localhost", "/get_json") + .putHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .handler( + new Handler() { + @Override + public void handle(HttpClientResponse response) { + Assertions.assertEquals(200, response.statusCode()); + countDownLatch.countDown(); + } + }) + .end(); + countDownLatch.await(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertEquals( + GetJsonHandler.RESPONSE_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } + + @Test + public void post() throws InterruptedException, TimeoutException { + CountDownLatch countDownLatch = new CountDownLatch(1); + httpClient + .request(HttpMethod.POST, testHttpServer.port(), "localhost", "/post") + .putHeader(REQUEST_HEADER_NAME, REQUEST_HEADER_VALUE) + .putHeader("Content-Type", "application/json") + .handler( + new Handler() { + @Override + public void handle(HttpClientResponse response) { + Assertions.assertEquals(204, response.statusCode()); + countDownLatch.countDown(); + } + }) + .end(REQUEST_BODY); + countDownLatch.await(); + + 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( + REQUEST_HEADER_VALUE, + clientSpan + .getAttributes() + .get(HypertraceSemanticAttributes.httpRequestHeader(REQUEST_HEADER_NAME))); + Assertions.assertEquals( + TestHttpServer.RESPONSE_HEADER_VALUE, + clientSpan + .getAttributes() + .get( + HypertraceSemanticAttributes.httpResponseHeader( + TestHttpServer.RESPONSE_HEADER_NAME))); + Assertions.assertEquals( + REQUEST_BODY, + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + Assertions.assertNull( + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY)); + } +} diff --git a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationTest.java b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxServerInstrumentationTest.java similarity index 99% rename from instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationTest.java rename to instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxServerInstrumentationTest.java index cef9dbcea..37e9c6e6e 100644 --- a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationTest.java +++ b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxServerInstrumentationTest.java @@ -39,7 +39,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -class VertxInstrumentationTest extends AbstractInstrumenterTest { +class VertxServerInstrumentationTest extends AbstractInstrumenterTest { static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"; static final String REQUEST_BODY = "{\"foo\": \"bar\"}"; diff --git a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxWebServer.java b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxWebServer.java index 9bd042a6d..6d93a00d6 100644 --- a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxWebServer.java +++ b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxWebServer.java @@ -33,7 +33,7 @@ public class VertxWebServer extends AbstractVerticle { @Override public void start(Future startFuture) { - int port = config().getInteger(VertxInstrumentationTest.CONFIG_HTTP_SERVER_PORT); + int port = config().getInteger(VertxServerInstrumentationTest.CONFIG_HTTP_SERVER_PORT); Router router = Router.router(vertx); router @@ -44,7 +44,8 @@ public void start(Future startFuture) { .bodyHandler( h -> { Assertions.assertEquals( - VertxInstrumentationTest.REQUEST_BODY, new String(h.getBytes())); + VertxServerInstrumentationTest.REQUEST_BODY, + new String(h.getBytes())); ctx.response() .putHeader("content-Type", "application/json; charset=utf-8"); ctx.response().putHeader(RESPONSE_HEADER_NAME, RESPONSE_HEADER_VALUE); @@ -62,7 +63,8 @@ public void start(Future startFuture) { .bodyHandler( h -> { Assertions.assertEquals( - VertxInstrumentationTest.REQUEST_BODY, new String(h.getBytes())); + VertxServerInstrumentationTest.REQUEST_BODY, + new String(h.getBytes())); ctx.response() .putHeader("content-Type", "application/json; charset=utf-8"); ctx.response().putHeader(RESPONSE_HEADER_NAME, RESPONSE_HEADER_VALUE); diff --git a/testing-common/src/main/java/org/hypertrace/agent/testing/TestHttpServer.java b/testing-common/src/main/java/org/hypertrace/agent/testing/TestHttpServer.java index 1aa5bf26d..f254a72c2 100644 --- a/testing-common/src/main/java/org/hypertrace/agent/testing/TestHttpServer.java +++ b/testing-common/src/main/java/org/hypertrace/agent/testing/TestHttpServer.java @@ -28,6 +28,10 @@ import org.eclipse.jetty.server.handler.HandlerList; public class TestHttpServer implements AutoCloseable { + + public static final String RESPONSE_HEADER_NAME = "test-response-header"; + public static final String RESPONSE_HEADER_VALUE = "test-value"; + private final Server server = new Server(0); private final HandlerList handlerList = new HandlerList(); @@ -62,7 +66,7 @@ public void handle( HttpServletRequest request, HttpServletResponse response) throws IOException { - response.setHeader("test-response-header", "test-value"); + response.setHeader(RESPONSE_HEADER_NAME, RESPONSE_HEADER_VALUE); } }