diff --git a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy
index bb082d0cc3dc..52035b10efa1 100644
--- a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy
+++ b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxCircuitBreakerWebClientTest.groovy
@@ -81,4 +81,11 @@ class VertxRxCircuitBreakerWebClientTest extends HttpClientTest implements Agent
boolean testCausality() {
false
}
+
+ @Override
+ boolean testCallbackWithParent() {
+ //Make rxjava2 instrumentation work with vert.x reactive in order to fix this test
+ return false
+ }
+
}
diff --git a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy
index 0974ba3fcd1e..5caf2b17b88a 100644
--- a/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy
+++ b/instrumentation/vertx-reactive-3.5/javaagent/src/test/groovy/client/VertxRxWebClientTest.groovy
@@ -61,4 +61,10 @@ class VertxRxWebClientTest extends HttpClientTest implements AgentTestTrait {
boolean testCausality() {
false
}
+
+ @Override
+ boolean testCallbackWithParent() {
+ //Make rxjava2 instrumentation work with vert.x reactive in order to fix this test
+ return false
+ }
}
diff --git a/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle b/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle
index 4ce0fe286213..566cc0212eb1 100644
--- a/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle
+++ b/instrumentation/vertx-reactive-3.5/javaagent/vertx-reactive-3.5-javaagent.gradle
@@ -18,6 +18,7 @@ dependencies {
testInstrumentation project(':instrumentation:jdbc:javaagent')
testInstrumentation project(':instrumentation:netty:netty-4.1:javaagent')
testInstrumentation project(':instrumentation:vertx-web-3.0')
+ //TODO we should include rjxava2 instrumentation here as well
testLibrary group: 'io.vertx', name: 'vertx-web-client', version: vertxVersion
testLibrary group: 'io.vertx', name: 'vertx-jdbc-client', version: vertxVersion
@@ -31,3 +32,7 @@ dependencies {
latestDepTestLibrary group: 'io.vertx', name: 'vertx-circuit-breaker', version: '3.+'
latestDepTestLibrary group: 'io.vertx', name: 'vertx-rx-java2', version: '3.+'
}
+
+test {
+ systemProperty "testLatestDeps", testLatestDeps
+}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/HttpRequestInstrumentation.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/HttpRequestInstrumentation.java
deleted file mode 100644
index f2ebe0eb8f13..000000000000
--- a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/HttpRequestInstrumentation.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.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.isPrivate;
-import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
-import static net.bytebuddy.matcher.ElementMatchers.named;
-
-import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
-import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
-import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
-import io.vertx.core.http.HttpClientRequest;
-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;
-
-/**
- * This hooks into two points in Vertx HttpClientRequest lifecycle.
- *
- *
First, when request is finished by the client, meaning that it is ready to be sent out, then
- * {@link AttachContextAdvice} attaches current context to that request.
- *
- *
Second, when HttpClientRequest calls any method that actually performs write on the underlying
- * Netty channel {@link MountContextAdvice} scopes that method call into the context captured on the
- * first step.
- *
- *
This ensures proper context transfer between the client who actually initiated the http call
- * and the Netty Channel that will perform that operation.
- */
-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 extends ElementMatcher super MethodDescription>, String> transformers() {
- Map, String> transformers = new HashMap<>();
-
- transformers.put(
- isMethod().and(nameStartsWith("end")),
- HttpRequestInstrumentation.class.getName() + "$AttachContextAdvice");
-
- transformers.put(
- isMethod().and(isPrivate()).and(nameStartsWith("write").or(nameStartsWith("connected"))),
- HttpRequestInstrumentation.class.getName() + "$MountContextAdvice");
- return transformers;
- }
-
- public static class AttachContextAdvice {
- @Advice.OnMethodEnter
- public static void attachContext(@Advice.This HttpClientRequest request) {
- InstrumentationContext.get(HttpClientRequest.class, Context.class)
- .put(request, Java8BytecodeBridge.currentContext());
- }
- }
-
- public static class MountContextAdvice {
- @Advice.OnMethodEnter
- public static Scope mountContext(@Advice.This HttpClientRequest request) {
- Context context =
- InstrumentationContext.get(HttpClientRequest.class, Context.class).get(request);
- return context == null ? null : context.makeCurrent();
- }
-
- @Advice.OnMethodExit
- public static void unmountContext(@Advice.Enter Scope scope) {
- if (scope != null) {
- scope.close();
- }
- }
- }
-}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxTracer.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxTracer.java
deleted file mode 100644
index 5f0213b04540..000000000000
--- a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxTracer.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.vertx;
-
-import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
-
-public class VertxTracer extends BaseTracer {
- private static final VertxTracer TRACER = new VertxTracer();
-
- public static VertxTracer tracer() {
- return TRACER;
- }
-
- @Override
- protected String getInstrumentationName() {
- return "io.opentelemetry.javaagent.vertx-web-3.0";
- }
-}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java
new file mode 100644
index 000000000000..ed2bd7e9db4f
--- /dev/null
+++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/Contexts.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.client;
+
+import io.opentelemetry.context.Context;
+
+public class Contexts {
+ public final Context parentContext;
+ public final Context context;
+
+ public Contexts(Context parentContext, Context context) {
+ this.parentContext = parentContext;
+ this.context = context;
+ }
+}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java
new file mode 100644
index 000000000000..b709c640bdc5
--- /dev/null
+++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/HttpRequestInstrumentation.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.client;
+
+import static io.opentelemetry.javaagent.instrumentation.vertx.client.VertxClientTracer.tracer;
+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.isPrivate;
+import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
+import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
+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;
+
+/**
+ * Two things happen in this instrumentation.
+ *
+ * First, {@link EndRequestAdvice}, {@link HandleExceptionAdvice} and {@link
+ * HandleResponseAdvice} deal with the common start span/end span functionality. As Vert.x is async
+ * framework, calls to the instrumented methods may happen from different threads. Thus, correct
+ * context is stored in {@code HttpClientRequest} itself.
+ *
+ *
Second, when HttpClientRequest calls any method that actually performs write on the underlying
+ * Netty channel, {@link MountContextAdvice} scopes that method call into the context captured on
+ * the first step. This ensures proper context transfer between the client who actually initiated
+ * the http call and the Netty Channel that will perform that operation. The main result of this
+ * transfer is a suppression of Netty CLIENT span.
+ */
+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 extends ElementMatcher super MethodDescription>, String> transformers() {
+ Map, String> transformers = new HashMap<>();
+
+ transformers.put(
+ isMethod().and(nameStartsWith("end").or(named("sendHead"))),
+ HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice");
+
+ transformers.put(
+ isMethod().and(named("handleException")),
+ HttpRequestInstrumentation.class.getName() + "$HandleExceptionAdvice");
+
+ transformers.put(
+ isMethod().and(named("handleResponse")),
+ HttpRequestInstrumentation.class.getName() + "$HandleResponseAdvice");
+
+ transformers.put(
+ isMethod().and(isPrivate()).and(nameStartsWith("write").or(nameStartsWith("connected"))),
+ HttpRequestInstrumentation.class.getName() + "$MountContextAdvice");
+ return transformers;
+ }
+
+ public static class EndRequestAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void attachContext(
+ @Advice.This HttpClientRequest request, @Advice.Local("otelScope") Scope scope) {
+ Context parentContext = Java8BytecodeBridge.currentContext();
+
+ if (!tracer().shouldStartSpan(parentContext)) {
+ return;
+ }
+
+ Context context = tracer().startSpan(parentContext, request, request);
+ Contexts contexts = new Contexts(parentContext, context);
+ InstrumentationContext.get(HttpClientRequest.class, Contexts.class).put(request, contexts);
+
+ scope = context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void endScope(@Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+
+ public static class HandleExceptionAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void handleException(
+ @Advice.This HttpClientRequest request,
+ @Advice.Argument(0) Throwable t,
+ @Advice.Local("otelScope") Scope scope) {
+ Contexts contexts =
+ InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request);
+
+ if (contexts == null) {
+ return;
+ }
+
+ tracer().endExceptionally(contexts.context, t);
+
+ // Scoping all potential callbacks etc to the parent context
+ scope = contexts.parentContext.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void handleResponseExit(@Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+
+ public static class HandleResponseAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void handleResponseEnter(
+ @Advice.This HttpClientRequest request,
+ @Advice.Argument(0) HttpClientResponse response,
+ @Advice.Local("otelScope") Scope scope) {
+ Contexts contexts =
+ InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request);
+
+ if (contexts == null) {
+ return;
+ }
+
+ tracer().end(contexts.context, response);
+
+ // Scoping all potential callbacks etc to the parent context
+ scope = contexts.parentContext.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void handleResponseExit(@Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+
+ public static class MountContextAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void mountContext(
+ @Advice.This HttpClientRequest request, @Advice.Local("otelScope") Scope scope) {
+ Contexts contexts =
+ InstrumentationContext.get(HttpClientRequest.class, Contexts.class).get(request);
+ if (contexts == null) {
+ return;
+ }
+
+ scope = contexts.context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void unmountContext(@Advice.Local("otelScope") Scope scope) {
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxClientInstrumentationModule.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java
similarity index 87%
rename from instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxClientInstrumentationModule.java
rename to instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java
index 9a4764068387..7b9b5a72bf40 100644
--- a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/VertxClientInstrumentationModule.java
+++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientInstrumentationModule.java
@@ -3,13 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.vertx;
+package io.opentelemetry.javaagent.instrumentation.vertx.client;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import com.google.auto.service.AutoService;
-import io.opentelemetry.context.Context;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
@@ -29,6 +28,6 @@ public List typeInstrumentations() {
@Override
public Map contextStore() {
- return singletonMap("io.vertx.core.http.HttpClientRequest", Context.class.getName());
+ return singletonMap("io.vertx.core.http.HttpClientRequest", Contexts.class.getName());
}
}
diff --git a/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java
new file mode 100644
index 000000000000..565d2ed0cbd3
--- /dev/null
+++ b/instrumentation/vertx-web-3.0/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/client/VertxClientTracer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.client;
+
+import io.opentelemetry.context.propagation.TextMapSetter;
+import io.opentelemetry.instrumentation.api.tracer.HttpClientTracer;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class VertxClientTracer
+ extends HttpClientTracer {
+ private static final VertxClientTracer TRACER = new VertxClientTracer();
+
+ public static VertxClientTracer tracer() {
+ return TRACER;
+ }
+
+ @Override
+ protected String getInstrumentationName() {
+ return "io.opentelemetry.javaagent.vertx-core-3.0";
+ }
+
+ @Override
+ protected String method(HttpClientRequest request) {
+ return request.method().name();
+ }
+
+ @Override
+ protected @Nullable URI url(HttpClientRequest request) throws URISyntaxException {
+ return new URI(request.uri());
+ }
+
+ @Override
+ protected @Nullable Integer status(HttpClientResponse response) {
+ return response.statusCode();
+ }
+
+ @Override
+ protected @Nullable String requestHeader(HttpClientRequest request, String name) {
+ return request.headers().get(name);
+ }
+
+ @Override
+ protected @Nullable String responseHeader(HttpClientResponse response, String name) {
+ return response.getHeader(name);
+ }
+
+ @Override
+ protected TextMapSetter getSetter() {
+ return Propagator.INSTANCE;
+ }
+
+ private static class Propagator implements TextMapSetter {
+ private static final Propagator INSTANCE = new Propagator();
+
+ @Override
+ public void set(HttpClientRequest carrier, String key, String value) {
+ if (carrier != null) {
+ carrier.putHeader(key, value);
+ }
+ }
+ }
+}
diff --git a/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxHttpClientTest.groovy b/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxHttpClientTest.groovy
index 54c1e3f9a1e8..c918e9dbec4b 100644
--- a/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxHttpClientTest.groovy
+++ b/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxHttpClientTest.groovy
@@ -7,6 +7,7 @@ package client
import io.opentelemetry.instrumentation.test.AgentTestTrait
import io.opentelemetry.instrumentation.test.base.HttpClientTest
+import io.opentelemetry.instrumentation.test.base.SingleConnection
import io.vertx.core.Vertx
import io.vertx.core.VertxOptions
import io.vertx.core.http.HttpClientOptions
@@ -54,4 +55,12 @@ class VertxHttpClientTest extends HttpClientTest implements AgentTestTrait {
// FIXME: figure out how to configure timeouts.
false
}
+
+ @Override
+ SingleConnection createSingleConnection(String host, int port) {
+ //This test fails on Vert.x 3.0 and only works starting from 3.1
+ //Most probably due to https://github.com/eclipse-vertx/vert.x/pull/1126
+ boolean shouldRun = Boolean.getBoolean("testLatestDeps")
+ return shouldRun ? new VertxSingleConnection(host, port) : null
+ }
}
diff --git a/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxSingleConnection.java b/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxSingleConnection.java
new file mode 100644
index 000000000000..69f560976ad5
--- /dev/null
+++ b/instrumentation/vertx-web-3.0/src/test/groovy/client/VertxSingleConnection.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package client;
+
+import static io.opentelemetry.instrumentation.test.base.SingleConnection.REQUEST_ID_HEADER;
+
+import io.opentelemetry.instrumentation.test.base.SingleConnection;
+import io.vertx.core.Vertx;
+import io.vertx.core.VertxOptions;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpMethod;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+public class VertxSingleConnection implements SingleConnection {
+
+ private final HttpClient httpClient;
+ private final String host;
+ private final int port;
+
+ public VertxSingleConnection(String host, int port) {
+ this.host = host;
+ this.port = port;
+ HttpClientOptions clientOptions =
+ new HttpClientOptions().setMaxPoolSize(1).setKeepAlive(true).setPipelining(true);
+ httpClient = Vertx.vertx(new VertxOptions()).createHttpClient(clientOptions);
+ }
+
+ @Override
+ public int doRequest(String path, Map headers)
+ throws ExecutionException, InterruptedException {
+ String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER));
+
+ String url;
+ try {
+ url = new URL("http", host, port, path).toString();
+ } catch (MalformedURLException e) {
+ throw new ExecutionException(e);
+ }
+ HttpClientRequest request = httpClient.request(HttpMethod.GET, port, host, url);
+ headers.forEach(request::putHeader);
+
+ CompletableFuture future = new CompletableFuture<>();
+ request.handler(future::complete);
+
+ request.end();
+ HttpClientResponse response = future.get();
+ String responseId = response.getHeader(REQUEST_ID_HEADER);
+ if (!requestId.equals(responseId)) {
+ throw new IllegalStateException(
+ String.format("Received response with id %s, expected %s", responseId, requestId));
+ }
+ return response.statusCode();
+ }
+}
diff --git a/instrumentation/vertx-web-3.0/vertx-web-3.0.gradle b/instrumentation/vertx-web-3.0/vertx-web-3.0.gradle
index f523b34df010..4f69729bcc96 100644
--- a/instrumentation/vertx-web-3.0/vertx-web-3.0.gradle
+++ b/instrumentation/vertx-web-3.0/vertx-web-3.0.gradle
@@ -24,3 +24,7 @@ dependencies {
latestDepTestLibrary group: 'io.vertx', name: 'vertx-web', version: '3.+'
latestDepTestLibrary group: 'io.vertx', name: 'vertx-web-client', version: '3.+'
}
+
+test {
+ systemProperty "testLatestDeps", testLatestDeps
+}
\ No newline at end of file
diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy
index 50ff4c2dc6e4..29066b8a76b1 100644
--- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy
+++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/HttpClientTest.groovy
@@ -43,7 +43,7 @@ abstract class HttpClientTest extends InstrumentationSpecification {
prefix("success") {
handleDistributedRequest()
String msg = "Hello."
- response.status(200).send(msg)
+ response.status(200).id(request.getHeader("test-request-id")).send(msg)
}
prefix("client-error") {
handleDistributedRequest()
@@ -439,7 +439,7 @@ abstract class HttpClientTest extends InstrumentationSpecification {
count.times { idx ->
trace(idx, 3) {
def rootSpan = it.span(0)
- //Traces can be in arbitrary order, let us find out the request id if the current one
+ //Traces can be in arbitrary order, let us find out the request id of the current one
def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length()))
basicSpan(it, 0, "Parent span " + requestId, null, null) {
@@ -452,7 +452,61 @@ abstract class HttpClientTest extends InstrumentationSpecification {
}
}
}
+ }
+
+ /**
+ * Almost similar to the "high concurrency test" test above, but all requests use the same single
+ * connection.
+ */
+ def "high concurrency test on single connection"() {
+ setup:
+ def singleConnection = createSingleConnection(server.address.host, server.address.port)
+ assumeTrue(singleConnection != null)
+ int count = 50
+ def method = 'GET'
+ def path = "/success"
+ def url = server.address.resolve(path)
+
+ def latch = new CountDownLatch(1)
+ def pool = Executors.newFixedThreadPool(4)
+
+ when:
+ count.times { index ->
+ def job = {
+ latch.await()
+ runUnderTrace("Parent span " + index) {
+ Span.current().setAttribute("test.request.id", index)
+ singleConnection.doRequest(path, [(SingleConnection.REQUEST_ID_HEADER): index.toString()])
+ }
+ }
+ pool.submit(job)
+ }
+ latch.countDown()
+ then:
+ assertTraces(count) {
+ count.times { idx ->
+ trace(idx, 3) {
+ def rootSpan = it.span(0)
+ //Traces can be in arbitrary order, let us find out the request id of the current one
+ def requestId = Integer.parseInt(rootSpan.name.substring("Parent span ".length()))
+
+ basicSpan(it, 0, "Parent span " + requestId, null, null) {
+ it."test.request.id" requestId
+ }
+ clientSpan(it, 1, span(0), method, url)
+ serverSpan(it, 2, span(1)) {
+ it."test.request.id" requestId
+ }
+ }
+ }
+ }
+ }
+
+ //This method should create either a single connection to the target uri or a http client
+ //which is guaranteed to use the same connection for all requests
+ SingleConnection createSingleConnection(String host, int port) {
+ return null
}
// parent span must be cast otherwise it breaks debugging classloading (junit loads it early)
diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java
new file mode 100644
index 000000000000..d254aa73fe08
--- /dev/null
+++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/base/SingleConnection.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.test.base;
+
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Helper class for http client tests which require a single connection.
+ *
+ * Tests for specific library should provide an implementation which satisfies the following
+ * conditions:
+ *
+ *
+ * - Has a constructor which accepts target host and port
+ *
- For a given instance all invocations of {@link #doRequest(String, Map)} will reuse the same
+ * underlying connection to target host.
+ *
+ */
+public interface SingleConnection {
+ String REQUEST_ID_HEADER = "test-request-id";
+
+ int doRequest(String path, Map headers)
+ throws ExecutionException, InterruptedException;
+}
diff --git a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/server/http/TestHttpServer.groovy b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/server/http/TestHttpServer.groovy
index df53240db12e..124b0b3fa206 100644
--- a/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/server/http/TestHttpServer.groovy
+++ b/testing-common/src/main/groovy/io/opentelemetry/instrumentation/test/server/http/TestHttpServer.groovy
@@ -242,7 +242,7 @@ class TestHttpServer implements AutoCloseable {
req.handled = true
}
- void handleDistributedRequest(Closure doInSpan = null) {
+ void handleDistributedRequest() {
boolean isTestServer = true
if (request.getHeader("is-test-server") != null) {
isTestServer = Boolean.parseBoolean(request.getHeader("is-test-server"))
@@ -301,16 +301,23 @@ class TestHttpServer implements AutoCloseable {
class ResponseApi {
private int status = 200
+ private String id
ResponseApi status(int status) {
this.status = status
return this
}
+ ResponseApi id(String id) {
+ this.id = id
+ return this
+ }
+
void send() {
assert !req.handled
req.contentType = "text/plain;charset=utf-8"
resp.status = status
+ resp.setHeader("test-request-id", id)
req.handled = true
}