diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/hibernate-reactive-2.0-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/AbstractHibernateReactiveTest.java b/instrumentation/hibernate/hibernate-reactive-1.0/hibernate-reactive-2.0-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/AbstractHibernateReactiveTest.java index 3edc954afa2a..caae4b0b01a5 100644 --- a/instrumentation/hibernate/hibernate-reactive-1.0/hibernate-reactive-2.0-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/AbstractHibernateReactiveTest.java +++ b/instrumentation/hibernate/hibernate-reactive-1.0/hibernate-reactive-2.0-testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v2_0/AbstractHibernateReactiveTest.java @@ -16,7 +16,9 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.POSTGRESQL; import static java.util.concurrent.TimeUnit.SECONDS; import io.opentelemetry.api.trace.Span; @@ -299,6 +301,9 @@ private void assertTrace() { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( diff --git a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java index cf081b7c23e7..36db636eba98 100644 --- a/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java +++ b/instrumentation/hibernate/hibernate-reactive-1.0/javaagent/src/hibernateReactive1Test/java/io/opentelemetry/javaagent/instrumentation/hibernate/reactive/v1_0/HibernateReactiveTest.java @@ -16,7 +16,9 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.POSTGRESQL; import static java.util.concurrent.TimeUnit.SECONDS; import io.opentelemetry.api.trace.Span; @@ -310,6 +312,9 @@ private static void assertTrace() { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts index e6e0739cedbe..a0ee394ae844 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/build.gradle.kts @@ -17,12 +17,14 @@ dependencies { compileOnly("io.vertx:vertx-web:$vertxVersion") compileOnly("io.vertx:vertx-rx-java2:$vertxVersion") + testInstrumentation(project(":instrumentation:executors:javaagent")) testInstrumentation(project(":instrumentation:jdbc:javaagent")) testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:rxjava:rxjava-2.0:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-http-client:vertx-http-client-3.0:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-http-client:vertx-http-client-4.0:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-http-client:vertx-http-client-5.0:javaagent")) + testInstrumentation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-4.0:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-5.0:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-web-3.0:javaagent")) } diff --git a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version5Test/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/server/VertxReactivePropagationTest.java b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version5Test/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/server/VertxReactivePropagationTest.java index 44ce5156217a..c368150ae84a 100644 --- a/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version5Test/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/server/VertxReactivePropagationTest.java +++ b/instrumentation/vertx/vertx-rx-java-3.5/javaagent/src/version5Test/java/io/opentelemetry/javaagent/instrumentation/vertx/reactive/server/VertxReactivePropagationTest.java @@ -27,9 +27,14 @@ import static io.opentelemetry.semconv.UrlAttributes.URL_QUERY; import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.HSQLDB; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.GlobalOpenTelemetry; @@ -120,10 +125,19 @@ void contextPropagation() { .hasKind(SpanKind.INTERNAL) .hasParent(trace.getSpan(1)), span -> - span.hasName("SELECT products") + span.hasName( + emitStableDatabaseSemconv() + ? "SELECT products" + : "SELECT test.products") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(2)) .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), HSQLDB), + equalTo(maybeStable(DB_NAME), "test"), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : "SA"), + equalTo( + DB_CONNECTION_STRING, + emitStableDatabaseSemconv() ? null : "hsqldb:mem:"), equalTo( maybeStable(DB_STATEMENT), "SELECT id, name, price, weight FROM products"), @@ -220,24 +234,33 @@ void highConcurrency() { .hasAttributesSatisfyingExactly( equalTo(longKey(TEST_REQUEST_ID_ATTRIBUTE), requestId)), span -> - span.hasName("SELECT products") + span.hasName( + emitStableDatabaseSemconv() + ? "SELECT products" + : "SELECT test.products") .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(3)) .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), HSQLDB), + equalTo(maybeStable(DB_NAME), "test"), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : "SA"), + equalTo( + DB_CONNECTION_STRING, + emitStableDatabaseSemconv() ? null : "hsqldb:mem:"), equalTo( maybeStable(DB_STATEMENT), "SELECT id AS request" + requestId + ", name, price, weight FROM products"), - equalTo( - DB_QUERY_SUMMARY, - emitStableDatabaseSemconv() ? "SELECT products" : null), equalTo( maybeStable(DB_OPERATION), emitStableDatabaseSemconv() ? null : "SELECT"), equalTo( maybeStable(DB_SQL_TABLE), - emitStableDatabaseSemconv() ? null : "products"))); + emitStableDatabaseSemconv() ? null : "products"), + equalTo( + DB_QUERY_SUMMARY, + emitStableDatabaseSemconv() ? "SELECT products" : null))); }); } testing.waitAndAssertTraces(assertions); diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/build.gradle.kts index 70b495c86b95..cf91da670427 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/build.gradle.kts @@ -18,13 +18,18 @@ dependencies { implementation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-common:javaagent")) + testInstrumentation(project(":instrumentation:jdbc:javaagent")) testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-5.0:javaagent")) testLibrary("io.vertx:vertx-pg-client:$version") + testImplementation("io.vertx:vertx-jdbc-client:$version") + testImplementation("io.agroal:agroal-pool:1.9") + testImplementation("org.hsqldb:hsqldb:2.3.4") latestDepTestLibrary("io.vertx:vertx-sql-client:4.+") // see vertx-sql-client-5.0 module latestDepTestLibrary("io.vertx:vertx-pg-client:4.+") // see vertx-sql-client-5.0 module + latestDepTestLibrary("io.vertx:vertx-jdbc-client:4.+") // see vertx-sql-client-5.0 module latestDepTestLibrary("io.vertx:vertx-codegen:4.+") // see vertx-sql-client-5.0 module } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java index 27f73b7a2d0e..b1a1fcacef0f 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java @@ -7,11 +7,14 @@ 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.sql.VertxSqlClientUtil.getDbSystemNameFromClassName; import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.getPoolSqlConnectOptions; import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.setPoolConnectOptions; import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.setSqlConnectOptions; import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.wrapContext; import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.attachConnectOptions; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.storeConnectOptionsDbSystem; +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.isStatic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; @@ -45,12 +48,16 @@ public ElementMatcher typeMatcher() { @Override public void transform(TypeTransformer transformer) { + // In vertx 4.x, database-specific sub-interfaces like PgPool and MySQLPool declare their own + // static pool() methods that take subtypes of SqlConnectOptions (e.g. PgConnectOptions) and + // return subtypes of Pool. These are independent static methods, not overrides, and have their + // own separate code paths. hasSuperType is needed to match these variant signatures. transformer.applyAdviceToMethod( named("pool") .and(isStatic()) .and(takesArguments(3)) - .and(takesArgument(1, named("io.vertx.sqlclient.SqlConnectOptions"))) - .and(returns(named("io.vertx.sqlclient.Pool"))), + .and(takesArgument(1, hasSuperType(named("io.vertx.sqlclient.SqlConnectOptions")))) + .and(returns(hasSuperType(named("io.vertx.sqlclient.Pool")))), PoolInstrumentation.class.getName() + "$PoolAdvice"); transformer.applyAdviceToMethod( @@ -82,6 +89,10 @@ public static void onExit( } setPoolConnectOptions(pool, sqlConnectOptions); + // Detect db system from pool implementation class (e.g. PgPool -> postgresql). + // This handles cases where connect options is a generic SqlConnectOptions + // but the pool is database-specific (e.g. Hibernate Reactive). + storeConnectOptionsDbSystem(sqlConnectOptions, getDbSystemNameFromClassName(pool)); setSqlConnectOptions(null); } } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java index 7aaced9c1a52..41743b74541e 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java @@ -19,6 +19,7 @@ import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientRequest; import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil; import io.vertx.core.impl.future.PromiseInternal; +import io.vertx.sqlclient.SqlConnectOptions; import io.vertx.sqlclient.impl.PreparedStatement; import io.vertx.sqlclient.impl.QueryExecutorUtil; import javax.annotation.Nullable; @@ -102,9 +103,21 @@ public static AdviceScope start(Object queryExecutor, Object[] arguments) { return new AdviceScope(callDepth); } + SqlConnectOptions connectOptions = QueryExecutorUtil.getConnectOptions(queryExecutor); + // connectOptions is null when the pool was created via JDBCPool which bypasses the + // Pool.pool() factory, in that case we skip vertx-sql-client span creation and let JDBC + // instrumentation handle it + if (connectOptions == null) { + return new AdviceScope(callDepth); + } + // Try db system stored from pool class first (handles generic SqlConnectOptions), + // fall back to class name detection on the connect options itself + String dbSystem = VertxSqlClientSingletons.getConnectOptionsDbSystem(connectOptions); + if (dbSystem == null) { + dbSystem = VertxSqlClientUtil.getDbSystemNameFromClassName(connectOptions); + } VertxSqlClientRequest otelRequest = - new VertxSqlClientRequest( - sql, QueryExecutorUtil.getConnectOptions(queryExecutor), preparedStatement); + new VertxSqlClientRequest(sql, connectOptions, preparedStatement, dbSystem); Context parentContext = Context.current(); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return new AdviceScope(callDepth, null, null, null); diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java index 49016a4e6676..e78e6ace88c5 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java @@ -13,6 +13,7 @@ import io.vertx.sqlclient.SqlConnectOptions; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.impl.SqlClientBase; +import javax.annotation.Nullable; public final class VertxSqlClientSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-sql-client-4.0"; @@ -26,6 +27,26 @@ public static Instrumenter instrumenter() { private static final VirtualField, SqlConnectOptions> connectOptionsField = VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); + private static final VirtualField connectOptionsDbSystem = + VirtualField.find(SqlConnectOptions.class, String.class); + + public static void storeConnectOptionsDbSystem( + SqlConnectOptions connectOptions, String dbSystem) { + if (connectOptions != null) { + connectOptionsDbSystem.set(connectOptions, dbSystem); + } + } + + @Nullable + public static String getConnectOptionsDbSystem(SqlConnectOptions connectOptions) { + if (connectOptions != null) { + return connectOptionsDbSystem.get(connectOptions); + } + // null when db system was not captured at pool creation time; callers should fall back + // to getDbSystemNameFromClassName() on the connect options instance + return null; + } + public static SqlConnectOptions getSqlConnectOptions(SqlClientBase sqlClientBase) { return connectOptionsField.get(sqlClientBase); } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxJdbcClientTest.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxJdbcClientTest.java new file mode 100644 index 000000000000..dd45b07ef79e --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxJdbcClientTest.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.vertx.core.Vertx; +import io.vertx.jdbcclient.JDBCConnectOptions; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +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; + +/** + * Tests that vertx-sql-client instrumentation is suppressed for JDBC-backed connections and JDBC + * instrumentation handles them instead. + */ +@SuppressWarnings("deprecation") // using deprecated semconv +class VertxJdbcClientTest { + + private static final String DB = "testdb"; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static Vertx vertx; + private static Pool pool; + + @BeforeAll + static void setUp() throws Exception { + vertx = Vertx.vertx(); + pool = + JDBCPool.pool( + vertx, + new JDBCConnectOptions().setJdbcUrl("jdbc:hsqldb:mem:" + DB), + new PoolOptions().setMaxSize(4)); + pool.query("create table test(id int primary key, name varchar(255))") + .execute() + .compose(r -> pool.query("insert into test values (1, 'Hello'), (2, 'World')").execute()) + .toCompletionStage() + .toCompletableFuture() + .get(30, SECONDS); + } + + @AfterAll + static void cleanUp() { + pool.close(); + vertx.close(); + } + + @Test + void testSimpleSelect() throws Exception { + testing + .runWithSpan("parent", () -> pool.query("select * from test").execute()) + .toCompletionStage() + .toCompletableFuture() + .get(30, SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName( + emitStableDatabaseSemconv() ? "SELECT test" : "SELECT " + DB + ".test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName("hsqldb")), + equalTo(maybeStable(DB_NAME), DB), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : "SA"), + equalTo( + DB_CONNECTION_STRING, + emitStableDatabaseSemconv() ? null : "hsqldb:mem:"), + equalTo(maybeStable(DB_STATEMENT), "select * from test"), + equalTo( + DB_QUERY_SUMMARY, + emitStableDatabaseSemconv() ? "SELECT test" : null), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? null : "SELECT"), + equalTo( + maybeStable(DB_SQL_TABLE), + emitStableDatabaseSemconv() ? null : "test")))); + } +} diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java index 04fb7f629ae9..fa6ce6901252 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java @@ -21,7 +21,9 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.POSTGRESQL; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.SECONDS; @@ -143,6 +145,9 @@ void testSimpleSelect() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test"), @@ -207,6 +212,9 @@ void testInvalidQuery() throws Exception { EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "invalid"), @@ -249,6 +257,9 @@ private static void assertPreparedSelect() { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test where id = $1"), @@ -287,6 +298,9 @@ void testBatch() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( @@ -384,6 +398,9 @@ void testManyQueries() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test"), @@ -458,6 +475,9 @@ void testConcurrency() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/build.gradle.kts index 9a02c3c482f2..413b463b839b 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/build.gradle.kts +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/build.gradle.kts @@ -22,10 +22,14 @@ dependencies { implementation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-common:javaagent")) + testInstrumentation(project(":instrumentation:jdbc:javaagent")) testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:vertx:vertx-sql-client:vertx-sql-client-4.0:javaagent")) testLibrary("io.vertx:vertx-pg-client:$version") + testImplementation("io.vertx:vertx-jdbc-client:$version") + testImplementation("io.agroal:agroal-pool:2.5") + testImplementation("org.hsqldb:hsqldb:2.3.4") } val collectMetadata = findProperty("collectMetadata")?.toString() ?: "false" diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/CommandSchedulerInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/CommandSchedulerInstrumentation.java new file mode 100644 index 000000000000..9fe2ec4a5ec0 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/CommandSchedulerInstrumentation.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql; + +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.sql.VertxSqlClientSingletons.getCommandContext; +import static io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql.VertxSqlClientSingletons.setCommandContext; +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.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.sqlclient.internal.command.CommandBase; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Propagates OpenTelemetry context through the Vert.x SQL client connection pool. + * + *

