From c320487308481c4ca7a90ec0f5b054ea1788e7c2 Mon Sep 17 00:00:00 2001
From: Pavol Loffay
Date: Mon, 21 Dec 2020 16:06:11 +0100
Subject: [PATCH] Add async apache HTTP client body and headers capture (#190)
* Add body capture for async Apache client
Signed-off-by: Pavol Loffay
* Some progress but failing
Signed-off-by: Pavol Loffay
* Some fixes
Signed-off-by: Pavol Loffay
* Working request body
Signed-off-by: Pavol Loffay
* working
Signed-off-by: Pavol Loffay
* Add readme
Signed-off-by: Pavol Loffay
* check content types
Signed-off-by: Pavol Loffay
* Fix
Signed-off-by: Pavol Loffay
---
README.md | 1 +
.../build.gradle.kts | 39 +++
.../DelegatingRequestAccessor.java | 31 +++
...pacheAsyncClientInstrumentationModule.java | 170 +++++++++++++
...cheAsyncHttpClientInstrumentationName.java | 28 +++
...eAsyncClientInstrumentationModuleTest.java | 228 ++++++++++++++++++
.../ApacheClientInstrumentationModule.java | 163 +------------
.../v4_0/ApacheHttpClientObjectRegistry.java | 38 +++
.../v4_0/ApacheHttpClientUtils.java | 48 ++--
.../v4_0/HttpEntityInstrumentation.java | 146 +++++++++++
.../ApacheHttpClientInstrumentationTest.java | 13 +-
instrumentation/build.gradle.kts | 1 +
.../InputStreamInstrumentationModule.java | 10 +
.../java/inputstream/InputStreamUtils.java | 15 +-
settings.gradle.kts | 2 +
.../agent/testing/TestHttpServer.java | 2 +-
16 files changed, 746 insertions(+), 189 deletions(-)
create mode 100644 instrumentation/apache-httpasyncclient-4.1/build.gradle.kts
create mode 100644 instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/DelegatingRequestAccessor.java
create mode 100644 instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
create mode 100644 instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncHttpClientInstrumentationName.java
create mode 100644 instrumentation/apache-httpasyncclient-4.1/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModuleTest.java
create mode 100644 instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientObjectRegistry.java
create mode 100644 instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/HttpEntityInstrumentation.java
diff --git a/README.md b/README.md
index 3ab16729e..e161ee312 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ and adds following capabilities:
List of supported frameworks with additional capabilities:
| Library/Framework | Versions |
|--------------------------------------------------------------------------------------------------------|-----------------|
+| [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ |
| [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+ |
diff --git a/instrumentation/apache-httpasyncclient-4.1/build.gradle.kts b/instrumentation/apache-httpasyncclient-4.1/build.gradle.kts
new file mode 100644
index 000000000..fdf18cd64
--- /dev/null
+++ b/instrumentation/apache-httpasyncclient-4.1/build.gradle.kts
@@ -0,0 +1,39 @@
+plugins {
+ `java-library`
+ id("net.bytebuddy.byte-buddy")
+ id("io.opentelemetry.instrumentation.auto-instrumentation")
+ muzzle
+}
+
+muzzle {
+ pass {
+ group = "org.apache.httpcomponents"
+ module = "httpasyncclient"
+ // 4.0 and 4.0.1 don't copy over the traceparent (etc) http headers on redirect
+ versions = "[4.1,)"
+ // TODO implement a muzzle check so that 4.0.x (at least 4.0 and 4.0.1) do not get applied
+ // and then bring back assertInverse
+ }
+}
+
+afterEvaluate{
+ io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator(project,
+ sourceSets.main.get(),
+ "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin",
+ project(":javaagent-tooling").configurations["instrumentationMuzzle"] + configurations.runtimeClasspath
+ ).configure()
+}
+
+val versions: Map by extra
+
+dependencies {
+ api(project(":instrumentation:java-streams"))
+ api(project(":instrumentation:apache-httpclient-4.0"))
+
+ api("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-apache-httpasyncclient-4.1:${versions["opentelemetry_java_agent"]}")
+
+ implementation("org.apache.httpcomponents:httpasyncclient:4.1")
+
+ testImplementation(project(":testing-common"))
+}
+
diff --git a/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/DelegatingRequestAccessor.java b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/DelegatingRequestAccessor.java
new file mode 100644
index 000000000..02d3259a5
--- /dev/null
+++ b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/apachehttpasyncclient/DelegatingRequestAccessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.apachehttpasyncclient;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientInstrumentation.DelegatingRequestProducer;
+
+/**
+ * TODO remove once https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/1951
+ * is merged
+ */
+public class DelegatingRequestAccessor {
+
+ public static Context get(DelegatingRequestProducer delegatingRequestProducer) {
+ return delegatingRequestProducer.context;
+ }
+}
diff --git a/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
new file mode 100644
index 000000000..a5f05a02d
--- /dev/null
+++ b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModule.java
@@ -0,0 +1,170 @@
+/*
+ * 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.apachehttpasyncclient;
+
+import static io.opentelemetry.javaagent.tooling.ClassLoaderMatcher.hasClassesNamed;
+import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.ApacheHttpAsyncClientInstrumentation.DelegatingRequestProducer;
+import io.opentelemetry.javaagent.instrumentation.apachehttpasyncclient.DelegatingRequestAccessor;
+import io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpclient.v4_0.ApacheHttpClientUtils;
+import io.opentelemetry.javaagent.tooling.InstrumentationModule;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.concurrent.FutureCallback;
+import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpCoreContext;
+
+@AutoService(InstrumentationModule.class)
+public class ApacheAsyncClientInstrumentationModule extends InstrumentationModule {
+
+ public ApacheAsyncClientInstrumentationModule() {
+ super(
+ ApacheAsyncHttpClientInstrumentationName.PRIMARY,
+ ApacheAsyncHttpClientInstrumentationName.OTHER);
+ }
+
+ @Override
+ public int getOrder() {
+ return 1;
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return Collections.singletonList(new HttpAsyncClientInstrumentation());
+ }
+
+ class HttpAsyncClientInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed("org.apache.http.nio.client.HttpAsyncClient");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return implementsInterface(named("org.apache.http.nio.client.HttpAsyncClient"));
+ }
+
+ @Override
+ public Map extends ElementMatcher super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isMethod()
+ .and(named("execute"))
+ .and(takesArguments(4))
+ .and(takesArgument(0, named("org.apache.http.nio.protocol.HttpAsyncRequestProducer")))
+ .and(
+ takesArgument(1, named("org.apache.http.nio.protocol.HttpAsyncResponseConsumer")))
+ .and(takesArgument(2, named("org.apache.http.protocol.HttpContext")))
+ .and(takesArgument(3, named("org.apache.http.concurrent.FutureCallback"))),
+ ApacheAsyncClientInstrumentationModule.class.getName()
+ + "$HttpAsyncClient_execute_Advice");
+ }
+ }
+
+ public static class HttpAsyncClient_execute_Advice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void enter(
+ @Advice.Argument(value = 0, readOnly = false) HttpAsyncRequestProducer requestProducer,
+ @Advice.Argument(value = 2) HttpContext httpContext,
+ @Advice.Argument(value = 3, readOnly = false) FutureCallback futureCallback) {
+ if (requestProducer instanceof DelegatingRequestProducer) {
+ DelegatingRequestProducer delegatingRequestProducer =
+ (DelegatingRequestProducer) requestProducer;
+ Context context = DelegatingRequestAccessor.get(delegatingRequestProducer);
+ requestProducer = new DelegatingCaptureBodyRequestProducer(context, requestProducer);
+ futureCallback = new BodyCaptureDelegatingCallback(context, httpContext, futureCallback);
+ }
+ }
+ }
+
+ public static class DelegatingCaptureBodyRequestProducer extends DelegatingRequestProducer {
+
+ final Context context;
+
+ public DelegatingCaptureBodyRequestProducer(
+ Context context, HttpAsyncRequestProducer delegate) {
+ super(context, delegate);
+ this.context = context;
+ }
+
+ @Override
+ public HttpRequest generateRequest() throws IOException, HttpException {
+ HttpRequest request = super.generateRequest();
+ ApacheHttpClientUtils.traceRequest(Span.fromContext(context), request);
+ return request;
+ }
+ }
+
+ public static class BodyCaptureDelegatingCallback implements FutureCallback {
+
+ final Context context;
+ final FutureCallback delegate;
+ final HttpContext httpContext;
+
+ public BodyCaptureDelegatingCallback(
+ Context context, HttpContext httpContext, FutureCallback delegate) {
+ this.delegate = delegate;
+ this.context = context;
+ this.httpContext = httpContext;
+ }
+
+ @Override
+ public void completed(T result) {
+ HttpResponse httpResponse = getResponse(httpContext);
+ ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
+ delegate.completed(result);
+ }
+
+ @Override
+ public void failed(Exception ex) {
+ HttpResponse httpResponse = getResponse(httpContext);
+ ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
+ delegate.failed(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ HttpResponse httpResponse = getResponse(httpContext);
+ ApacheHttpClientUtils.traceResponse(Span.fromContext(context), httpResponse);
+ delegate.cancelled();
+ }
+
+ private static HttpResponse getResponse(HttpContext context) {
+ return (HttpResponse) context.getAttribute(HttpCoreContext.HTTP_RESPONSE);
+ }
+ }
+}
diff --git a/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncHttpClientInstrumentationName.java b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncHttpClientInstrumentationName.java
new file mode 100644
index 000000000..991957496
--- /dev/null
+++ b/instrumentation/apache-httpasyncclient-4.1/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncHttpClientInstrumentationName.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.apachehttpasyncclient;
+
+public class ApacheAsyncHttpClientInstrumentationName {
+
+ public static final String PRIMARY = "apache-httpasyncclient";
+ public static final String[] OTHER = {
+ "apache-httpasyncclient-4.1",
+ "ht",
+ "apache-httpasyncclient-ht",
+ "apache-httpasyncclient-4.1-ht",
+ };
+}
diff --git a/instrumentation/apache-httpasyncclient-4.1/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModuleTest.java b/instrumentation/apache-httpasyncclient-4.1/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModuleTest.java
new file mode 100644
index 000000000..818ffbcc2
--- /dev/null
+++ b/instrumentation/apache-httpasyncclient-4.1/src/test/java/io/opentelemetry/instrumentation/hypertrace/apachehttpasyncclient/ApacheAsyncClientInstrumentationModuleTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright The Hypertrace Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.instrumentation.hypertrace.apachehttpasyncclient;
+
+import io.opentelemetry.sdk.trace.data.SpanData;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeoutException;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.concurrent.FutureCallback;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.apache.http.impl.nio.client.HttpAsyncClients;
+import org.apache.http.message.BasicHeader;
+import org.hypertrace.agent.core.HypertraceSemanticAttributes;
+import org.hypertrace.agent.testing.AbstractInstrumenterTest;
+import org.hypertrace.agent.testing.TestHttpServer;
+import org.hypertrace.agent.testing.TestHttpServer.GetJsonHandler;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class ApacheAsyncClientInstrumentationModuleTest extends AbstractInstrumenterTest {
+
+ private static final String JSON = "{\"id\":1,\"name\":\"John\"}";
+ private static final TestHttpServer testHttpServer = new TestHttpServer();
+
+ private static final CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
+
+ @BeforeAll
+ public static void startServer() throws Exception {
+ testHttpServer.start();
+ client.start();
+ }
+
+ @AfterAll
+ public static void closeServer() throws Exception {
+ testHttpServer.close();
+ }
+
+ @Test
+ public void getJson()
+ throws ExecutionException, InterruptedException, TimeoutException, IOException {
+ HttpGet getRequest =
+ new HttpGet(String.format("http://localhost:%s/get_json", testHttpServer.port()));
+ getRequest.addHeader("foo", "bar");
+ Future futureResponse = client.execute(getRequest, new NoopFutureCallback());
+
+ HttpResponse response = futureResponse.get();
+ Assertions.assertEquals(200, response.getStatusLine().getStatusCode());
+ String responseBody = readInputStream(response.getEntity().getContent());
+ Assertions.assertEquals(GetJsonHandler.RESPONSE_BODY, responseBody);
+
+ TEST_WRITER.waitForTraces(1);
+ List> traces = TEST_WRITER.getTraces();
+ Assertions.assertEquals(1, traces.size());
+ Assertions.assertEquals(2, traces.get(0).size());
+ SpanData clientSpan = traces.get(0).get(0);
+
+ Assertions.assertEquals(
+ "test-value",
+ clientSpan
+ .getAttributes()
+ .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header")));
+ Assertions.assertEquals(
+ "bar",
+ clientSpan.getAttributes().get(HypertraceSemanticAttributes.httpRequestHeader("foo")));
+ Assertions.assertNull(
+ clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
+ SpanData responseBodySpan = traces.get(0).get(1);
+ Assertions.assertEquals(
+ GetJsonHandler.RESPONSE_BODY,
+ responseBodySpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
+ }
+
+ @Test
+ public void postJson()
+ throws IOException, TimeoutException, InterruptedException, ExecutionException {
+ StringEntity entity =
+ new StringEntity(JSON, ContentType.create(ContentType.APPLICATION_JSON.getMimeType()));
+ postJsonEntity(entity);
+ }
+
+ @Test
+ public void postJsonNonRepeatableEntity()
+ throws IOException, TimeoutException, InterruptedException, ExecutionException {
+ StringEntity entity = new NonRepeatableStringEntity(JSON);
+ postJsonEntity(entity);
+ }
+
+ public void postJsonEntity(HttpEntity entity)
+ throws TimeoutException, InterruptedException, IOException, ExecutionException {
+ HttpPost postRequest = new HttpPost();
+ postRequest.setEntity(entity);
+ postRequest.setHeader("Content-type", "application/json");
+ postRequest.setURI(
+ URI.create(String.format("http://localhost:%d/post", testHttpServer.port())));
+
+ Future responseFuture = client.execute(postRequest, new NoopFutureCallback());
+
+ HttpResponse response = responseFuture.get();
+ Assertions.assertEquals(204, response.getStatusLine().getStatusCode());
+
+ 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);
+
+ String requestBody = readInputStream(entity.getContent());
+ Assertions.assertEquals(
+ "test-value",
+ clientSpan
+ .getAttributes()
+ .get(HypertraceSemanticAttributes.httpResponseHeader("test-response-header")));
+ Assertions.assertEquals(
+ requestBody,
+ clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY));
+ Assertions.assertNull(
+ clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY));
+ }
+
+ private static String readInputStream(InputStream inputStream) throws IOException {
+ StringBuilder textBuilder = new StringBuilder();
+
+ try (BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
+ int c;
+ while ((c = reader.read()) != -1) {
+ textBuilder.append((char) c);
+ }
+ }
+ return textBuilder.toString();
+ }
+
+ class NonRepeatableStringEntity extends StringEntity {
+
+ public NonRepeatableStringEntity(String s) throws UnsupportedEncodingException {
+ super(s);
+ }
+
+ @Override
+ public Header getContentType() {
+ return new BasicHeader("Content-Type", "json");
+ }
+
+ @Override
+ public boolean isRepeatable() {
+ return false;
+ }
+
+ @Override
+ public InputStream getContent() {
+ return new TestInputStream(this.content);
+ }
+ }
+
+ // TODO remove once https://github.com/hypertrace/javaagent/issues/189 is fixed
+ static class TestInputStream extends ByteArrayInputStream {
+
+ public TestInputStream(byte[] buf) {
+ super(buf);
+ }
+
+ @Override
+ public synchronized int read() {
+ return super.read();
+ }
+
+ @Override
+ public int read(@NotNull byte[] b) throws IOException {
+ return super.read(b);
+ }
+
+ @Override
+ public synchronized int read(byte[] b, int off, int len) {
+ return super.read(b, off, len);
+ }
+
+ @Override
+ public synchronized int available() {
+ return super.available();
+ }
+ }
+
+ static class NoopFutureCallback implements FutureCallback {
+ @Override
+ public void completed(HttpResponse result) {}
+
+ @Override
+ public void failed(Exception ex) {}
+
+ @Override
+ public void cancelled() {}
+ }
+}
diff --git a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheClientInstrumentationModule.java b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheClientInstrumentationModule.java
index d589a8666..32a93d130 100644
--- a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheClientInstrumentationModule.java
+++ b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheClientInstrumentationModule.java
@@ -18,28 +18,17 @@
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
-import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
-import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
-import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import com.google.auto.service.AutoService;
-import io.opentelemetry.api.trace.Span;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
-import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
-import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -48,19 +37,8 @@
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
-import org.hypertrace.agent.config.Config.AgentConfig;
-import org.hypertrace.agent.core.BoundedByteArrayOutputStreamFactory;
-import org.hypertrace.agent.core.ContentEncodingUtils;
-import org.hypertrace.agent.core.ContentLengthUtils;
-import org.hypertrace.agent.core.ContentTypeUtils;
-import org.hypertrace.agent.core.GlobalObjectRegistry;
-import org.hypertrace.agent.core.GlobalObjectRegistry.SpanAndBuffer;
-import org.hypertrace.agent.core.HypertraceConfig;
-import org.hypertrace.agent.core.HypertraceSemanticAttributes;
@AutoService(InstrumentationModule.class)
public class ApacheClientInstrumentationModule extends InstrumentationModule {
@@ -74,13 +52,6 @@ public int getOrder() {
return 1;
}
- @Override
- protected Map contextStore() {
- Map context = new HashMap<>();
- context.put("org.apache.http.HttpEntity", Span.class.getName());
- return context;
- }
-
@Override
public List typeInstrumentations() {
return Arrays.asList(new HttpEntityInstrumentation(), new ApacheClientInstrumentation());
@@ -127,9 +98,7 @@ public static boolean enter(@Advice.Argument(0) HttpMessage request) {
if (callDepth > 0) {
return false;
}
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- ApacheHttpClientUtils.traceRequest(contextStore, request);
+ ApacheHttpClientUtils.traceRequest(Java8BytecodeBridge.currentSpan(), request);
return true;
}
@@ -149,9 +118,7 @@ public static boolean enter(@Advice.Argument(1) HttpMessage request) {
if (callDepth > 0) {
return false;
}
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- ApacheHttpClientUtils.traceRequest(contextStore, request);
+ ApacheHttpClientUtils.traceRequest(Java8BytecodeBridge.currentSpan(), request);
return true;
}
@@ -183,131 +150,7 @@ public static void exit(@Advice.Return Object response, @Advice.Enter boolean re
CallDepthThreadLocalMap.reset(HttpResponse.class);
if (response instanceof HttpResponse) {
HttpResponse httpResponse = (HttpResponse) response;
- Span currentSpan = Java8BytecodeBridge.currentSpan();
- AgentConfig agentConfig = HypertraceConfig.get();
- if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) {
- ApacheHttpClientUtils.addResponseHeaders(currentSpan, httpResponse.headerIterator());
- }
-
- if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) {
- HttpEntity entity = httpResponse.getEntity();
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- ApacheHttpClientUtils.traceEntity(
- contextStore,
- currentSpan,
- HypertraceSemanticAttributes.HTTP_RESPONSE_BODY.getKey(),
- entity);
- }
- }
- }
- }
-
- static class HttpEntityInstrumentation implements TypeInstrumentation {
- @Override
- public ElementMatcher super TypeDescription> typeMatcher() {
- return implementsInterface(named("org.apache.http.HttpEntity"));
- }
-
- @Override
- public Map extends ElementMatcher super MethodDescription>, String> transformers() {
- Map, String> transformers = new HashMap<>();
-
- // instrumentation for request body along with OutputStream instrumentation
- transformers.put(
- named("writeTo").and(takesArguments(1)).and(takesArgument(0, is(OutputStream.class))),
- ApacheClientInstrumentationModule.class.getName() + "$HttpEntity_WriteToAdvice");
-
- // instrumentation for response body along with InputStream instrumentation
- transformers.put(
- named("getContent").and(takesArguments(0)).and(returns(InputStream.class)),
- ApacheClientInstrumentationModule.class.getName() + "$HttpEntity_GetContentAdvice");
- return transformers;
- }
- }
-
- static class HttpEntity_GetContentAdvice {
-
- @Advice.OnMethodExit(suppress = Throwable.class)
- public static void exit(@Advice.This HttpEntity thizz, @Advice.Return InputStream inputStream) {
- // here the Span.current() is finished for response entities
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- Span clientSpan = contextStore.get(thizz);
- // HttpEntity might be wrapped multiple times
- // this ensures that the advice runs only for the most outer one
- // the returned inputStream is put into globally accessible map
- // The InputStream instrumentation then checks if the input stream is in the map and only
- // then intercepts the reads.
- if (clientSpan == null) {
- return;
- }
-
- Header contentType = thizz.getContentType();
- if (contentType == null || !ContentTypeUtils.shouldCapture(contentType.getValue())) {
- return;
- }
-
- long contentSize = thizz.getContentLength();
- if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
- contentSize = ContentLengthUtils.DEFAULT;
- }
-
- String encoding =
- thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
- Charset charset = ContentEncodingUtils.toCharset(encoding);
- SpanAndBuffer spanAndBuffer =
- new SpanAndBuffer(
- clientSpan,
- BoundedByteArrayOutputStreamFactory.create((int) contentSize),
- HypertraceSemanticAttributes.HTTP_RESPONSE_BODY,
- charset);
- GlobalObjectRegistry.inputStreamToSpanAndBufferMap.put(inputStream, spanAndBuffer);
- }
- }
-
- static class HttpEntity_WriteToAdvice {
- @Advice.OnMethodEnter(suppress = Throwable.class)
- public static void enter(
- @Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- if (contextStore.get(thizz) == null) {
- return;
- }
-
- long contentSize = thizz.getContentLength();
- if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
- contentSize = ContentLengthUtils.DEFAULT;
- }
- ByteArrayOutputStream byteArrayOutputStream =
- BoundedByteArrayOutputStreamFactory.create((int) contentSize);
-
- GlobalObjectRegistry.outputStreamToBufferMap.put(outputStream, byteArrayOutputStream);
- }
-
- @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
- public static void exit(
- @Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
- ContextStore contextStore =
- InstrumentationContext.get(HttpEntity.class, Span.class);
- Span clientSpan = contextStore.get(thizz);
- if (clientSpan == null) {
- return;
- }
-
- String encoding =
- thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
- Charset charset = ContentEncodingUtils.toCharset(encoding);
-
- ByteArrayOutputStream bufferedOutStream =
- GlobalObjectRegistry.outputStreamToBufferMap.remove(outputStream);
- try {
- String requestBody = bufferedOutStream.toString(charset.name());
- System.out.printf("Captured request body via outputstream: %s\n", requestBody);
- clientSpan.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, requestBody);
- } catch (UnsupportedEncodingException e) {
- // should not happen, the charset has been parsed before
+ ApacheHttpClientUtils.traceResponse(Java8BytecodeBridge.currentSpan(), httpResponse);
}
}
}
diff --git a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientObjectRegistry.java b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientObjectRegistry.java
new file mode 100644
index 000000000..2819f23e0
--- /dev/null
+++ b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientObjectRegistry.java
@@ -0,0 +1,38 @@
+/*
+ * 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.apachehttpclient.v4_0;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.javaagent.instrumentation.api.WeakMap;
+import org.apache.http.HttpEntity;
+
+public class ApacheHttpClientObjectRegistry {
+
+ public static final WeakMap entityToSpan =
+ WeakMap.Provider.newWeakMap();
+
+ public static class SpanAndAttributeKey {
+ public final Span span;
+ public final AttributeKey attributeKey;
+
+ public SpanAndAttributeKey(Span span, AttributeKey attributeKey) {
+ this.span = span;
+ this.attributeKey = attributeKey;
+ }
+ }
+}
diff --git a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientUtils.java b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientUtils.java
index 7d834bdac..dab4105cf 100644
--- a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientUtils.java
+++ b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientUtils.java
@@ -18,7 +18,7 @@
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
+import io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpclient.v4_0.ApacheHttpClientObjectRegistry.SpanAndAttributeKey;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -29,9 +29,11 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpMessage;
+import org.apache.http.HttpResponse;
import org.hypertrace.agent.config.Config.AgentConfig;
import org.hypertrace.agent.core.BoundedByteArrayOutputStreamFactory;
import org.hypertrace.agent.core.ContentEncodingUtils;
+import org.hypertrace.agent.core.ContentTypeUtils;
import org.hypertrace.agent.core.HypertraceConfig;
import org.hypertrace.agent.core.HypertraceSemanticAttributes;
import org.slf4j.Logger;
@@ -60,12 +62,10 @@ private static void addHeaders(
}
}
- public static void traceRequest(
- ContextStore contextStore, HttpMessage request) {
- Span currentSpan = Span.current();
+ public static void traceRequest(Span span, HttpMessage request) {
AgentConfig agentConfig = HypertraceConfig.get();
if (agentConfig.getDataCapture().getHttpHeaders().getRequest().getValue()) {
- ApacheHttpClientUtils.addRequestHeaders(currentSpan, request.headerIterator());
+ ApacheHttpClientUtils.addRequestHeaders(span, request.headerIterator());
}
if (agentConfig.getDataCapture().getHttpBody().getRequest().getValue()
@@ -73,22 +73,34 @@ public static void traceRequest(
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
HttpEntity entity = entityRequest.getEntity();
ApacheHttpClientUtils.traceEntity(
- contextStore,
- currentSpan,
- HypertraceSemanticAttributes.HTTP_REQUEST_BODY.getKey(),
- entity);
+ span, HypertraceSemanticAttributes.HTTP_REQUEST_BODY, entity);
+ }
+ }
+
+ public static void traceResponse(Span span, HttpResponse response) {
+ AgentConfig agentConfig = HypertraceConfig.get();
+ if (agentConfig.getDataCapture().getHttpHeaders().getResponse().getValue()) {
+ ApacheHttpClientUtils.addResponseHeaders(span, response.headerIterator());
+ }
+
+ if (agentConfig.getDataCapture().getHttpBody().getResponse().getValue()) {
+ HttpEntity entity = response.getEntity();
+ ApacheHttpClientUtils.traceEntity(
+ span, HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, entity);
}
}
public static void traceEntity(
- ContextStore contextStore,
- Span span,
- String bodyAttributeKey,
- HttpEntity entity) {
+ Span span, AttributeKey bodyAttributeKey, HttpEntity entity) {
if (entity == null) {
return;
}
+ Header contentType = entity.getContentType();
+ if (contentType == null || !ContentTypeUtils.shouldCapture(contentType.getValue())) {
+ return;
+ }
+
if (entity.isRepeatable()) {
try {
ByteArrayOutputStream byteArrayOutputStream = BoundedByteArrayOutputStreamFactory.create();
@@ -99,7 +111,6 @@ public static void traceEntity(
Charset charset = ContentEncodingUtils.toCharset(encoding);
try {
String body = byteArrayOutputStream.toString(charset.name());
- System.out.printf("captured %s readable body is %s\n", bodyAttributeKey, body);
span.setAttribute(bodyAttributeKey, body);
} catch (UnsupportedEncodingException e) {
log.error("Could not parse charset from encoding {}", encoding, e);
@@ -111,9 +122,14 @@ public static void traceEntity(
return;
}
- // request body is traced via HttpEntity.writeTo(OutputStream) and OutputStream instrumentation
+ // sync client: request body is traced via HttpEntity.writeTo(OutputStream) and OutputStream
+ // instrumentation
+ // async client: request body is traced via InputStream HttpEntity.getContent() and InputStream
+ // instrumentation
+
// response body is traced via InputStream HttpEntity.getContent() and InputStream
// instrumentation
- contextStore.put(entity, span);
+ ApacheHttpClientObjectRegistry.entityToSpan.put(
+ entity, new SpanAndAttributeKey(span, bodyAttributeKey));
}
}
diff --git a/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/HttpEntityInstrumentation.java b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/HttpEntityInstrumentation.java
new file mode 100644
index 000000000..e8a90e5b5
--- /dev/null
+++ b/instrumentation/apache-httpclient-4.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/HttpEntityInstrumentation.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.apachehttpclient.v4_0;
+
+import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
+import static net.bytebuddy.matcher.ElementMatchers.is;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.returns;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.javaagent.instrumentation.hypertrace.apachehttpclient.v4_0.ApacheHttpClientObjectRegistry.SpanAndAttributeKey;
+import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.http.HttpEntity;
+import org.hypertrace.agent.core.BoundedByteArrayOutputStreamFactory;
+import org.hypertrace.agent.core.ContentEncodingUtils;
+import org.hypertrace.agent.core.ContentLengthUtils;
+import org.hypertrace.agent.core.GlobalObjectRegistry;
+import org.hypertrace.agent.core.GlobalObjectRegistry.SpanAndBuffer;
+
+public class HttpEntityInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher super TypeDescription> typeMatcher() {
+ return implementsInterface(named("org.apache.http.HttpEntity"));
+ }
+
+ @Override
+ public Map extends ElementMatcher super MethodDescription>, String> transformers() {
+ Map, String> transformers = new HashMap<>();
+
+ // instrumentation for request body along with OutputStream instrumentation
+ transformers.put(
+ named("writeTo").and(takesArguments(1)).and(takesArgument(0, is(OutputStream.class))),
+ HttpEntityInstrumentation.class.getName() + "$HttpEntity_WriteToAdvice");
+
+ // instrumentation for response body along with InputStream instrumentation
+ transformers.put(
+ named("getContent").and(takesArguments(0)).and(returns(InputStream.class)),
+ HttpEntityInstrumentation.class.getName() + "$HttpEntity_GetContentAdvice");
+ return transformers;
+ }
+
+ static class HttpEntity_GetContentAdvice {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void exit(@Advice.This HttpEntity thizz, @Advice.Return InputStream inputStream) {
+ // here the Span.current() is finished for response entities
+ // TODO the entry from map is nto explicitly removed, It could be done by instrumenting
+ // CloseableHttpResponse
+ // instroduced in version 4.3
+ SpanAndAttributeKey clientSpan = ApacheHttpClientObjectRegistry.entityToSpan.get(thizz);
+ // HttpEntity might be wrapped multiple times
+ // this ensures that the advice runs only for the most outer one
+ // the returned inputStream is put into globally accessible map
+ // The InputStream instrumentation then checks if the input stream is in the map and only
+ // then intercepts the reads.
+ if (clientSpan == null) {
+ return;
+ }
+
+ long contentSize = thizz.getContentLength();
+ if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
+ contentSize = ContentLengthUtils.DEFAULT;
+ }
+
+ String encoding =
+ thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
+ Charset charset = ContentEncodingUtils.toCharset(encoding);
+ SpanAndBuffer spanAndBuffer =
+ new SpanAndBuffer(
+ clientSpan.span,
+ BoundedByteArrayOutputStreamFactory.create((int) contentSize),
+ clientSpan.attributeKey,
+ charset);
+ GlobalObjectRegistry.inputStreamToSpanAndBufferMap.put(inputStream, spanAndBuffer);
+ }
+ }
+
+ static class HttpEntity_WriteToAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void enter(
+ @Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
+
+ if (ApacheHttpClientObjectRegistry.entityToSpan.get(thizz) == null) {
+ return;
+ }
+
+ long contentSize = thizz.getContentLength();
+ if (contentSize <= 0 || contentSize == Long.MAX_VALUE) {
+ contentSize = ContentLengthUtils.DEFAULT;
+ }
+ ByteArrayOutputStream byteArrayOutputStream =
+ BoundedByteArrayOutputStreamFactory.create((int) contentSize);
+
+ GlobalObjectRegistry.outputStreamToBufferMap.put(outputStream, byteArrayOutputStream);
+ }
+
+ @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
+ public static void exit(
+ @Advice.This HttpEntity thizz, @Advice.Argument(0) OutputStream outputStream) {
+ SpanAndAttributeKey spanAndAttributeKey =
+ ApacheHttpClientObjectRegistry.entityToSpan.remove(thizz);
+ if (spanAndAttributeKey == null) {
+ return;
+ }
+
+ String encoding =
+ thizz.getContentEncoding() != null ? thizz.getContentEncoding().getValue() : "";
+ Charset charset = ContentEncodingUtils.toCharset(encoding);
+
+ ByteArrayOutputStream bufferedOutStream =
+ GlobalObjectRegistry.outputStreamToBufferMap.remove(outputStream);
+ try {
+ String requestBody = bufferedOutStream.toString(charset.name());
+ spanAndAttributeKey.span.setAttribute(spanAndAttributeKey.attributeKey, requestBody);
+ } catch (UnsupportedEncodingException e) {
+ // should not happen, the charset has been parsed before
+ }
+ }
+ }
+}
diff --git a/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java b/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java
index 0116ba491..d2d710e78 100644
--- a/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java
+++ b/instrumentation/apache-httpclient-4.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/apachehttpclient/v4_0/ApacheHttpClientInstrumentationTest.java
@@ -22,7 +22,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
@@ -136,6 +135,7 @@ public void postUrlEncoded() throws IOException, TimeoutException, InterruptedEx
@Test
public void postJson() throws IOException, TimeoutException, InterruptedException {
StringEntity entity = new StringEntity(JSON);
+ entity.setContentType("application/json");
postJsonEntity(entity);
}
@@ -143,6 +143,7 @@ public void postJson() throws IOException, TimeoutException, InterruptedExceptio
public void postJsonNonRepeatableEntity()
throws IOException, TimeoutException, InterruptedException {
StringEntity entity = new NonRepeatableStringEntity(JSON);
+ entity.setContentType("application/json");
postJsonEntity(entity);
}
@@ -214,15 +215,5 @@ public NonRepeatableStringEntity(String s) throws UnsupportedEncodingException {
public boolean isRepeatable() {
return false;
}
-
- @Override
- public InputStream getContent() throws IOException {
- return super.getContent();
- }
-
- @Override
- public void writeTo(OutputStream outstream) throws IOException {
- super.writeTo(outstream);
- }
}
}
diff --git a/instrumentation/build.gradle.kts b/instrumentation/build.gradle.kts
index 802092374..de8dd6473 100644
--- a/instrumentation/build.gradle.kts
+++ b/instrumentation/build.gradle.kts
@@ -39,6 +39,7 @@ dependencies{
implementation(project(":instrumentation:apache-httpclient-4.0"))
implementation(project(":instrumentation:jaxrs-client-2.0"))
implementation(project(":instrumentation:java-streams"))
+ implementation(project(":instrumentation:apache-httpasyncclient-4.1"))
implementation(project(":otel-extensions"))
}
diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java
index 2ff980ec6..d9152ad95 100644
--- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java
+++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamInstrumentationModule.java
@@ -103,6 +103,9 @@ public Map extends ElementMatcher super MethodDescription>, String> transfor
.and(takesArgument(2, is(int.class)))
.and(isPublic()),
InputStreamInstrumentationModule.class.getName() + "$InputStream_ReadNBytes");
+ transformers.put(
+ named("available").and(takesArguments(0)).and(isPublic()),
+ InputStreamInstrumentationModule.class.getName() + "$InputStream_Available");
return transformers;
}
}
@@ -197,4 +200,11 @@ public static void exit(
InputStreamUtils.readNBytes(thizz, spanAndBuffer, read, b, off, len);
}
}
+
+ public static class InputStream_Available {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void exit(@Advice.This InputStream thizz, @Advice.Return int available) {
+ InputStreamUtils.available(thizz, available);
+ }
+ }
}
diff --git a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java
index 386aac8ef..eead108b9 100644
--- a/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java
+++ b/instrumentation/java-streams/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/java/inputstream/InputStreamUtils.java
@@ -47,7 +47,6 @@ private InputStreamUtils() {}
* child.
*/
public static void addAttribute(Span span, AttributeKey attributeKey, String value) {
- System.out.printf("Captured %s attribute: %s\n", attributeKey.getKey(), value);
if (span.isRecording()) {
span.setAttribute(attributeKey, value);
} else {
@@ -149,4 +148,18 @@ public static void readNBytes(
}
CallDepthThreadLocalMap.reset(InputStream.class);
}
+
+ public static void available(InputStream inputStream, int available) {
+ if (available != 0) {
+ return;
+ }
+ SpanAndBuffer spanAndBuffer =
+ GlobalObjectRegistry.inputStreamToSpanAndBufferMap.get(inputStream);
+ InputStreamUtils.addBody(
+ spanAndBuffer.span,
+ spanAndBuffer.attributeKey,
+ spanAndBuffer.byteArrayBuffer,
+ spanAndBuffer.charset);
+ GlobalObjectRegistry.inputStreamToSpanAndBufferMap.remove(inputStream);
+ }
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a7eff6ddb..70416eae5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -43,3 +43,5 @@ include("instrumentation:jaxrs-client-2.0")
findProject(":instrumentation:jaxrs-client-2.0")?.name = "jaxrs-client-2.0"
include("instrumentation:java-streams")
findProject(":instrumentation:java-streams")?.name = "java-streams"
+include("instrumentation:apache-httpasyncclient-4.1")
+findProject(":instrumentation:apache-httpasyncclient-4.1")?.name = "apache-httpasyncclient-4.1"
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 f5019eb8e..1aa5bf26d 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
@@ -122,7 +122,7 @@ public void handle(
while ((nRead = inputStream.read()) != -1) {
buffer.write((byte) nRead);
}
- System.out.printf("Received: %s\n", buffer.toString());
+ System.out.printf("Test server received: %s\n", buffer.toString());
response.setStatus(204);
baseRequest.setHandled(true);