From de119e964fe78b97103dc01e554aec0b385c988e Mon Sep 17 00:00:00 2001
From: Pavol Loffay
Date: Wed, 13 Jan 2021 13:22:47 +0100
Subject: [PATCH 01/10] Add netty client HTTP payload and headers data capture
Signed-off-by: Pavol Loffay
---
.../netty/netty-4.0/build.gradle.kts | 1 +
.../NettyChannelPipelineInstrumentation.java | 51 ++++--
.../netty/v4_0/client/DataCaptureUtils.java | 86 +++++++++++
.../HttpClientRequestTracingHandler.java | 106 +++++++++++++
.../HttpClientResponseTracingHandler.java | 117 ++++++++++++++
.../v4_0/client/HttpClientTracingHandler.java | 28 ++++
.../Netty40ClientInstrumentationTest.java | 146 ++++++++++++++++++
.../agent/testing/TestHttpServer.java | 6 +-
8 files changed, 525 insertions(+), 16 deletions(-)
create mode 100644 instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/DataCaptureUtils.java
create mode 100644 instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientRequestTracingHandler.java
create mode 100644 instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientResponseTracingHandler.java
create mode 100644 instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/HttpClientTracingHandler.java
create mode 100644 instrumentation/netty/netty-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/Netty40ClientInstrumentationTest.java
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/NettyChannelPipelineInstrumentation.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/NettyChannelPipelineInstrumentation.java
index 4d6b246f1..1b6e4ad20 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,37 @@ 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/DataCaptureUtils.java b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/DataCaptureUtils.java
new file mode 100644
index 000000000..875893065
--- /dev/null
+++ b/instrumentation/netty/netty-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/netty/v4_0/client/DataCaptureUtils.java
@@ -0,0 +1,86 @@
+/*
+ * 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.handler.codec.http.HttpContent;
+import io.netty.handler.codec.http.HttpMessage;
+import io.netty.handler.codec.http.LastHttpContent;
+import io.netty.util.Attribute;
+import io.netty.util.AttributeKey;
+import io.opentelemetry.api.trace.Span;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import org.hypertrace.agent.core.instrumentation.buffer.BoundedByteArrayOutputStream;
+
+public class DataCaptureUtils {
+
+ private DataCaptureUtils() {}
+
+ public static void captureBody(
+ Span span,
+ Channel channel,
+ AttributeKey attributeKey,
+ Object httpContentOrBuffer) {
+
+ Attribute bufferAttr = channel.attr(attributeKey);
+ BoundedByteArrayOutputStream buffer = bufferAttr.get();
+ if (buffer == null) {
+ // not capturing body e.g. unknown content type
+ return;
+ }
+
+ ByteBuf content = castToBuf(httpContentOrBuffer);
+ if (content != null && content.isReadable()) {
+ final ByteArrayOutputStream finalBuffer = buffer;
+ content.forEachByte(
+ value -> {
+ finalBuffer.write(value);
+ return true;
+ });
+ }
+
+ if (httpContentOrBuffer instanceof LastHttpContent) {
+ bufferAttr.remove();
+ try {
+ span.setAttribute(attributeKey.name(), buffer.toStringWithSuppliedCharset());
+ } catch (UnsupportedEncodingException e) {
+ // ignore charset was parsed before
+ }
+ }
+ }
+
+ private static ByteBuf castToBuf(Object msg) {
+ if (msg instanceof ByteBuf) {
+ return (ByteBuf) msg;
+ } else if (msg instanceof HttpContent) {
+ HttpContent httpContent = (HttpContent) msg;
+ return httpContent.content();
+ }
+ return null;
+ }
+
+ // see io.netty.handler.codec.http.HttpUtil
+ public static CharSequence getContentType(HttpMessage message) {
+ return message.headers().get("content-type");
+ }
+
+ public static CharSequence getContentLength(HttpMessage message) {
+ return message.headers().get("content-length");
+ }
+}
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..22d9d396c
--- /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 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..0b48766ec
--- /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,117 @@
+/*
+ * 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 static io.opentelemetry.javaagent.instrumentation.netty.v4_0.server.NettyHttpServerTracer.tracer;
+
+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 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
+ && 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) {
+ 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/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