When a query is initiated (e.g. {@code pool.query("SELECT ...").execute()}), the correct + * OpenTelemetry context is active on the event loop thread. The pool may queue the request and + * dispatch it later when a connection becomes available — by which time the event loop may be + * handling a different request with a different context. + * + *

This instrumentation captures the context on the first {@code CommandScheduler.schedule()} + * call (from the query executor, with the correct context) and restores it on subsequent calls + * (from the pool to the connection, where the context may be stale). This ensures that downstream + * instrumentation (e.g. JDBC) on worker threads sees the correct parent context. + */ +public class CommandSchedulerInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.sqlclient.internal.command.CommandScheduler"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.sqlclient.internal.command.CommandScheduler")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("schedule") + .and(takesArgument(0, named("io.vertx.sqlclient.internal.command.CommandBase"))) + .and(takesArgument(1, named("io.vertx.core.Completable"))), + getClass().getName() + "$ScheduleAdvice"); + } + + @SuppressWarnings("unused") + public static class ScheduleAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + @Nullable + public static Scope onEnter(@Advice.Argument(0) CommandBase command) { + Context stored = getCommandContext(command); + if (stored == null) { + // First schedule call (query executor → pool or direct connection). + // The current OpenTelemetry context is correct — store it on the command. + setCommandContext(command, Context.current()); + return null; + } + // Subsequent schedule call (pool → connection). + // Restore the stored context so that executeBlocking dispatches with + // the correct parent for downstream instrumentation (e.g. JDBC). + return stored.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter @Nullable Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/DriverInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/DriverInstrumentation.java new file mode 100644 index 000000000000..6e215be22dc2 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/DriverInstrumentation.java @@ -0,0 +1,57 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql; + +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.sql.VertxSqlClientUtil.getDbSystemNameFromClassName; +import static io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql.VertxSqlClientSingletons.storePoolDbSystem; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.sqlclient.Pool; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class DriverInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("io.vertx.sqlclient.spi.Driver"); + } + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.vertx.sqlclient.spi.Driver")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("newPool") + .and(not(isStatic())) + .and(takesArguments(6)) + .and(returns(named("io.vertx.sqlclient.Pool"))), + DriverInstrumentation.class.getName() + "$NewPoolAdvice"); + } + + @SuppressWarnings("unused") + public static class NewPoolAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This Object driver, @Advice.Return Pool pool) { + if (pool != null) { + storePoolDbSystem(pool, getDbSystemNameFromClassName(driver)); + } + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/PoolInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/PoolInstrumentation.java index bb560548a3f4..28a1b5210395 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/PoolInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/PoolInstrumentation.java @@ -12,6 +12,7 @@ import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.setSqlConnectOptions; import static io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil.wrapContext; import static io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql.VertxSqlClientSingletons.attachConnectOptions; +import static io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql.VertxSqlClientSingletons.resolveAndStoreDbSystem; import static net.bytebuddy.matcher.ElementMatchers.isStatic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; @@ -40,7 +41,10 @@ public ElementMatcher classLoaderOptimization() { @Override public ElementMatcher typeMatcher() { - return implementsInterface(named("io.vertx.sqlclient.Pool")); + // Match both the Pool interface (for static pool() factory methods) and classes/interfaces + // that implement/extend Pool (for instance methods like getConnection()) + return implementsInterface(named("io.vertx.sqlclient.Pool")) + .or(named("io.vertx.sqlclient.Pool")); } @Override @@ -83,6 +87,7 @@ public static void onExit( } setPoolConnectOptions(pool, sqlConnectOptions); + resolveAndStoreDbSystem(pool, sqlConnectOptions); setSqlConnectOptions(null); } } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/QueryExecutorInstrumentation.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/QueryExecutorInstrumentation.java index 343e6b0cfb4a..785cd29e8a96 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/QueryExecutorInstrumentation.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/QueryExecutorInstrumentation.java @@ -19,6 +19,7 @@ import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientRequest; import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientUtil; import io.vertx.core.internal.PromiseInternal; +import io.vertx.sqlclient.SqlConnectOptions; import io.vertx.sqlclient.impl.QueryExecutorUtil; import io.vertx.sqlclient.internal.PreparedStatement; import javax.annotation.Nullable; @@ -101,9 +102,16 @@ public static AdviceScope start(Object queryExecutor, Object[] arguments) { return new AdviceScope(callDepth); } + SqlConnectOptions connectOptions = QueryExecutorUtil.getConnectOptions(queryExecutor); + // connectOptions is null when the pool was created via JDBCPool which bypasses the + // Pool.pool() factory, in that case we skip vertx-sql-client span creation and let JDBC + // instrumentation handle it + if (connectOptions == null) { + return new AdviceScope(callDepth); + } + String dbSystem = VertxSqlClientSingletons.getConnectOptionsDbSystem(connectOptions); VertxSqlClientRequest otelRequest = - new VertxSqlClientRequest( - sql, QueryExecutorUtil.getConnectOptions(queryExecutor), preparedStatement); + new VertxSqlClientRequest(sql, connectOptions, preparedStatement, dbSystem); Context parentContext = Context.current(); if (!instrumenter().shouldStart(parentContext, otelRequest)) { return new AdviceScope(callDepth); diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientInstrumentationModule.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientInstrumentationModule.java index 25c2c7da6b4f..d5d344e1f725 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientInstrumentationModule.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientInstrumentationModule.java @@ -43,6 +43,8 @@ public List injectedClassNames() { @Override public List typeInstrumentations() { return asList( + new CommandSchedulerInstrumentation(), + new DriverInstrumentation(), new PoolInstrumentation(), new SqlClientBaseInstrumentation(), new QueryExecutorInstrumentation(), diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientSingletons.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientSingletons.java index ba3ffec19dfe..32f8dab63581 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientSingletons.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientSingletons.java @@ -5,14 +5,19 @@ package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.OTHER_SQL; + +import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlClientRequest; import io.opentelemetry.javaagent.instrumentation.vertx.sql.VertxSqlInstrumenterFactory; import io.vertx.core.Future; +import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.SqlConnectOptions; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.internal.SqlClientBase; +import io.vertx.sqlclient.internal.command.CommandBase; public final class VertxSqlClientSingletons { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-sql-client-5.0"; @@ -23,9 +28,53 @@ public static Instrumenter instrumenter() { return INSTRUMENTER; } + private static final VirtualField poolDbSystem = + VirtualField.find(Pool.class, String.class); + + private static final VirtualField connectOptionsDbSystem = + VirtualField.find(SqlConnectOptions.class, String.class); + private static final VirtualField connectOptionsField = VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); + // CommandBase is a generic type; VirtualField.find requires the raw type + @SuppressWarnings("rawtypes") + private static final VirtualField commandContextField = + VirtualField.find(CommandBase.class, Context.class); + + // CommandBase is a generic type used as VirtualField key + @SuppressWarnings("rawtypes") + public static Context getCommandContext(CommandBase command) { + return commandContextField.get(command); + } + + // CommandBase is a generic type used as VirtualField key + @SuppressWarnings("rawtypes") + public static void setCommandContext(CommandBase command, Context context) { + commandContextField.set(command, context); + } + + public static void storePoolDbSystem(Pool pool, String dbSystem) { + poolDbSystem.set(pool, dbSystem); + } + + public static String getConnectOptionsDbSystem(SqlConnectOptions sqlConnectOptions) { + if (sqlConnectOptions != null) { + String dbSystem = connectOptionsDbSystem.get(sqlConnectOptions); + if (dbSystem != null) { + return dbSystem; + } + } + return OTHER_SQL; + } + + public static void resolveAndStoreDbSystem(Pool pool, SqlConnectOptions sqlConnectOptions) { + String dbSystem = poolDbSystem.get(pool); + if (sqlConnectOptions != null && dbSystem != null) { + connectOptionsDbSystem.set(sqlConnectOptions, dbSystem); + } + } + public static SqlConnectOptions getSqlConnectOptions(SqlClientBase sqlClientBase) { return connectOptionsField.get(sqlClientBase); } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxJdbcClientTest.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxJdbcClientTest.java new file mode 100644 index 000000000000..1a2135ddbb27 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxJdbcClientTest.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v5_0.sql; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.DbAttributes.DB_QUERY_SUMMARY; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static java.util.concurrent.TimeUnit.SECONDS; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.vertx.core.Vertx; +import io.vertx.jdbcclient.JDBCConnectOptions; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +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; + +/** + * Tests that vertx-sql-client instrumentation is suppressed for JDBC-backed connections and JDBC + * instrumentation handles them instead. + */ +@SuppressWarnings("deprecation") // using deprecated semconv +class VertxJdbcClientTest { + + private static final String DB = "testdb"; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static Vertx vertx; + private static Pool pool; + + @BeforeAll + static void setUp() throws Exception { + vertx = Vertx.vertx(); + pool = + JDBCPool.pool( + vertx, + new JDBCConnectOptions().setJdbcUrl("jdbc:hsqldb:mem:" + DB), + new PoolOptions().setMaxSize(4)); + pool.query("create table test(id int primary key, name varchar(255))") + .execute() + .compose(r -> pool.query("insert into test values (1, 'Hello'), (2, 'World')").execute()) + .toCompletionStage() + .toCompletableFuture() + .get(30, SECONDS); + } + + @AfterAll + static void cleanUp() { + pool.close(); + vertx.close(); + } + + @Test + void testSimpleSelect() throws Exception { + testing + .runWithSpan("parent", () -> pool.query("select * from test").execute()) + .toCompletionStage() + .toCompletableFuture() + .get(30, SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName( + emitStableDatabaseSemconv() ? "SELECT test" : "SELECT " + DB + ".test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName("hsqldb")), + equalTo(maybeStable(DB_NAME), DB), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : "SA"), + equalTo( + DB_CONNECTION_STRING, + emitStableDatabaseSemconv() ? null : "hsqldb:mem:"), + equalTo(maybeStable(DB_STATEMENT), "select * from test"), + equalTo( + DB_QUERY_SUMMARY, + emitStableDatabaseSemconv() ? "SELECT test" : null), + equalTo( + maybeStable(DB_OPERATION), + emitStableDatabaseSemconv() ? null : "SELECT"), + equalTo( + maybeStable(DB_SQL_TABLE), + emitStableDatabaseSemconv() ? null : "test")))); + } +} diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientTest.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientTest.java index 3b72b864149c..4228aa6c7f7e 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientTest.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-5.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v5_0/sql/VertxSqlClientTest.java @@ -21,7 +21,9 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.POSTGRESQL; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.SECONDS; @@ -144,6 +146,9 @@ void testSimpleSelect() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test"), @@ -214,6 +219,9 @@ void testInvalidQuery() throws Exception { EXCEPTION_STACKTRACE, val -> val.isInstanceOf(String.class)))) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "invalid"), @@ -250,6 +258,9 @@ private static void assertPreparedSelect() { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test where id = $1"), @@ -288,6 +299,9 @@ void testBatch() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( @@ -386,6 +400,9 @@ void testManyQueries() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo(maybeStable(DB_STATEMENT), "select * from test"), @@ -460,6 +477,9 @@ void testConcurrency() throws Exception { .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + emitStableDatabaseSemconv() ? POSTGRESQL : null), equalTo(maybeStable(DB_NAME), DB), equalTo(DB_USER, emitStableDatabaseSemconv() ? null : USER_DB), equalTo( diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientAttributesGetter.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientAttributesGetter.java index 4051e7871312..3f25f403bb15 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientAttributesGetter.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientAttributesGetter.java @@ -5,7 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.vertx.sql; +import static io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlDialect.DOUBLE_QUOTES_ARE_IDENTIFIERS; import static io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlDialect.DOUBLE_QUOTES_ARE_STRING_LITERALS; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.MICROSOFT_SQL_SERVER; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.MYSQL; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.POSTGRESQL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.IBM_DB2; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ORACLE_DB; import static java.util.Collections.singleton; import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter; @@ -25,18 +31,34 @@ enum VertxSqlClientAttributesGetter @Override public String getDbSystemName(VertxSqlClientRequest request) { + return request.getDbSystemName(); + } + + @Deprecated // to be removed in 3.0 + @Override + @Nullable + public String getDbSystem(VertxSqlClientRequest request) { + // preserving old behavior: db.system was never set for vertx sql client return null; } @Override public SqlDialect getSqlDialect(VertxSqlClientRequest request) { - // the underlying database is unknown, use the safer default that sanitizes double-quoted - // fragments as string literals (note that this can lead to incorrect summarization - // for databases that do use double quotes as identifiers) - // - // TODO do better in - // https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16254 - return DOUBLE_QUOTES_ARE_STRING_LITERALS; + switch (request.getDbSystemName()) { + case MYSQL: + // MySQL treats double-quoted fragments as string literals by default + return DOUBLE_QUOTES_ARE_STRING_LITERALS; + case MICROSOFT_SQL_SERVER: + // SQL Server can treat double quotes as string literals when QUOTED_IDENTIFIER is OFF. + return DOUBLE_QUOTES_ARE_STRING_LITERALS; + case POSTGRESQL: + case ORACLE_DB: + case IBM_DB2: + // These databases treat double-quoted fragments as identifiers + return DOUBLE_QUOTES_ARE_IDENTIFIERS; + default: + return DOUBLE_QUOTES_ARE_STRING_LITERALS; + } } @Deprecated // to be removed in 3.0 diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientRequest.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientRequest.java index ca277fee08d2..f33fbad95dd9 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientRequest.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientRequest.java @@ -8,15 +8,21 @@ import io.vertx.sqlclient.SqlConnectOptions; public final class VertxSqlClientRequest { + private final String queryText; private final SqlConnectOptions sqlConnectOptions; private final boolean parameterizedQuery; + private final String dbSystemName; public VertxSqlClientRequest( - String queryText, SqlConnectOptions sqlConnectOptions, boolean parameterizedQuery) { + String queryText, + SqlConnectOptions sqlConnectOptions, + boolean parameterizedQuery, + String dbSystemName) { this.queryText = queryText; this.sqlConnectOptions = sqlConnectOptions; this.parameterizedQuery = parameterizedQuery; + this.dbSystemName = dbSystemName; } public String getQueryText() { @@ -42,4 +48,8 @@ public Integer getPort() { public boolean isParameterizedQuery() { return parameterizedQuery; } + + public String getDbSystemName() { + return dbSystemName; + } } diff --git a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientUtil.java b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientUtil.java index 470ed8d1977b..f2d291bb9c47 100644 --- a/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientUtil.java +++ b/instrumentation/vertx/vertx-sql-client/vertx-sql-client-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/sql/VertxSqlClientUtil.java @@ -5,6 +5,13 @@ package io.opentelemetry.javaagent.instrumentation.vertx.sql; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.MICROSOFT_SQL_SERVER; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.MYSQL; +import static io.opentelemetry.semconv.DbAttributes.DbSystemNameValues.POSTGRESQL; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.IBM_DB2; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.ORACLE_DB; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues.OTHER_SQL; + import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -13,9 +20,13 @@ import io.vertx.core.Promise; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.SqlConnectOptions; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; public final class VertxSqlClientUtil { + private static final ThreadLocal connectOptions = new ThreadLocal<>(); public static void setSqlConnectOptions(SqlConnectOptions sqlConnectOptions) { @@ -29,6 +40,9 @@ public static SqlConnectOptions getSqlConnectOptions() { private static final VirtualField poolConnectOptions = VirtualField.find(Pool.class, SqlConnectOptions.class); + private static final Map DB_SYSTEM_NAME_BY_PACKAGE = + buildPackageDbSystemNameMap(); + public static void setPoolConnectOptions(Pool pool, SqlConnectOptions sqlConnectOptions) { poolConnectOptions.set(pool, sqlConnectOptions); } @@ -37,6 +51,30 @@ public static SqlConnectOptions getPoolSqlConnectOptions(Pool pool) { return poolConnectOptions.get(pool); } + public static String getDbSystemNameFromClassName(@Nullable Object instance) { + if (instance != null) { + String className = instance.getClass().getName(); + for (Map.Entry entry : DB_SYSTEM_NAME_BY_PACKAGE.entrySet()) { + if (className.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + } + return OTHER_SQL; + } + + // See https://github.com/eclipse-vertx/vertx-sql-client for the full list of supported + // database-specific client modules + private static Map buildPackageDbSystemNameMap() { + Map map = new HashMap<>(); + map.put("io.vertx.pgclient.", POSTGRESQL); + map.put("io.vertx.mysqlclient.", MYSQL); + map.put("io.vertx.mssqlclient.", MICROSOFT_SQL_SERVER); + map.put("io.vertx.oracleclient.", ORACLE_DB); + map.put("io.vertx.db2client.", IBM_DB2); + return map; + } + private static final VirtualField, RequestData> requestDataField = VirtualField.find(Promise.class, RequestData.class);