Răsfoiți Sursa

Add instrumentation for vertx-sql-client (#8311)

Lauri Tulmin 1 an în urmă
părinte
comite
04097b3093
14 a modificat fișierele cu 870 adăugiri și 0 ștergeri
  1. 1 0
      docs/supported-libraries.md
  2. 25 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts
  3. 64 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java
  4. 127 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java
  5. 71 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java
  6. 79 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java
  7. 43 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java
  8. 35 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java
  9. 32 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java
  10. 38 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientRequest.java
  11. 78 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java
  12. 25 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/vertx/sqlclient/impl/QueryExecutorUtil.java
  13. 251 0
      instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java
  14. 1 0
      settings.gradle.kts

+ 1 - 0
docs/supported-libraries.md

@@ -131,6 +131,7 @@ These are the supported libraries and frameworks:
 | [Vert.x HttpClient](https://vertx.io/docs/apidocs/io/vertx/core/http/HttpClient.html)                                                       | 3.0+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | [HTTP Client Spans], [HTTP Client Metrics]                                             |
 | [Vert.x Kafka Client](https://vertx.io/docs/vertx-kafka-client/java/)                                                                       | 3.6+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | [Messaging Spans]                                                                      |
 | [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/)                                                                                     | 3.5+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | context propagation only                                                               |
+| [Vert.x SQL Client](https://github.com/eclipse-vertx/vertx-sql-client/)                                                                     | 4.0+                          | N/A                                                                                                                                                                                                                                                                                                                                                                                     | [Database Client Spans]                                                                |
 | [Vibur DBCP](https://www.vibur.org/)                                                                                                        | 11.0+                         | [opentelemetry-vibur-dbcp-11.0](../instrumentation/vibur-dbcp-11.0/library)                                                                                                                                                                                                                                                                                                             | [Database Pool Metrics]                                                                |
 | [ZIO](https://zio.dev/)                                                                                                                     | 2.0.0+                        | N/A                                                                                                                                                                                                                                                                                                                                                                                     | Context propagation                                                                    |
 

+ 25 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts

@@ -0,0 +1,25 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+  pass {
+    group.set("io.vertx")
+    module.set("vertx-sql-client")
+    versions.set("[4.0.0,)")
+    assertInverse.set(true)
+  }
+}
+
+dependencies {
+  library("io.vertx:vertx-sql-client:4.1.0")
+  compileOnly("io.vertx:vertx-codegen:4.1.0")
+
+  testLibrary("io.vertx:vertx-pg-client:4.1.0")
+  testLibrary("io.vertx:vertx-codegen:4.1.0")
+  testLibrary("io.vertx:vertx-opentelemetry:4.1.0")
+}
+
+tasks.withType<Test>().configureEach {
+  usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
+}

+ 64 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.returns;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.javaagent.bootstrap.CallDepth;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.SqlConnectOptions;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class PoolInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("io.vertx.sqlclient.Pool");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("pool")
+            .and(takesArguments(3))
+            .and(takesArgument(1, named("io.vertx.sqlclient.SqlConnectOptions")))
+            .and(returns(named("io.vertx.sqlclient.Pool"))),
+        PoolInstrumentation.class.getName() + "$PoolAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class PoolAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Argument(1) SqlConnectOptions sqlConnectOptions,
+        @Advice.Local("otelCallDepth") CallDepth callDepth) {
+      callDepth = CallDepth.forClass(Pool.class);
+      if (callDepth.getAndIncrement() > 0) {
+        return;
+      }
+
+      // set connection options to ThreadLocal, they will be read in SqlClientBase constructor
+      setSqlConnectOptions(sqlConnectOptions);
+    }
+
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) {
+      if (callDepth.decrementAndGet() > 0) {
+        return;
+      }
+
+      setSqlConnectOptions(null);
+    }
+  }
+}

+ 127 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_CONTEXT_KEY;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_PARENT_CONTEXT_KEY;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_REQUEST_KEY;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.getSqlConnectOptions;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.instrumenter;
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.bootstrap.CallDepth;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.vertx.core.impl.future.PromiseInternal;
+import io.vertx.sqlclient.impl.PreparedStatement;
+import io.vertx.sqlclient.impl.QueryExecutorUtil;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class QueryExecutorInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("io.vertx.sqlclient.impl.QueryExecutor");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        isConstructor(), QueryExecutorInstrumentation.class.getName() + "$ConstructorAdvice");
+    transformer.applyAdviceToMethod(
+        namedOneOf("executeSimpleQuery", "executeExtendedQuery", "executeBatchQuery"),
+        QueryExecutorInstrumentation.class.getName() + "$QueryAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ConstructorAdvice {
+
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit(@Advice.This Object queryExecutor) {
+      // copy connection options from ThreadLocal to VirtualField
+      QueryExecutorUtil.setConnectOptions(queryExecutor, getSqlConnectOptions());
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class QueryAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.This Object queryExecutor,
+        @Advice.AllArguments Object[] arguments,
+        @Advice.Local("otelCallDepth") CallDepth callDepth,
+        @Advice.Local("otelRequest") VertxSqlClientRequest otelRequest,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      callDepth = CallDepth.forClass(queryExecutor.getClass());
+      if (callDepth.getAndIncrement() > 0) {
+        return;
+      }
+
+      // The parameter we need are in different positions, we are not going to have separate
+      // advices for all of them. The method gets the statement either as String or
+      // PreparedStatement, use the first argument that is either of these. PromiseInternal is
+      // always at the end of the argument list.
+      String sql = null;
+      PromiseInternal<?> promiseInternal = null;
+      for (Object argument : arguments) {
+        if (sql == null) {
+          if (argument instanceof String) {
+            sql = (String) argument;
+          } else if (argument instanceof PreparedStatement) {
+            sql = ((PreparedStatement) argument).sql();
+          }
+        } else if (argument instanceof PromiseInternal) {
+          promiseInternal = (PromiseInternal) argument;
+        }
+      }
+      if (sql == null || promiseInternal == null) {
+        return;
+      }
+
+      otelRequest =
+          new VertxSqlClientRequest(sql, QueryExecutorUtil.getConnectOptions(queryExecutor));
+      Context parentContext = currentContext();
+      if (!instrumenter().shouldStart(parentContext, otelRequest)) {
+        return;
+      }
+
+      context = instrumenter().start(parentContext, otelRequest);
+      scope = context.makeCurrent();
+      promiseInternal.context().localContextData().put(OTEL_REQUEST_KEY, otelRequest);
+      promiseInternal.context().localContextData().put(OTEL_CONTEXT_KEY, context);
+      promiseInternal.context().localContextData().put(OTEL_PARENT_CONTEXT_KEY, parentContext);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Thrown Throwable throwable,
+        @Advice.Local("otelCallDepth") CallDepth callDepth,
+        @Advice.Local("otelRequest") VertxSqlClientRequest otelRequest,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      if (callDepth.decrementAndGet() > 0) {
+        return;
+      }
+      if (scope == null) {
+        return;
+      }
+
+      scope.close();
+      if (throwable != null) {
+        instrumenter().end(context, otelRequest, null, throwable);
+      }
+      // span will be ended in QueryResultBuilderInstrumentation
+    }
+  }
+}

