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, 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 typeMatcher() { - return implementsInterface(named("org.apache.http.HttpEntity")); - } - - @Override - public Map, 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 typeMatcher() { + return implementsInterface(named("org.apache.http.HttpEntity")); + } + + @Override + public Map, 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, 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);