diff --git a/README.md b/README.md index 5345751a0..8c0a413cc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ List of supported frameworks with additional capabilities: | [Servlet](https://javaee.github.io/javaee-spec/javadocs/javax/servlet/package-summary.html) | 2.3+ | | [Spark Web Framework](https://github.com/perwendel/spark) | 2.3+ | | [Spring Webflux](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/package-summary.html) | 5.0+ | -| [Vert.x](https://vertx.io) | 3.0+ | +| [Vert.x](https://vertx.io) | 3.0+ (4 not supported yet) | | [Struts](https://struts.apache.org/) | 2.3+ | ### Adding custom filter implementation diff --git a/instrumentation/vertx-web-3.0/build.gradle.kts b/instrumentation/vertx-web-3.0/build.gradle.kts index 120fb52b5..8f296ad9d 100644 --- a/instrumentation/vertx-web-3.0/build.gradle.kts +++ b/instrumentation/vertx-web-3.0/build.gradle.kts @@ -2,6 +2,7 @@ plugins { `java-library` id("net.bytebuddy.byte-buddy") id("io.opentelemetry.instrumentation.auto-instrumentation") + muzzle } afterEvaluate{ @@ -12,17 +13,25 @@ afterEvaluate{ ).configure() } +muzzle { + pass { + group = "io.vertx" + module = "vertx-web" + versions = "[3.0.0,4.0.0)" + } +} + val versions: Map by extra // version used by io.vertx:vertx-web:3.0.0 val nettyVersion = "4.0.28.Final" dependencies { + implementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-vertx-web-3.0:${versions["opentelemetry_java_agent"]}") + implementation("io.vertx:vertx-web:3.0.0") + testImplementation(project(":testing-common")) testImplementation(project(":instrumentation:netty:netty-4.0")) testImplementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-netty-4.0:${versions["opentelemetry_java_agent"]}") - testImplementation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-vertx-web-3.0:${versions["opentelemetry_java_agent"]}") - testImplementation("io.vertx:vertx-web:3.0.0") - testImplementation("io.netty:netty-codec-http:${nettyVersion}") { version { diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestHandleInstrumentation.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestHandleInstrumentation.java new file mode 100644 index 000000000..46e7e7d75 --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestHandleInstrumentation.java @@ -0,0 +1,96 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.instrumentation.vertx.client.Contexts; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +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.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpRequestHandleInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.core.http.HttpClientRequest"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.core.http.HttpClientRequest")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + + // capture response data + transformers.put( + isMethod().and(named("handleResponse")), + HttpRequestHandleInstrumentation.class.getName() + "$HandleResponseAdvice"); + return transformers; + } + + public static class HandleResponseAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleResponseEnter( + @Advice.This HttpClientRequest request, @Advice.Argument(0) HttpClientResponse response) { + + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + if (contexts == null) { + return; + } + Span span = Span.fromContext(contexts.context); + + if (HypertraceConfig.get().getDataCapture().getHttpHeaders().getRequest().getValue()) { + for (Map.Entry entry : request.headers()) { + span.setAttribute( + HypertraceSemanticAttributes.httpRequestHeader(entry.getKey()), entry.getValue()); + } + } + + if (HypertraceConfig.get().getDataCapture().getHttpHeaders().getResponse().getValue()) { + for (Map.Entry entry : response.headers()) { + span.setAttribute( + HypertraceSemanticAttributes.httpResponseHeader(entry.getKey()), entry.getValue()); + } + } + + String contentType = response.getHeader("Content-Type"); + if (HypertraceConfig.get().getDataCapture().getHttpBody().getResponse().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + InstrumentationContext.get(HttpClientResponse.class, Span.class).put(response, span); + } + } + } +} diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestInstrumentation.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestInstrumentation.java new file mode 100644 index 000000000..d15972b46 --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpRequestInstrumentation.java @@ -0,0 +1,261 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.is; +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 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.vertx.client.Contexts; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +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.hypertrace.agent.core.config.HypertraceConfig; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedBuffersFactory; +import org.hypertrace.agent.core.instrumentation.buffer.BoundedCharArrayWriter; +import org.hypertrace.agent.core.instrumentation.utils.ContentTypeUtils; + +public class HttpRequestInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.core.http.HttpClientRequest"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.core.http.HttpClientRequest")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + + transformers.put( + isMethod().and(named("write").and(takesArgument(0, is(String.class)))), + HttpRequestInstrumentation.class.getName() + "$WriteRequestAdvice_string"); + transformers.put( + isMethod() + .and(named("write").and(takesArguments(1)).and(takesArgument(0, is(Buffer.class)))), + HttpRequestInstrumentation.class.getName() + "$WriteRequestAdvice_buffer"); + + transformers.put( + isMethod().and(named("end").and(takesArguments(0))), + HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice"); + transformers.put( + isMethod().and(named("end").and(takesArgument(0, is(String.class)))), + HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice_string"); + transformers.put( + isMethod().and(named("end").and(takesArguments(1)).and(takesArgument(0, is(Buffer.class)))), + HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice_buffer"); + + return transformers; + } + + public static class EndRequestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter(@Advice.This HttpClientRequest request) { + + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClientRequest.class); + if (callDepth > 0) { + return; + } + + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + if (contexts == null) { + return; + } + Span span = Span.fromContext(contexts.context); + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(MultiMap.class, BoundedCharArrayWriter.class) + .get(request.headers()); + if (buffer != null) { + span.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, buffer.toString()); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit() { + CallDepthThreadLocalMap.decrementCallDepth(HttpClientRequest.class); + } + } + + public static class EndRequestAdvice_string { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.This HttpClientRequest request, @Advice.Argument(0) String chunk) + throws IOException { + + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClientRequest.class); + if (callDepth > 0) { + return; + } + + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + if (contexts == null) { + return; + } + Span span = Span.fromContext(contexts.context); + + String contentType = request.headers().get("Content-Type"); + if (HypertraceConfig.get().getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + BoundedCharArrayWriter buffer = + InstrumentationContext.get(MultiMap.class, BoundedCharArrayWriter.class) + .get(request.headers()); + if (buffer == null) { + span.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, chunk); + } else { + buffer.write(chunk); + span.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, buffer.toString()); + } + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit() { + CallDepthThreadLocalMap.decrementCallDepth(HttpClientRequest.class); + } + } + + public static class EndRequestAdvice_buffer { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.This HttpClientRequest request, @Advice.Argument(0) Buffer chunk) + throws IOException { + + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClientRequest.class); + if (callDepth > 0) { + return; + } + + Contexts contexts = + InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request); + if (contexts == null) { + return; + } + Span span = Span.fromContext(contexts.context); + + String contentType = request.headers().get("Content-Type"); + if (HypertraceConfig.get().getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + + BoundedCharArrayWriter buffer = + InstrumentationContext.get(MultiMap.class, BoundedCharArrayWriter.class) + .get(request.headers()); + if (buffer == null) { + span.setAttribute( + HypertraceSemanticAttributes.HTTP_REQUEST_BODY, + chunk.toString(StandardCharsets.UTF_8.name())); + } else { + buffer.write(chunk.toString(StandardCharsets.UTF_8.name())); + span.setAttribute(HypertraceSemanticAttributes.HTTP_REQUEST_BODY, buffer.toString()); + } + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit() { + CallDepthThreadLocalMap.decrementCallDepth(HttpClientRequest.class); + } + } + + public static class WriteRequestAdvice_string { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.This HttpClientRequest request, @Advice.Argument(0) String chunk) + throws IOException { + + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClientRequest.class); + if (callDepth > 0) { + return; + } + + String contentType = request.headers().get("Content-Type"); + if (HypertraceConfig.get().getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + + ContextStore contextStore = + InstrumentationContext.get(MultiMap.class, BoundedCharArrayWriter.class); + BoundedCharArrayWriter buffer = contextStore.get(request.headers()); + if (buffer == null) { + buffer = BoundedBuffersFactory.createWriter(); + contextStore.put(request.headers(), buffer); + } + buffer.write(chunk); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit() { + CallDepthThreadLocalMap.decrementCallDepth(HttpClientRequest.class); + } + } + + public static class WriteRequestAdvice_buffer { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void enter( + @Advice.This HttpClientRequest request, @Advice.Argument(0) Buffer chunk) + throws IOException { + + int callDepth = CallDepthThreadLocalMap.incrementCallDepth(HttpClientRequest.class); + if (callDepth > 0) { + return; + } + + String contentType = request.headers().get("Content-Type"); + if (HypertraceConfig.get().getDataCapture().getHttpBody().getRequest().getValue() + && ContentTypeUtils.shouldCapture(contentType)) { + + ContextStore contextStore = + InstrumentationContext.get(MultiMap.class, BoundedCharArrayWriter.class); + BoundedCharArrayWriter buffer = contextStore.get(request.headers()); + if (buffer == null) { + buffer = BoundedBuffersFactory.createWriter(); + contextStore.put(request.headers(), buffer); + } + buffer.write(chunk.toString(StandardCharsets.UTF_8.name())); + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit() { + CallDepthThreadLocalMap.decrementCallDepth(HttpClientRequest.class); + } + } +} diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpResponseInstrumentation.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpResponseInstrumentation.java new file mode 100644 index 000000000..2b1025340 --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/HttpResponseInstrumentation.java @@ -0,0 +1,76 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientResponse; +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; + +public class HttpResponseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.core.http.HttpClientResponse"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.core.http.HttpClientResponse")); + } + + @Override + public Map, String> transformers() { + Map, String> transformers = new HashMap<>(); + + transformers.put( + isMethod().and(named("bodyHandler").and(takesArguments(1))), + HttpResponseInstrumentation.class.getName() + "$ResponseBodyHandler"); + return transformers; + } + + public static class ResponseBodyHandler { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleResponseEnter( + @Advice.This HttpClientResponse response, + @Advice.Argument(value = 0, readOnly = false) Handler handler) { + + Span span = InstrumentationContext.get(HttpClientResponse.class, Span.class).get(response); + if (span == null) { + // request not traced - e.g. wrong content type + return; + } + InstrumentationContext.get(HttpClientResponse.class, Span.class).put(response, null); + + handler = new ResponseBodyWrappingHandler(handler, span); + } + } +} diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java new file mode 100644 index 000000000..6cb72287d --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/ResponseBodyWrappingHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; + +public class ResponseBodyWrappingHandler implements Handler { + + private static final Tracer tracer = + GlobalOpenTelemetry.getTracer("io.opentelemetry.javaagent.vertx-core-3.0"); + + private final Handler wrapped; + private final Span span; + + public ResponseBodyWrappingHandler(Handler wrapped, Span span) { + this.wrapped = wrapped; + this.span = span; + } + + @Override + public void handle(Buffer event) { + String responseBody = event.getString(0, event.length()); + if (span.isRecording()) { + span.setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, responseBody); + } else { + tracer + .spanBuilder(HypertraceSemanticAttributes.ADDITIONAL_DATA_SPAN_NAME) + .setParent(Context.root().with(span)) + .setAttribute(HypertraceSemanticAttributes.HTTP_RESPONSE_BODY, responseBody) + .startSpan() + .end(); + } + + wrapped.handle(event); + } +} diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxHandleResponseInstrumentationModule.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxHandleResponseInstrumentationModule.java new file mode 100644 index 000000000..6cfaa8eed --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxHandleResponseInstrumentationModule.java @@ -0,0 +1,43 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxHandleResponseInstrumentationModule extends InstrumentationModule { + + public VertxHandleResponseInstrumentationModule() { + super("vertx"); + } + + @Override + public int getOrder() { + // run before OTEL to have access to request span. + // OTEL uses the same instrumentation point and it ends span there. + return -1; + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new HttpRequestHandleInstrumentation()); + } +} diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationModule.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationModule.java new file mode 100644 index 000000000..a69d8661f --- /dev/null +++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxInstrumentationModule.java @@ -0,0 +1,41 @@ +/* + * Copyright The Hypertrace Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.tooling.InstrumentationModule; +import io.opentelemetry.javaagent.tooling.TypeInstrumentation; +import java.util.Arrays; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxInstrumentationModule extends InstrumentationModule { + + public VertxInstrumentationModule() { + super("vertx-client", "vertx", "ht", "vertx-ht", "vertx-client-ht"); + } + + @Override + public int getOrder() { + return 1; + } + + @Override + public List typeInstrumentations() { + return Arrays.asList(new HttpRequestInstrumentation(), new HttpResponseInstrumentation()); + } +} diff --git a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java index 4c32c1d08..29b5ae2f6 100644 --- a/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java +++ b/instrumentation/vertx-web-3.0/src/test/java/io/opentelemetry/javaagent/instrumentation/hypertrace/vertx/VertxClientInstrumentationTest.java @@ -16,6 +16,7 @@ package io.opentelemetry.javaagent.instrumentation.hypertrace.vertx; +import io.opentelemetry.sdk.trace.data.SpanData; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; @@ -25,20 +26,23 @@ import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpMethod; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import org.hypertrace.agent.core.instrumentation.HypertraceSemanticAttributes; import org.hypertrace.agent.testing.AbstractHttpClientTest; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; -@Disabled("Vertex instrumentation is broken, body and header capture") public class VertxClientInstrumentationTest extends AbstractHttpClientTest { - private static final Vertx vertx = Vertx.vertx(new VertxOptions()); + private final Vertx vertx = Vertx.vertx(new VertxOptions()); private final HttpClientOptions clientOptions = new HttpClientOptions(); private final HttpClient httpClient = vertx.createHttpClient(clientOptions); public VertxClientInstrumentationTest() { - super(false); + super(true); } @Override @@ -116,4 +120,88 @@ public void handle(Buffer responseBodyBuffer) { countDownLatch.countDown(); } } + + @Test + public void postJson_write_end() throws TimeoutException, InterruptedException { + String uri = String.format("http://localhost:%d/echo", testHttpServer.port()); + + CountDownLatch countDownLatch = new CountDownLatch(1); + HttpClientRequest request = httpClient.requestAbs(HttpMethod.POST, uri); + request = request.putHeader("Content-Type", "application/json"); + request.setChunked(true); + BufferHandler bufferHandler = new BufferHandler(countDownLatch); + ResponseHandler responseHandler = new ResponseHandler(bufferHandler); + + request + .handler(responseHandler) + .write("write") + .write(Buffer.buffer().appendString(" buffer")) + .write(" str_encoding ", "utf-8") + .end(); + countDownLatch.await(); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size()); + SpanData clientSpan = traces.get(0).get(0); + Assertions.assertEquals( + "write buffer str_encoding ", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + } + + @Test + public void postJson_write_end_string() throws TimeoutException, InterruptedException { + String uri = String.format("http://localhost:%d/echo", testHttpServer.port()); + + CountDownLatch countDownLatch = new CountDownLatch(1); + HttpClientRequest request = httpClient.requestAbs(HttpMethod.POST, uri); + request = request.putHeader("Content-Type", "application/json"); + request.setChunked(true); + BufferHandler bufferHandler = new BufferHandler(countDownLatch); + ResponseHandler responseHandler = new ResponseHandler(bufferHandler); + + request + .handler(responseHandler) + .write("write") + .write(Buffer.buffer().appendString(" buffer")) + .write(" str_encoding ", "utf-8") + .end("end"); + countDownLatch.await(); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size(), String.format("was: %d", traces.size())); + SpanData clientSpan = traces.get(0).get(0); + Assertions.assertEquals( + "write buffer str_encoding end", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + } + + @Test + public void postJson_write_end_buffer() throws TimeoutException, InterruptedException { + String uri = String.format("http://localhost:%d/echo", testHttpServer.port()); + + CountDownLatch countDownLatch = new CountDownLatch(1); + HttpClientRequest request = httpClient.requestAbs(HttpMethod.POST, uri); + request = request.putHeader("Content-Type", "application/json"); + request.setChunked(true); + BufferHandler bufferHandler = new BufferHandler(countDownLatch); + ResponseHandler responseHandler = new ResponseHandler(bufferHandler); + + request + .handler(responseHandler) + .write("write") + .write(Buffer.buffer().appendString(" buffer")) + .write(" str_encoding ", "utf-8") + .end(Buffer.buffer("end")); + countDownLatch.await(); + + TEST_WRITER.waitForTraces(1); + List> traces = TEST_WRITER.getTraces(); + Assertions.assertEquals(1, traces.size(), String.format("was: %d", traces.size())); + SpanData clientSpan = traces.get(0).get(0); + Assertions.assertEquals( + "write buffer str_encoding end", + clientSpan.getAttributes().get(HypertraceSemanticAttributes.HTTP_REQUEST_BODY)); + } }