diff --git a/.fossa.yml b/.fossa.yml index 0d29905b0eb0..35cfb798538f 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -1114,6 +1114,9 @@ targets: - type: gradle path: ./ target: ':instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:vertx:vertx-http-client:vertx-http-client-5.0:javaagent' - type: gradle path: ./ target: ':instrumentation:vertx:vertx-http-client:vertx-http-client-common:javaagent' diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/build.gradle.kts index 97a9d5f4b6a5..8ba053117f7c 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/build.gradle.kts @@ -21,6 +21,6 @@ dependencies { testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) - latestDepTestLibrary("io.vertx:vertx-core:4.+") // documented limitation, 5.x not supported yet - latestDepTestLibrary("io.vertx:vertx-codegen:4.+") // documented limitation, 5.x not supported yet + latestDepTestLibrary("io.vertx:vertx-core:4.+") // see vertx-http-client-5.0 module + latestDepTestLibrary("io.vertx:vertx-codegen:4.+") // see vertx-http-client-5.0 module } diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientInstrumentationModule.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientInstrumentationModule.java index b6eb5918d904..c18687d452a9 100644 --- a/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientInstrumentationModule.java +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/client/VertxClientInstrumentationModule.java @@ -25,7 +25,9 @@ public VertxClientInstrumentationModule() { @Override public ElementMatcher.Junction classLoaderMatcher() { // class removed in 4.0 - return not(hasClassesNamed("io.vertx.core.Starter")); + return not(hasClassesNamed("io.vertx.core.Starter")) + // class added in 5.0 + .and(not(hasClassesNamed("io.vertx.core.http.impl.HttpClientConnectionInternal"))); } @Override diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..a36013f357f2 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.vertx") + module.set("vertx-core") + versions.set("[5.0.0,)") + assertInverse.set(true) + } +} + +otelJava { + minJavaVersionSupported.set(JavaVersion.VERSION_11) +} + +dependencies { + library("io.vertx:vertx-core:5.0.0") + + // vertx-codegen dependency is needed for Xlint's annotation checking + library("io.vertx:vertx-codegen:5.0.0") + + implementation(project(":instrumentation:vertx:vertx-http-client:vertx-http-client-common:javaagent")) + + testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) + testInstrumentation(project(":instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent")) +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpClientRequestBaseInstrumentation.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpClientRequestBaseInstrumentation.java new file mode 100644 index 000000000000..62b67043e413 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpClientRequestBaseInstrumentation.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.http.impl.HttpClientRequestBase; +import io.vertx.core.net.HostAndPort; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HttpClientRequestBaseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.core.http.impl.HttpClientRequestBase"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), this.getClass().getName() + "$ConstructorAdvice"); + + transformer.applyAdviceToMethod( + named("authority").and(takesArgument(0, named("io.vertx.core.net.HostAndPort"))), + this.getClass().getName() + "$SetAuthorityAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This HttpClientRequestBase request, + @Advice.FieldValue("authority") HostAndPort authority) { + VertxClientSingletons.setAuthority(request, authority); + } + } + + @SuppressWarnings("unused") + public static class SetAuthorityAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This HttpClientRequestBase request, @Advice.Argument(0) HostAndPort authority) { + VertxClientSingletons.setAuthority(request, authority); + } + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpRequestInstrumentation.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpRequestInstrumentation.java new file mode 100644 index 000000000000..c1d579a8c5bf --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/HttpRequestInstrumentation.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client.VertxClientSingletons.instrumenter; +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 static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.opentelemetry.javaagent.instrumentation.vertx.client.Contexts; +import io.opentelemetry.javaagent.instrumentation.vertx.client.ExceptionHandlerWrapper; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import net.bytebuddy.asm.Advice; +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 void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isMethod().and(nameStartsWith("end").or(named("sendHead"))), + HttpRequestInstrumentation.class.getName() + "$EndRequestAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(named("handleException")), + HttpRequestInstrumentation.class.getName() + "$HandleExceptionAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("handleResponse")) + .and(takesArgument(1, named("io.vertx.core.http.HttpClientResponse"))), + HttpRequestInstrumentation.class.getName() + "$HandleResponseAdvice"); + + transformer.applyAdviceToMethod( + isMethod().and(isPrivate()).and(nameStartsWith("write").or(nameStartsWith("connected"))), + HttpRequestInstrumentation.class.getName() + "$MountContextAdvice"); + + transformer.applyAdviceToMethod( + isMethod() + .and(named("exceptionHandler")) + .and(takesArgument(0, named("io.vertx.core.Handler"))), + HttpRequestInstrumentation.class.getName() + "$ExceptionHandlerAdvice"); + } + + @SuppressWarnings("unused") + public static class EndRequestAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void attachContext( + @Advice.This HttpClientRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = Java8BytecodeBridge.currentContext(); + + if (!instrumenter().shouldStart(parentContext, request)) { + return; + } + + context = instrumenter().start(parentContext, request); + Contexts contexts = new Contexts(parentContext, context); + VirtualField.find(HttpClientRequest.class, Contexts.class).set(request, contexts); + + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void endScope( + @Advice.This HttpClientRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope, + @Advice.Thrown Throwable throwable) { + if (scope != null) { + scope.close(); + } + if (throwable != null) { + instrumenter().end(context, request, null, throwable); + } + } + } + + @SuppressWarnings("unused") + 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 = VirtualField.find(HttpClientRequest.class, Contexts.class).get(request); + + if (contexts == null) { + return; + } + + instrumenter().end(contexts.context, request, null, 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(); + } + } + } + + @SuppressWarnings("unused") + public static class HandleResponseAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void handleResponseEnter( + @Advice.This HttpClientRequest request, + @Advice.Argument(1) HttpClientResponse response, + @Advice.Local("otelScope") Scope scope) { + Contexts contexts = VirtualField.find(HttpClientRequest.class, Contexts.class).get(request); + + if (contexts == null) { + return; + } + + instrumenter().end(contexts.context, request, response, null); + + // 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(); + } + } + } + + @SuppressWarnings("unused") + public static class MountContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void mountContext( + @Advice.This HttpClientRequest request, @Advice.Local("otelScope") Scope scope) { + Contexts contexts = VirtualField.find(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(); + } + } + } + + @SuppressWarnings("unused") + public static class ExceptionHandlerAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void wrapExceptionHandler( + @Advice.This HttpClientRequest request, + @Advice.Argument(value = 0, readOnly = false) Handler handler) { + if (handler != null) { + VirtualField virtualField = + VirtualField.find(HttpClientRequest.class, Contexts.class); + handler = ExceptionHandlerWrapper.wrap(instrumenter(), request, virtualField, handler); + } + } + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/ResourceManagerInstrumentation.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/ResourceManagerInstrumentation.java new file mode 100644 index 000000000000..9f7d18efbf25 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/ResourceManagerInstrumentation.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.Future; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Propagate context to connection established callback. */ +public class ResourceManagerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.core.internal.resource.ResourceManager"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("withResourceAsync").and(returns(named("io.vertx.core.Future"))), + this.getClass().getName() + "$WithResourceAsyncAdvice"); + } + + @SuppressWarnings("unused") + public static class WithResourceAsyncAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void wrapFuture(@Advice.Return(readOnly = false) Future future) { + future = VertxClientSingletons.wrapFuture(future); + } + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/Vertx5HttpAttributesGetter.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/Vertx5HttpAttributesGetter.java new file mode 100644 index 000000000000..3aac746100a7 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/Vertx5HttpAttributesGetter.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import io.opentelemetry.javaagent.instrumentation.vertx.client.AbstractVertxHttpAttributesGetter; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.HostAndPort; +import io.vertx.core.net.SocketAddress; +import javax.annotation.Nullable; + +final class Vertx5HttpAttributesGetter extends AbstractVertxHttpAttributesGetter { + + @Override + public String getUrlFull(HttpClientRequest request) { + String uri = request.getURI(); + if (!isAbsolute(uri)) { + uri = request.absoluteURI(); + } + return uri; + } + + private static boolean isAbsolute(String uri) { + return uri.startsWith("http://") || uri.startsWith("https://"); + } + + @Override + public String getHttpRequestMethod(HttpClientRequest request) { + return request.getMethod().name(); + } + + @Override + public String getNetworkProtocolName( + HttpClientRequest request, @Nullable HttpClientResponse response) { + return "http"; + } + + @Nullable + @Override + public String getNetworkProtocolVersion( + HttpClientRequest request, @Nullable HttpClientResponse response) { + HttpVersion version = request.version(); + if (version == null) { + return null; + } + switch (version) { + case HTTP_1_0: + return "1.0"; + case HTTP_1_1: + return "1.1"; + case HTTP_2: + return "2"; + } + return null; + } + + @Nullable + @Override + public String getServerAddress(HttpClientRequest request) { + HostAndPort authority = VertxClientSingletons.getAuthority(request); + return authority != null ? authority.host() : null; + } + + @Nullable + @Override + public Integer getServerPort(HttpClientRequest request) { + HostAndPort authority = VertxClientSingletons.getAuthority(request); + return authority != null ? authority.port() : null; + } + + @Nullable + @Override + public String getNetworkPeerAddress( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + SocketAddress socketAddress = response.netSocket().remoteAddress(); + return socketAddress == null ? null : socketAddress.hostAddress(); + } + + @Nullable + @Override + public Integer getNetworkPeerPort( + HttpClientRequest request, @Nullable HttpClientResponse response) { + if (response == null) { + return null; + } + SocketAddress socketAddress = response.netSocket().remoteAddress(); + return socketAddress == null ? null : socketAddress.port(); + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientInstrumentationModule.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientInstrumentationModule.java new file mode 100644 index 000000000000..03df65695bee --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientInstrumentationModule.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class VertxClientInstrumentationModule extends InstrumentationModule { + + public VertxClientInstrumentationModule() { + super("vertx-http-client", "vertx-http-client-5.0", "vertx"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // class added in 5.0 + return hasClassesNamed("io.vertx.core.http.impl.HttpClientConnectionInternal"); + } + + @Override + public List typeInstrumentations() { + return asList( + new HttpRequestInstrumentation(), + new HttpClientRequestBaseInstrumentation(), + new ResourceManagerInstrumentation()); + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientSingletons.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientSingletons.java new file mode 100644 index 000000000000..e7deee794716 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxClientSingletons.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.instrumentation.vertx.client.VertxClientInstrumenterFactory; +import io.vertx.core.Future; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.net.HostAndPort; +import java.util.concurrent.CompletableFuture; + +public final class VertxClientSingletons { + + private static final Instrumenter INSTRUMENTER = + VertxClientInstrumenterFactory.create( + "io.opentelemetry.vertx-http-client-5.0", new Vertx5HttpAttributesGetter()); + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private static final VirtualField authorityField = + VirtualField.find(HttpClientRequest.class, HostAndPort.class); + + public static void setAuthority(HttpClientRequest request, HostAndPort authority) { + authorityField.set(request, authority); + } + + public static HostAndPort getAuthority(HttpClientRequest request) { + return authorityField.get(request); + } + + public static Future wrapFuture(Future future) { + Context context = Context.current(); + CompletableFuture result = new CompletableFuture<>(); + future + .toCompletionStage() + .whenComplete( + (value, throwable) -> { + try (Scope ignore = context.makeCurrent()) { + if (throwable != null) { + result.completeExceptionally(throwable); + } else { + result.complete(value); + } + } + }); + return Future.fromCompletionStage(result); + } + + private VertxClientSingletons() {} +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxHttpClientTest.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxHttpClientTest.java new file mode 100644 index 000000000000..5747f67ba808 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxHttpClientTest.java @@ -0,0 +1,123 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import static java.util.Collections.emptySet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpClientTest; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientResult; +import io.opentelemetry.instrumentation.testing.junit.http.HttpClientTestOptions; +import io.vertx.core.Future; +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.HttpMethod; +import io.vertx.core.http.RequestOptions; +import java.net.URI; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.extension.RegisterExtension; + +class VertxHttpClientTest extends AbstractHttpClientTest> { + + @RegisterExtension + static final InstrumentationExtension testing = HttpClientInstrumentationExtension.forAgent(); + + private final HttpClient httpClient = buildClient(); + + private static HttpClient buildClient() { + Vertx vertx = Vertx.vertx(new VertxOptions()); + HttpClientOptions clientOptions = + new HttpClientOptions().setConnectTimeout(Math.toIntExact(CONNECTION_TIMEOUT.toMillis())); + return vertx.createHttpClient(clientOptions); + } + + @Override + public Future buildRequest( + String method, URI uri, Map headers) { + RequestOptions requestOptions = + new RequestOptions().setMethod(HttpMethod.valueOf(method)).setAbsoluteURI(uri.toString()); + headers.forEach(requestOptions::putHeader); + return httpClient.request(requestOptions); + } + + private static CompletableFuture sendRequest(Future request) { + CompletableFuture future = new CompletableFuture<>(); + + request + .compose( + req -> + req.send() + .onComplete( + asyncResult -> { + if (asyncResult.succeeded()) { + future.complete(asyncResult.result().statusCode()); + } else { + future.completeExceptionally(asyncResult.cause()); + } + })) + .onFailure(future::completeExceptionally); + + return future; + } + + @Override + public int sendRequest( + Future request, String method, URI uri, Map headers) + throws Exception { + // Vertx doesn't seem to provide any synchronous API so bridge through a callback + return sendRequest(request).get(30, TimeUnit.SECONDS); + } + + @Override + public void sendRequestWithCallback( + Future request, + String method, + URI uri, + Map headers, + HttpClientResult httpClientResult) { + sendRequest(request) + .whenComplete((status, throwable) -> httpClientResult.complete(() -> status, throwable)); + } + + @Override + protected void configure(HttpClientTestOptions.Builder optionsBuilder) { + optionsBuilder.disableTestRedirects(); + optionsBuilder.disableTestReusedRequest(); + optionsBuilder.disableTestHttps(); + optionsBuilder.disableTestReadTimeout(); + optionsBuilder.setHttpAttributes(VertxHttpClientTest::getHttpAttributes); + optionsBuilder.setExpectedClientSpanNameMapper(VertxHttpClientTest::getExpectedClientSpanName); + + optionsBuilder.setSingleConnectionFactory(VertxSingleConnection::new); + } + + private static Set> getHttpAttributes(URI uri) { + String uriString = uri.toString(); + // http://localhost:61/ => unopened port, http://192.0.2.1/ => non routable address + if ("http://localhost:61/".equals(uriString) || "http://192.0.2.1/".equals(uriString)) { + return emptySet(); + } + return HttpClientTestOptions.DEFAULT_HTTP_ATTRIBUTES; + } + + private static String getExpectedClientSpanName(URI uri, String method) { + switch (uri.toString()) { + case "http://localhost:61/": // unopened port + case "http://192.0.2.1/": // non routable address + return "CONNECT"; + default: + return HttpClientTestOptions.DEFAULT_EXPECTED_CLIENT_SPAN_NAME_MAPPER.apply(uri, method); + } + } +} diff --git a/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxSingleConnection.java b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxSingleConnection.java new file mode 100644 index 000000000000..1e665bc46e90 --- /dev/null +++ b/instrumentation/vertx/vertx-http-client/vertx-http-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/client/VertxSingleConnection.java @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.client; + +import io.opentelemetry.instrumentation.testing.junit.http.SingleConnection; +import io.vertx.core.Future; +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.PoolOptions; +import io.vertx.core.http.RequestOptions; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +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().setKeepAlive(true).setPipelining(true); + PoolOptions poolOptions = new PoolOptions().setHttp1MaxSize(1); + httpClient = Vertx.vertx(new VertxOptions()).createHttpClient(clientOptions, poolOptions); + } + + @Override + public int doRequest(String path, Map headers) + throws ExecutionException, InterruptedException { + String requestId = Objects.requireNonNull(headers.get(REQUEST_ID_HEADER)); + RequestOptions requestOptions = new RequestOptions().setHost(host).setPort(port).setURI(path); + headers.forEach(requestOptions::putHeader); + Future request = httpClient.request(requestOptions); + + HttpClientResponse response = + request.compose(HttpClientRequest::send).toCompletionStage().toCompletableFuture().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/settings.gradle.kts b/settings.gradle.kts index cefa52b83cb7..02f87bf5993f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -599,6 +599,7 @@ include(":instrumentation:vaadin-14.2:javaagent") include(":instrumentation:vaadin-14.2:testing") include(":instrumentation:vertx:vertx-http-client:vertx-http-client-3.0:javaagent") include(":instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent") +include(":instrumentation:vertx:vertx-http-client:vertx-http-client-5.0:javaagent") include(":instrumentation:vertx:vertx-http-client:vertx-http-client-common:javaagent") include(":instrumentation:vertx:vertx-kafka-client-3.6:javaagent") include(":instrumentation:vertx:vertx-kafka-client-3.6:testing")