Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public VertxClientInstrumentationModule() {
@Override
public ElementMatcher.Junction<ClassLoader> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"))
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("io.vertx.core.http.HttpClientRequest");
}

@Override
public ElementMatcher<TypeDescription> 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<Throwable> handler) {
if (handler != null) {
VirtualField<HttpClientRequest, Contexts> virtualField =
VirtualField.find(HttpClientRequest.class, Contexts.class);
handler = ExceptionHandlerWrapper.wrap(instrumenter(), request, virtualField, handler);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);
}
}
}
Loading
Loading