+ 71 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.endQuerySpan;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.vertx.core.Context;
+import io.vertx.core.impl.ContextInternal;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class QueryResultBuilderInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("io.vertx.sqlclient.impl.QueryResultBuilder");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("tryComplete"),
+        QueryResultBuilderInstrumentation.class.getName() + "$CompleteAdvice");
+    transformer.applyAdviceToMethod(
+        named("tryFail").and(takesArguments(Throwable.class)),
+        QueryResultBuilderInstrumentation.class.getName() + "$FailAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class CompleteAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static Scope onEnter(@Advice.FieldValue("context") Context vertxContext) {
+      ContextInternal contextInternal = (ContextInternal) vertxContext;
+      return endQuerySpan(contextInternal.localContextData(), null);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(@Advice.Enter Scope scope) {
+      if (scope != null) {
+        scope.close();
+      }
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class FailAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static Scope onEnter(
+        @Advice.Argument(0) Throwable throwable,
+        @Advice.FieldValue("context") Context vertxContext) {
+      ContextInternal contextInternal = (ContextInternal) vertxContext;
+      return endQuerySpan(contextInternal.localContextData(), throwable);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(@Advice.Enter Scope scope) {
+      if (scope != null) {
+        scope.close();
+      }
+    }
+  }
+}

+ 79 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.getSqlConnectOptions;
+import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions;
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
+
+import io.opentelemetry.instrumentation.api.util.VirtualField;
+import io.opentelemetry.javaagent.bootstrap.CallDepth;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.vertx.sqlclient.SqlConnectOptions;
+import io.vertx.sqlclient.impl.SqlClientBase;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class SqlClientBaseInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("io.vertx.sqlclient.impl.SqlClientBase");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        isConstructor(), SqlClientBaseInstrumentation.class.getName() + "$ConstructorAdvice");
+    transformer.applyAdviceToMethod(
+        namedOneOf("query", "preparedQuery"),
+        SqlClientBaseInstrumentation.class.getName() + "$QueryAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ConstructorAdvice {
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit(@Advice.This SqlClientBase<?> sqlClientBase) {
+      // copy connection options from ThreadLocal to VirtualField
+      VirtualField<SqlClientBase<?>, SqlConnectOptions> virtualField =
+          VirtualField.find(SqlClientBase.class, SqlConnectOptions.class);
+      virtualField.set(sqlClientBase, getSqlConnectOptions());
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class QueryAdvice {
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.This SqlClientBase<?> sqlClientBase,
+        @Advice.Local("otelCallDepth") CallDepth callDepth) {
+      callDepth = CallDepth.forClass(SqlClientBase.class);
+      if (callDepth.getAndIncrement() > 0) {
+        return;
+      }
+
+      // set connection options to ThreadLocal, they will be read in QueryExecutor constructor
+      VirtualField<SqlClientBase<?>, SqlConnectOptions> virtualField =
+          VirtualField.find(SqlClientBase.class, SqlConnectOptions.class);
+      SqlConnectOptions sqlConnectOptions = virtualField.get(sqlClientBase);
+      setSqlConnectOptions(sqlConnectOptions);
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(
+        @Advice.Thrown Throwable throwable, @Advice.Local("otelCallDepth") CallDepth callDepth) {
+      if (callDepth.decrementAndGet() > 0) {
+        return;
+      }
+
+      setSqlConnectOptions(null);
+    }
+  }
+}

+ 43 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter;
+import javax.annotation.Nullable;
+
+public enum VertxSqlClientAttributesGetter
+    implements SqlClientAttributesGetter<VertxSqlClientRequest> {
+  INSTANCE;
+
+  @Override
+  public String getSystem(VertxSqlClientRequest request) {
+    return null;
+  }
+
+  @Override
+  @Nullable
+  public String getUser(VertxSqlClientRequest request) {
+    return request.getUser();
+  }
+
+  @Override
+  @Nullable
+  public String getName(VertxSqlClientRequest request) {
+    return request.getDatabase();
+  }
+
+  @Override
+  @Nullable
+  public String getConnectionString(VertxSqlClientRequest request) {
+    return null;
+  }
+
+  @Override
+  @Nullable
+  public String getRawStatement(VertxSqlClientRequest request) {
+    return request.getStatement();
+  }
+}

+ 35 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+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;
+
+@AutoService(InstrumentationModule.class)
+public class VertxSqlClientInstrumentationModule extends InstrumentationModule {
+
+  public VertxSqlClientInstrumentationModule() {
+    super("vertx-sql-client", "vertx-sql-client-4.0", "vertx");
+  }
+
+  @Override
+  public boolean isHelperClass(String className) {
+    return "io.vertx.sqlclient.impl.QueryExecutorUtil".equals(className);
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return asList(
+        new PoolInstrumentation(),
+        new SqlClientBaseInstrumentation(),
+        new QueryExecutorInstrumentation(),
+        new QueryResultBuilderInstrumentation());
+  }
+}

+ 32 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter;
+import javax.annotation.Nullable;
+
+public enum VertxSqlClientNetAttributesGetter
+    implements NetClientAttributesGetter<VertxSqlClientRequest, Void> {
+  INSTANCE;
+
+  @Nullable
+  @Override
+  public String getTransport(VertxSqlClientRequest request, @Nullable Void unused) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getPeerName(VertxSqlClientRequest request) {
+    return request.getHost();
+  }
+
+  @Nullable
+  @Override
+  public Integer getPeerPort(VertxSqlClientRequest request) {
+    return request.getPort();
+  }
+}

+ 38 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientRequest.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import io.vertx.sqlclient.SqlConnectOptions;
+
+public final class VertxSqlClientRequest {
+  private final String statement;
+  private final SqlConnectOptions sqlConnectOptions;
+
+  public VertxSqlClientRequest(String statement, SqlConnectOptions sqlConnectOptions) {
+    this.statement = statement;
+    this.sqlConnectOptions = sqlConnectOptions;
+  }
+
+  public String getStatement() {
+    return statement;
+  }
+
+  public String getUser() {
+    return sqlConnectOptions != null ? sqlConnectOptions.getUser() : null;
+  }
+
+  public String getDatabase() {
+    return sqlConnectOptions != null ? sqlConnectOptions.getDatabase() : null;
+  }
+
+  public String getHost() {
+    return sqlConnectOptions != null ? sqlConnectOptions.getHost() : null;
+  }
+
+  public Integer getPort() {
+    return sqlConnectOptions != null ? sqlConnectOptions.getPort() : null;
+  }
+}

+ 78 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java

@@ -0,0 +1,78 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor;
+import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig;
+import io.vertx.sqlclient.SqlConnectOptions;
+import java.util.Map;
+
+public final class VertxSqlClientSingletons {
+  public static final String OTEL_REQUEST_KEY = "otel.request";
+  public static final String OTEL_CONTEXT_KEY = "otel.context";
+  public static final String OTEL_PARENT_CONTEXT_KEY = "otel.parent-context";
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-sql-client-4.0";
+  private static final Instrumenter<VertxSqlClientRequest, Void> INSTRUMENTER;
+  private static final ThreadLocal<SqlConnectOptions> connectOptions = new ThreadLocal<>();
+
+  static {
+    SpanNameExtractor<VertxSqlClientRequest> spanNameExtractor =
+        DbClientSpanNameExtractor.create(VertxSqlClientAttributesGetter.INSTANCE);
+
+    InstrumenterBuilder<VertxSqlClientRequest, Void> builder =
+        Instrumenter.<VertxSqlClientRequest, Void>builder(
+                GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor)
+            .addAttributesExtractor(
+                SqlClientAttributesExtractor.builder(VertxSqlClientAttributesGetter.INSTANCE)
+                    .setStatementSanitizationEnabled(
+                        CommonConfig.get().isStatementSanitizationEnabled())
+                    .build())
+            .addAttributesExtractor(
+                NetClientAttributesExtractor.create(VertxSqlClientNetAttributesGetter.INSTANCE))
+            .addAttributesExtractor(
+                PeerServiceAttributesExtractor.create(
+                    VertxSqlClientNetAttributesGetter.INSTANCE,
+                    CommonConfig.get().getPeerServiceMapping()));
+
+    INSTRUMENTER = builder.buildInstrumenter(SpanKindExtractor.alwaysClient());
+  }
+
+  public static Instrumenter<VertxSqlClientRequest, Void> instrumenter() {
+    return INSTRUMENTER;
+  }
+
+  public static void setSqlConnectOptions(SqlConnectOptions sqlConnectOptions) {
+    connectOptions.set(sqlConnectOptions);
+  }
+
+  public static SqlConnectOptions getSqlConnectOptions() {
+    return connectOptions.get();
+  }
+
+  public static Scope endQuerySpan(Map<Object, Object> contextData, Throwable throwable) {
+    VertxSqlClientRequest otelRequest =
+        (VertxSqlClientRequest) contextData.remove(OTEL_REQUEST_KEY);
+    Context otelContext = (Context) contextData.remove(OTEL_CONTEXT_KEY);
+    Context otelParentContext = (Context) contextData.remove(OTEL_PARENT_CONTEXT_KEY);
+    if (otelRequest == null || otelContext == null || otelParentContext == null) {
+      return null;
+    }
+    instrumenter().end(otelContext, otelRequest, null, throwable);
+    return otelParentContext.makeCurrent();
+  }
+
+  private VertxSqlClientSingletons() {}
+}

+ 25 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/vertx/sqlclient/impl/QueryExecutorUtil.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.vertx.sqlclient.impl;
+
+import io.opentelemetry.instrumentation.api.util.VirtualField;
+import io.vertx.sqlclient.SqlConnectOptions;
+
+// Helper class for accessing virtual field on package private QueryExecutor class.
+public final class QueryExecutorUtil {
+  private static final VirtualField<QueryExecutor<?, ?, ?>, SqlConnectOptions> connectOptionsFiled =
+      VirtualField.find(QueryExecutor.class, SqlConnectOptions.class);
+
+  public static void setConnectOptions(Object queryExecutor, SqlConnectOptions connectOptions) {
+    connectOptionsFiled.set((QueryExecutor) queryExecutor, connectOptions);
+  }
+
+  public static SqlConnectOptions getConnectOptions(Object queryExecutor) {
+    return connectOptionsFiled.get((QueryExecutor) queryExecutor);
+  }
+
+  private QueryExecutorUtil() {}
+}

+ 251 - 0
instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java

@@ -0,0 +1,251 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME;
+import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT;
+
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.sdk.trace.data.StatusData;
+import io.vertx.core.Vertx;
+import io.vertx.pgclient.PgConnectOptions;
+import io.vertx.pgclient.PgException;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PoolOptions;
+import io.vertx.sqlclient.Tuple;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+
+class VertxSqlClientTest {
+  private static final Logger logger = LoggerFactory.getLogger(VertxSqlClientTest.class);
+
+  private static final String USER_DB = "SA";
+  private static final String PW_DB = "password123";
+  private static final String DB = "tempdb";
+
+  @RegisterExtension
+  private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  private static GenericContainer<?> container;
+  private static Vertx vertx;
+  private static Pool pool;
+  private static int port;
+
+  @BeforeAll
+  static void setUp() throws Exception {
+    container =
+        new GenericContainer<>("postgres:9.6.8")
+            .withEnv("POSTGRES_USER", USER_DB)
+            .withEnv("POSTGRES_PASSWORD", PW_DB)
+            .withEnv("POSTGRES_DB", DB)
+            .withExposedPorts(5432)
+            .withLogConsumer(new Slf4jLogConsumer(logger))
+            .withStartupTimeout(Duration.ofMinutes(2));
+    container.start();
+    vertx = Vertx.vertx();
+    port = container.getMappedPort(5432);
+    PgConnectOptions options =
+        new PgConnectOptions()
+            .setPort(port)
+            .setHost(container.getHost())
+            .setDatabase(DB)
+            .setUser(USER_DB)
+            .setPassword(PW_DB);
+    pool = Pool.pool(vertx, options, new PoolOptions().setMaxSize(4));
+    pool.query("create table test(id int primary key, name varchar(255))")
+        .execute()
+        .compose(
+            r ->
+                // insert some test data
+                pool.query("insert into test values (1, 'Hello'), (2, 'World')").execute())
+        .toCompletionStage()
+        .toCompletableFuture()
+        .get(30, TimeUnit.SECONDS);
+  }
+
+  @AfterAll
+  static void cleanUp() {
+    pool.close();
+    vertx.close();
+    container.stop();
+  }
+
+  @Test
+  void testSimpleSelect() throws Exception {
+    CompletableFuture<Object> result = new CompletableFuture<>();
+    result.whenComplete((rows, throwable) -> testing.runWithSpan("callback", () -> {}));
+    testing.runWithSpan(
+        "parent",
+        () ->
+            pool.query("select * from test")
+                .execute(
+                    rowSetAsyncResult -> {
+                      if (rowSetAsyncResult.succeeded()) {
+                        result.complete(rowSetAsyncResult.result());
+                      } else {
+                        result.completeExceptionally(rowSetAsyncResult.cause());
+                      }
+                    }));
+    result.get(30, TimeUnit.SECONDS);
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
+                span ->
+                    span.hasName("SELECT tempdb.test")
+                        .hasKind(SpanKind.CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(DB_NAME, DB),
+                            equalTo(DB_USER, USER_DB),
+                            equalTo(DB_STATEMENT, "select * from test"),
+                            equalTo(DB_OPERATION, "SELECT"),
+                            equalTo(DB_SQL_TABLE, "test"),
+                            equalTo(NET_PEER_NAME, "localhost"),
+                            equalTo(NET_PEER_PORT, port)),
+                span ->
+                    span.hasName("callback")
+                        .hasKind(SpanKind.INTERNAL)
+                        .hasParent(trace.getSpan(0))));
+  }
+
+  @Test
+  void testInvalidQuery() throws Exception {
+    CountDownLatch latch = new CountDownLatch(1);
+    CompletableFuture<Object> result = new CompletableFuture<>();
+    result.whenComplete(
+        (rows, throwable) -> testing.runWithSpan("callback", () -> latch.countDown()));
+    testing.runWithSpan(
+        "parent",
+        () ->
+            pool.query("invalid")
+                .execute(
+                    rowSetAsyncResult -> {
+                      if (rowSetAsyncResult.succeeded()) {
+                        result.complete(rowSetAsyncResult.result());
+                      } else {
+                        result.completeExceptionally(rowSetAsyncResult.cause());
+                      }
+                    }));
+
+    latch.await(30, TimeUnit.SECONDS);
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
+                span ->
+                    span.hasName("tempdb")
+                        .hasKind(SpanKind.CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasStatus(StatusData.error())
+                        .hasEventsSatisfyingExactly(
+                            event ->
+                                event
+                                    .hasName(EXCEPTION_EVENT_NAME)
+                                    .hasAttributesSatisfyingExactly(
+                                        equalTo(EXCEPTION_TYPE, PgException.class.getName()),
+                                        satisfies(
+                                            EXCEPTION_MESSAGE,
+                                            val -> val.contains("syntax error at or near")),
+                                        satisfies(
+                                            EXCEPTION_STACKTRACE,
+                                            val -> val.isInstanceOf(String.class))))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(DB_NAME, DB),
+                            equalTo(DB_USER, USER_DB),
+                            equalTo(DB_STATEMENT, "invalid"),
+                            equalTo(NET_PEER_NAME, "localhost"),
+                            equalTo(NET_PEER_PORT, port)),
+                span ->
+                    span.hasName("callback")
+                        .hasKind(SpanKind.INTERNAL)
+                        .hasParent(trace.getSpan(0))));
+  }
+
+  @Test
+  void testPreparedSelect() throws Exception {
+    testing
+        .runWithSpan(
+            "parent",
+            () -> pool.preparedQuery("select * from test where id = $1").execute(Tuple.of(1)))
+        .toCompletionStage()
+        .toCompletableFuture()
+        .get(30, TimeUnit.SECONDS);
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
+                span ->
+                    span.hasName("SELECT tempdb.test")
+                        .hasKind(SpanKind.CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(DB_NAME, DB),
+                            equalTo(DB_USER, USER_DB),
+                            equalTo(DB_STATEMENT, "select * from test where id = $?"),
+                            equalTo(DB_OPERATION, "SELECT"),
+                            equalTo(DB_SQL_TABLE, "test"),
+                            equalTo(NET_PEER_NAME, "localhost"),
+                            equalTo(NET_PEER_PORT, port))));
+  }
+
+  @Test
+  void testBatch() throws Exception {
+    testing
+        .runWithSpan(
+            "parent",
+            () ->
+                pool.preparedQuery("insert into test values ($1, $2) returning *")
+                    .executeBatch(Arrays.asList(Tuple.of(3, "Three"), Tuple.of(4, "Four"))))
+        .toCompletionStage()
+        .toCompletableFuture()
+        .get(30, TimeUnit.SECONDS);
+
+    testing.waitAndAssertTraces(
+        trace ->
+            trace.hasSpansSatisfyingExactly(
+                span -> span.hasName("parent").hasKind(SpanKind.INTERNAL),
+                span ->
+                    span.hasName("INSERT tempdb.test")
+                        .hasKind(SpanKind.CLIENT)
+                        .hasParent(trace.getSpan(0))
+                        .hasAttributesSatisfyingExactly(
+                            equalTo(DB_NAME, DB),
+                            equalTo(DB_USER, USER_DB),
+                            equalTo(DB_STATEMENT, "insert into test values ($?, $?) returning *"),
+                            equalTo(DB_OPERATION, "INSERT"),
+                            equalTo(DB_SQL_TABLE, "test"),
+                            equalTo(NET_PEER_NAME, "localhost"),
+                            equalTo(NET_PEER_PORT, port))));
+  }
+}

+ 1 - 0
settings.gradle.kts

@@ -518,6 +518,7 @@ hideFromDependabot(":instrumentation:vertx:vertx-http-client:vertx-http-client-c
 hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:javaagent")
 hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:testing")
 hideFromDependabot(":instrumentation:vertx:vertx-rx-java-3.5:javaagent")
+hideFromDependabot(":instrumentation:vertx:vertx-sql-client-4.0:javaagent")
 hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:javaagent")
 hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:testing")
 hideFromDependabot(":instrumentation:vibur-dbcp-11.0:javaagent")