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, 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, 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: + * + *

+ */ +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 }