diff --git a/instrumentation/jdbc/README.md b/instrumentation/jdbc/README.md index 7e3440fd8c01..720aaefbd5e3 100644 --- a/instrumentation/jdbc/README.md +++ b/instrumentation/jdbc/README.md @@ -1,5 +1,6 @@ # Settings for the JDBC instrumentation -| System property | Type | Default | Description | -|---------------------------------------------------------|---------|---------|----------------------------------------| -| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| System property | Type | Default | Description | +|--------------------------------------------------------------|---------|---------|------------------------------------------------------------------------------------------| +| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| `otel.instrumentation.jdbc.experimental.transaction.enabled` | Boolean | `false` | Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations. | diff --git a/instrumentation/jdbc/javaagent/build.gradle.kts b/instrumentation/jdbc/javaagent/build.gradle.kts index ac4617836eaf..10720fb42a6c 100644 --- a/instrumentation/jdbc/javaagent/build.gradle.kts +++ b/instrumentation/jdbc/javaagent/build.gradle.kts @@ -91,3 +91,9 @@ tasks { dependsOn(testSlickStableSemconv) } } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.jdbc.experimental.transaction.enabled=true") + } +} diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java index f739db400778..3f356fb6db32 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java @@ -5,17 +5,27 @@ package io.opentelemetry.javaagent.instrumentation.jdbc; +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.transactionInstrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcData; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.sql.Connection; import java.sql.PreparedStatement; +import java.util.Locale; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -40,6 +50,9 @@ public void transform(TypeTransformer transformer) { // Also include CallableStatement, which is a sub type of PreparedStatement .and(returns(implementsInterface(named("java.sql.PreparedStatement")))), ConnectionInstrumentation.class.getName() + "$PrepareAdvice"); + transformer.applyAdviceToMethod( + namedOneOf("commit", "rollback").and(takesNoArguments()).and(isPublic()), + ConnectionInstrumentation.class.getName() + "$TransactionAdvice"); } @SuppressWarnings("unused") @@ -51,4 +64,39 @@ public static void addDbInfo( JdbcData.preparedStatement.set(statement, sql); } } + + @SuppressWarnings("unused") + public static class TransactionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Connection connection, + @Advice.Origin("#m") String methodName, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + Context parentContext = currentContext(); + DbRequest request = + DbRequest.createTransaction(connection, methodName.toUpperCase(Locale.ROOT)); + + if (request == null || !transactionInstrumenter().shouldStart(parentContext, request)) { + return; + } + + context = transactionInstrumenter().start(parentContext, request); + scope = context.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelRequest") DbRequest request, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (scope == null) { + return; + } + scope.close(); + transactionInstrumenter().end(context, request, null, throwable); + } + } } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java index e9c5290aa217..1b474d833d7a 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java @@ -8,51 +8,50 @@ import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics; -import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; -import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; -import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; -import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; -import io.opentelemetry.instrumentation.jdbc.internal.JdbcAttributesGetter; +import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory; import io.opentelemetry.instrumentation.jdbc.internal.JdbcNetworkAttributesGetter; import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; import io.opentelemetry.javaagent.bootstrap.jdbc.DbInfo; +import java.util.Collections; import javax.sql.DataSource; public final class JdbcSingletons { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jdbc"; - private static final Instrumenter STATEMENT_INSTRUMENTER; + private static final Instrumenter TRANSACTION_INSTRUMENTER; public static final Instrumenter DATASOURCE_INSTRUMENTER = createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true); static { - JdbcAttributesGetter dbAttributesGetter = new JdbcAttributesGetter(); JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter(); + AttributesExtractor peerServiceExtractor = + PeerServiceAttributesExtractor.create( + netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver()); STATEMENT_INSTRUMENTER = - Instrumenter.builder( - GlobalOpenTelemetry.get(), - INSTRUMENTATION_NAME, - DbClientSpanNameExtractor.create(dbAttributesGetter)) - .addAttributesExtractor( - SqlClientAttributesExtractor.builder(dbAttributesGetter) - .setStatementSanitizationEnabled( - AgentInstrumentationConfig.get() - .getBoolean( - "otel.instrumentation.jdbc.statement-sanitizer.enabled", - AgentCommonConfig.get().isStatementSanitizationEnabled())) - .build()) - .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver())) - .addOperationMetrics(DbClientMetrics.get()) - .buildInstrumenter(SpanKindExtractor.alwaysClient()); + JdbcInstrumenterFactory.createStatementInstrumenter( + GlobalOpenTelemetry.get(), + Collections.singletonList(peerServiceExtractor), + true, + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.jdbc.statement-sanitizer.enabled", + AgentCommonConfig.get().isStatementSanitizationEnabled())); + + TRANSACTION_INSTRUMENTER = + JdbcInstrumenterFactory.createTransactionInstrumenter( + GlobalOpenTelemetry.get(), + Collections.singletonList(peerServiceExtractor), + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false)); + } + + public static Instrumenter transactionInstrumenter() { + return TRANSACTION_INSTRUMENTER; } public static Instrumenter statementInstrumenter() { diff --git a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/JdbcInstrumentationTest.java b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/JdbcInstrumentationTest.java index 369a09f31fcf..2050cef44978 100644 --- a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/JdbcInstrumentationTest.java +++ b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/JdbcInstrumentationTest.java @@ -1632,4 +1632,128 @@ void testPreparedBatch(String system, Connection connection, String username, St DB_OPERATION_BATCH_SIZE, emitStableDatabaseSemconv() ? 2L : null)))); } + + @ParameterizedTest + @MethodSource("transactionOperationsStream") + void testCommitTransaction(String system, Connection connection, String username, String url) + throws SQLException { + + String tableName = "TXN_COMMIT_TEST_" + system.toUpperCase(Locale.ROOT); + Statement createTable = connection.createStatement(); + createTable.execute("CREATE TABLE " + tableName + " (id INTEGER not NULL, PRIMARY KEY ( id ))"); + cleanup.deferCleanup(createTable); + + connection.setAutoCommit(false); + + testing.waitForTraces(1); + testing.clearData(); + + Statement insertStatement = connection.createStatement(); + cleanup.deferCleanup(insertStatement); + + testing.runWithSpan( + "parent", + () -> { + insertStatement.executeUpdate("INSERT INTO " + tableName + " VALUES(1)"); + connection.commit(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("INSERT jdbcunittest." + tableName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo( + maybeStable(DB_STATEMENT), + "INSERT INTO " + tableName + " VALUES(?)"), + equalTo(maybeStable(DB_OPERATION), "INSERT"), + equalTo(maybeStable(DB_SQL_TABLE), tableName)), + span -> + span.hasName("COMMIT") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(maybeStable(DB_OPERATION), "COMMIT"), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo( + DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url)))); + } + + @ParameterizedTest + @MethodSource("transactionOperationsStream") + void testRollbackTransaction(String system, Connection connection, String username, String url) + throws SQLException { + + String tableName = "TXN_ROLLBACK_TEST_" + system.toUpperCase(Locale.ROOT); + Statement createTable = connection.createStatement(); + createTable.execute("CREATE TABLE " + tableName + " (id INTEGER not NULL, PRIMARY KEY ( id ))"); + cleanup.deferCleanup(createTable); + + connection.setAutoCommit(false); + + testing.waitForTraces(1); + testing.clearData(); + + Statement insertStatement = connection.createStatement(); + cleanup.deferCleanup(insertStatement); + + testing.runWithSpan( + "parent", + () -> { + insertStatement.executeUpdate("INSERT INTO " + tableName + " VALUES(1)"); + connection.rollback(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName("INSERT jdbcunittest." + tableName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo( + maybeStable(DB_STATEMENT), + "INSERT INTO " + tableName + " VALUES(?)"), + equalTo(maybeStable(DB_OPERATION), "INSERT"), + equalTo(maybeStable(DB_SQL_TABLE), tableName)), + span -> + span.hasName("ROLLBACK") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(maybeStable(DB_OPERATION), "ROLLBACK"), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo( + DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url)))); + } + + static Stream transactionOperationsStream() throws SQLException { + return Stream.of( + Arguments.of("h2", new org.h2.Driver().connect(jdbcUrls.get("h2"), null), null, "h2:mem:"), + Arguments.of( + "derby", + new EmbeddedDriver().connect(jdbcUrls.get("derby"), null), + "APP", + "derby:memory:"), + Arguments.of( + "hsqldb", new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null), "SA", "hsqldb:mem:")); + } } diff --git a/instrumentation/jdbc/library/build.gradle.kts b/instrumentation/jdbc/library/build.gradle.kts index e61a0cbacbef..03e0f372c10d 100644 --- a/instrumentation/jdbc/library/build.gradle.kts +++ b/instrumentation/jdbc/library/build.gradle.kts @@ -59,3 +59,9 @@ tasks { dependsOn(testStableSemconv) } } + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.jdbc.experimental.transaction.enabled=true") + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java index d50039ca9bdd..73f0e2f71434 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java @@ -244,7 +244,10 @@ public Connection connect(String url, Properties info) throws SQLException { Instrumenter statementInstrumenter = JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + Instrumenter transactionInstrumenter = + JdbcInstrumenterFactory.createTransactionInstrumenter(openTelemetry); + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, transactionInstrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java index cc857b823563..dd86484ca689 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java @@ -26,16 +26,22 @@ public static JdbcTelemetryBuilder builder(OpenTelemetry openTelemetry) { private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; + private final Instrumenter transactionInstrumenter; JdbcTelemetry( Instrumenter dataSourceInstrumenter, - Instrumenter statementInstrumenter) { + Instrumenter statementInstrumenter, + Instrumenter transactionInstrumenter) { this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; + this.transactionInstrumenter = transactionInstrumenter; } public DataSource wrap(DataSource dataSource) { return new OpenTelemetryDataSource( - dataSource, this.dataSourceInstrumenter, this.statementInstrumenter); + dataSource, + this.dataSourceInstrumenter, + this.statementInstrumenter, + this.transactionInstrumenter); } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java index 825b29547334..bdb173928f32 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java @@ -16,6 +16,7 @@ public final class JdbcTelemetryBuilder { private boolean dataSourceInstrumenterEnabled = true; private boolean statementInstrumenterEnabled = true; private boolean statementSanitizationEnabled = true; + private boolean transactionInstrumenterEnabled = false; JdbcTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -42,12 +43,21 @@ public JdbcTelemetryBuilder setStatementSanitizationEnabled(boolean enabled) { return this; } + /** Configures whether spans are created for JDBC Transactions. Disabled by default. */ + @CanIgnoreReturnValue + public JdbcTelemetryBuilder setTransactionInstrumenterEnabled(boolean enabled) { + this.transactionInstrumenterEnabled = enabled; + return this; + } + /** Returns a new {@link JdbcTelemetry} with the settings of this {@link JdbcTelemetryBuilder}. */ public JdbcTelemetry build() { return new JdbcTelemetry( JdbcInstrumenterFactory.createDataSourceInstrumenter( openTelemetry, dataSourceInstrumenterEnabled), JdbcInstrumenterFactory.createStatementInstrumenter( - openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled)); + openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled), + JdbcInstrumenterFactory.createTransactionInstrumenter( + openTelemetry, transactionInstrumenterEnabled)); } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java index deb0425ff223..021897e02ae7 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java @@ -22,6 +22,7 @@ import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createTransactionInstrumenter; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcUtils.computeDbInfo; import io.opentelemetry.api.GlobalOpenTelemetry; @@ -47,6 +48,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable { private final DataSource delegate; private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; + private final Instrumenter transactionInstrumenter; private volatile DbInfo cachedDbInfo; /** @@ -71,6 +73,7 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) this.delegate = delegate; this.dataSourceInstrumenter = createDataSourceInstrumenter(openTelemetry, true); this.statementInstrumenter = createStatementInstrumenter(openTelemetry); + this.transactionInstrumenter = createTransactionInstrumenter(openTelemetry, false); } /** @@ -83,24 +86,28 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) OpenTelemetryDataSource( DataSource delegate, Instrumenter dataSourceInstrumenter, - Instrumenter statementInstrumenter) { + Instrumenter statementInstrumenter, + Instrumenter transactionInstrumenter) { this.delegate = delegate; this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; + this.transactionInstrumenter = transactionInstrumenter; } @Override public Connection getConnection() throws SQLException { Connection connection = wrapCall(delegate::getConnection); DbInfo dbInfo = getDbInfo(connection); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, transactionInstrumenter); } @Override public Connection getConnection(String username, String password) throws SQLException { Connection connection = wrapCall(() -> delegate.getConnection(username, password)); DbInfo dbInfo = getDbInfo(connection); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, transactionInstrumenter); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java index a5e8e0559178..cb16d91542e8 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java @@ -63,7 +63,26 @@ public static DbRequest create(DbInfo dbInfo, String queryText, Long batchSize) } public static DbRequest create(DbInfo dbInfo, Collection queryTexts, Long batchSize) { - return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize); + return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize, null); + } + + public static DbRequest create( + DbInfo dbInfo, Collection queryTexts, Long batchSize, String operation) { + return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize, operation); + } + + @Nullable + public static DbRequest createTransaction(Connection connection, String operation) { + Connection realConnection = JdbcUtils.unwrapConnection(connection); + if (realConnection == null) { + return null; + } + + return createTransaction(JdbcUtils.extractDbInfo(realConnection), operation); + } + + public static DbRequest createTransaction(DbInfo dbInfo, String operation) { + return create(dbInfo, Collections.emptyList(), null, operation); } public abstract DbInfo getDbInfo(); @@ -72,4 +91,8 @@ public static DbRequest create(DbInfo dbInfo, Collection queryTexts, Lon @Nullable public abstract Long getBatchSize(); + + // used for transaction instrumentation + @Nullable + public abstract String getOperation(); } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java index 74cd1c52d51a..f01d836ab72d 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java @@ -11,11 +11,7 @@ import java.util.Collection; import javax.annotation.Nullable; -/** - * This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. - */ -public final class JdbcAttributesGetter implements SqlClientAttributesGetter { +final class JdbcAttributesGetter implements SqlClientAttributesGetter { @Nullable @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java index 861e1dd11759..626e62452fc8 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java @@ -5,18 +5,21 @@ package io.opentelemetry.instrumentation.jdbc.internal; -import io.opentelemetry.api.GlobalOpenTelemetry; +import static java.util.Collections.emptyList; + import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics; import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; +import java.util.List; import javax.sql.DataSource; /** @@ -29,10 +32,6 @@ public final class JdbcInstrumenterFactory { private static final JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter(); - public static Instrumenter createStatementInstrumenter() { - return createStatementInstrumenter(GlobalOpenTelemetry.get()); - } - public static Instrumenter createStatementInstrumenter( OpenTelemetry openTelemetry) { return createStatementInstrumenter( @@ -44,6 +43,15 @@ public static Instrumenter createStatementInstrumenter( public static Instrumenter createStatementInstrumenter( OpenTelemetry openTelemetry, boolean enabled, boolean statementSanitizationEnabled) { + return createStatementInstrumenter( + openTelemetry, emptyList(), enabled, statementSanitizationEnabled); + } + + public static Instrumenter createStatementInstrumenter( + OpenTelemetry openTelemetry, + List> extractors, + boolean enabled, + boolean statementSanitizationEnabled) { return Instrumenter.builder( openTelemetry, INSTRUMENTATION_NAME, @@ -53,6 +61,7 @@ public static Instrumenter createStatementInstrumenter( .setStatementSanitizationEnabled(statementSanitizationEnabled) .build()) .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractors(extractors) .addOperationMetrics(DbClientMetrics.get()) .setEnabled(enabled) .buildInstrumenter(SpanKindExtractor.alwaysClient()); @@ -69,5 +78,32 @@ public static Instrumenter createDataSourceInstrumenter( .buildInstrumenter(); } + public static Instrumenter createTransactionInstrumenter( + OpenTelemetry openTelemetry) { + return createTransactionInstrumenter( + openTelemetry, + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.jdbc.experimental.transaction.enabled", false)); + } + + public static Instrumenter createTransactionInstrumenter( + OpenTelemetry openTelemetry, boolean enabled) { + return createTransactionInstrumenter(openTelemetry, emptyList(), enabled); + } + + public static Instrumenter createTransactionInstrumenter( + OpenTelemetry openTelemetry, + List> extractors, + boolean enabled) { + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, DbRequest::getOperation) + .addAttributesExtractor(SqlClientAttributesExtractor.builder(dbAttributesGetter).build()) + .addAttributesExtractor(TransactionAttributeExtractor.INSTANCE) + .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) + .addAttributesExtractors(extractors) + .setEnabled(enabled) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + private JdbcInstrumenterFactory() {} } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java index 8fe5ca26ab1d..4874ed4a6f5e 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java @@ -20,6 +20,8 @@ package io.opentelemetry.instrumentation.jdbc.internal; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.sql.Array; @@ -52,12 +54,17 @@ public class OpenTelemetryConnection implements Connection { protected final Connection delegate; private final DbInfo dbInfo; protected final Instrumenter statementInstrumenter; + protected final Instrumenter transactionInstrumenter; protected OpenTelemetryConnection( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + Instrumenter transactionInstrumenter) { this.delegate = delegate; this.dbInfo = dbInfo; this.statementInstrumenter = statementInstrumenter; + this.transactionInstrumenter = transactionInstrumenter; } // visible for testing @@ -71,11 +78,16 @@ static boolean hasJdbc43() { } public static Connection create( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + Instrumenter transactionInstrumenter) { if (hasJdbc43) { - return new OpenTelemetryConnectionJdbc43(delegate, dbInfo, statementInstrumenter); + return new OpenTelemetryConnectionJdbc43( + delegate, dbInfo, statementInstrumenter, transactionInstrumenter); } - return new OpenTelemetryConnection(delegate, dbInfo, statementInstrumenter); + return new OpenTelemetryConnection( + delegate, dbInfo, statementInstrumenter, transactionInstrumenter); } @Override @@ -173,7 +185,7 @@ public CallableStatement prepareCall( @Override public void commit() throws SQLException { - delegate.commit(); + wrapCall(delegate::commit, "COMMIT"); } @Override @@ -279,13 +291,13 @@ public Savepoint setSavepoint(String name) throws SQLException { @SuppressWarnings("UngroupedOverloads") @Override public void rollback() throws SQLException { - delegate.rollback(); + wrapCall(delegate::rollback, "ROLLBACK"); } @SuppressWarnings("UngroupedOverloads") @Override public void rollback(Savepoint savepoint) throws SQLException { - delegate.rollback(savepoint); + wrapCall(() -> delegate.rollback(savepoint), "ROLLBACK"); } @Override @@ -393,8 +405,11 @@ public DbInfo getDbInfo() { // JDBC 4.3 static class OpenTelemetryConnectionJdbc43 extends OpenTelemetryConnection { OpenTelemetryConnectionJdbc43( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { - super(delegate, dbInfo, statementInstrumenter); + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + Instrumenter transactionInstrumenter) { + super(delegate, dbInfo, statementInstrumenter, transactionInstrumenter); } @SuppressWarnings("Since15") @@ -435,4 +450,27 @@ public void setShardingKey(ShardingKey shardingKey) throws SQLException { delegate.setShardingKey(shardingKey); } } + + protected void wrapCall(ThrowingSupplier callable, String operation) + throws E { + Context parentContext = Context.current(); + DbRequest request = DbRequest.createTransaction(dbInfo, operation); + if (!this.transactionInstrumenter.shouldStart(parentContext, request)) { + callable.call(); + return; + } + + Context context = this.transactionInstrumenter.start(parentContext, request); + try (Scope ignored = context.makeCurrent()) { + callable.call(); + } catch (Throwable t) { + this.transactionInstrumenter.end(context, request, null, t); + throw t; + } + this.transactionInstrumenter.end(context, request, null, null); + } + + protected interface ThrowingSupplier { + void call() throws E; + } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/TransactionAttributeExtractor.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/TransactionAttributeExtractor.java new file mode 100644 index 000000000000..cb3553b1e7cc --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/TransactionAttributeExtractor.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal; + +import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import javax.annotation.Nullable; + +enum TransactionAttributeExtractor implements AttributesExtractor { + INSTANCE; + + // copied from DbIncubatingAttributes + private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); + private static final AttributeKey DB_OPERATION_NAME = + AttributeKey.stringKey("db.operation.name"); + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, DbRequest request) { + if (SemconvStability.emitOldDatabaseSemconv()) { + internalSet(attributes, DB_OPERATION, request.getOperation()); + } + if (SemconvStability.emitStableDatabaseSemconv()) { + internalSet(attributes, DB_OPERATION_NAME, request.getOperation()); + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + DbRequest request, + @Nullable Void unused, + @Nullable Throwable error) {} +} diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java index f3ec381ade4a..145f8e07df7d 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryTest.java @@ -130,6 +130,7 @@ void buildWithAllInstrumentersDisabled() throws SQLException { JdbcTelemetry.builder(testing.getOpenTelemetry()) .setDataSourceInstrumenterEnabled(false) .setStatementInstrumenterEnabled(false) + .setTransactionInstrumenterEnabled(false) .build(); DataSource dataSource = telemetry.wrap(new TestDataSource()); @@ -178,6 +179,30 @@ void buildWithStatementInstrumenterDisabled() throws SQLException { span -> span.hasName("TestDataSource.getConnection"))); } + @Test + void buildWithTransactionInstrumenterDisabled() throws SQLException { + JdbcTelemetry telemetry = + JdbcTelemetry.builder(testing.getOpenTelemetry()) + .setTransactionInstrumenterEnabled(false) + .build(); + + DataSource dataSource = telemetry.wrap(new TestDataSource()); + + testing.runWithSpan( + "parent", + () -> { + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent"), + span -> span.hasName("TestDataSource.getConnection"))); + } + @Test void buildWithSanitizationDisabled() throws SQLException { JdbcTelemetry telemetry = diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java index adbadfbd05d7..836525ff2ec3 100644 --- a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java +++ b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java @@ -7,6 +7,7 @@ import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createTransactionInstrumenter; 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; @@ -43,11 +44,7 @@ class OpenTelemetryConnectionTest { @Test void testVerifyCreateStatement() throws SQLException { - Instrumenter instrumenter = - createStatementInstrumenter(testing.getOpenTelemetry()); - DbInfo dbInfo = getDbInfo(); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + OpenTelemetryConnection connection = getConnection(); String query = "SELECT * FROM users"; Statement statement = connection.createStatement(); @@ -57,7 +54,7 @@ void testVerifyCreateStatement() throws SQLException { assertThat(statement.execute(query)).isTrue(); }); - jdbcTraceAssertion(dbInfo, query); + jdbcTraceAssertion(connection.getDbInfo(), query); statement.close(); connection.close(); @@ -66,27 +63,21 @@ void testVerifyCreateStatement() throws SQLException { @SuppressWarnings("unchecked") @Test void testVerifyCreateStatementReturnsOtelWrapper() throws Exception { - OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); - Instrumenter instrumenter = createStatementInstrumenter(ot); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + OpenTelemetry openTelemetry = OpenTelemetry.propagating(ContextPropagators.noop()); + OpenTelemetryConnection connection = getConnection(openTelemetry); assertThat(connection.createStatement()).isInstanceOf(OpenTelemetryStatement.class); assertThat(connection.createStatement(0, 0)).isInstanceOf(OpenTelemetryStatement.class); assertThat(connection.createStatement(0, 0, 0)).isInstanceOf(OpenTelemetryStatement.class); assertThat(((OpenTelemetryStatement) connection.createStatement()).instrumenter) - .isEqualTo(instrumenter); + .isEqualTo(connection.statementInstrumenter); connection.close(); } @Test void testVerifyPrepareStatement() throws SQLException { - Instrumenter instrumenter = - createStatementInstrumenter(testing.getOpenTelemetry()); - DbInfo dbInfo = getDbInfo(); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + OpenTelemetryConnection connection = getConnection(); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareStatement(query); @@ -99,7 +90,7 @@ void testVerifyPrepareStatement() throws SQLException { assertThat(resultSet.getStatement()).isEqualTo(statement); }); - jdbcTraceAssertion(dbInfo, query); + jdbcTraceAssertion(connection.getDbInfo(), query); statement.close(); connection.close(); @@ -107,11 +98,7 @@ void testVerifyPrepareStatement() throws SQLException { @Test void testVerifyPrepareStatementUpdate() throws SQLException { - Instrumenter instrumenter = - createStatementInstrumenter(testing.getOpenTelemetry()); - DbInfo dbInfo = getDbInfo(); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + OpenTelemetryConnection connection = getConnection(); String query = "UPDATE users SET name = name"; PreparedStatement statement = connection.prepareStatement(query); @@ -122,7 +109,7 @@ void testVerifyPrepareStatementUpdate() throws SQLException { assertThat(statement.getResultSet()).isNull(); }); - jdbcTraceAssertion(dbInfo, query, "UPDATE"); + jdbcTraceAssertion(connection.getDbInfo(), query, "UPDATE"); statement.close(); connection.close(); @@ -130,11 +117,7 @@ void testVerifyPrepareStatementUpdate() throws SQLException { @Test void testVerifyPrepareStatementQuery() throws SQLException { - Instrumenter instrumenter = - createStatementInstrumenter(testing.getOpenTelemetry()); - DbInfo dbInfo = getDbInfo(); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + OpenTelemetryConnection connection = getConnection(); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareStatement(query); @@ -146,7 +129,7 @@ void testVerifyPrepareStatementQuery() throws SQLException { assertThat(resultSet.getStatement()).isEqualTo(statement); }); - jdbcTraceAssertion(dbInfo, query); + jdbcTraceAssertion(connection.getDbInfo(), query); statement.close(); connection.close(); @@ -155,10 +138,9 @@ void testVerifyPrepareStatementQuery() throws SQLException { @SuppressWarnings("unchecked") @Test void testVerifyPrepareStatementReturnsOtelWrapper() throws Exception { - OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); - Instrumenter instrumenter = createStatementInstrumenter(ot); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + OpenTelemetry openTelemetry = OpenTelemetry.propagating(ContextPropagators.noop()); + OpenTelemetryConnection connection = getConnection(openTelemetry); + String query = "SELECT * FROM users"; assertThat(connection.prepareStatement(query)) @@ -175,18 +157,14 @@ void testVerifyPrepareStatementReturnsOtelWrapper() throws Exception { .isInstanceOf(OpenTelemetryPreparedStatement.class); assertThat( ((OpenTelemetryStatement) connection.prepareStatement(query)).instrumenter) - .isEqualTo(instrumenter); + .isEqualTo(connection.statementInstrumenter); connection.close(); } @Test void testVerifyPrepareCall() throws SQLException { - Instrumenter instrumenter = - createStatementInstrumenter(testing.getOpenTelemetry()); - DbInfo dbInfo = getDbInfo(); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + OpenTelemetryConnection connection = getConnection(); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareCall(query); @@ -196,7 +174,7 @@ void testVerifyPrepareCall() throws SQLException { assertThat(statement.execute()).isTrue(); }); - jdbcTraceAssertion(dbInfo, query); + jdbcTraceAssertion(connection.getDbInfo(), query); statement.close(); connection.close(); @@ -205,10 +183,9 @@ void testVerifyPrepareCall() throws SQLException { @SuppressWarnings("unchecked") @Test void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { - OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); - Instrumenter instrumenter = createStatementInstrumenter(ot); - OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + OpenTelemetry openTelemetry = OpenTelemetry.propagating(ContextPropagators.noop()); + OpenTelemetryConnection connection = getConnection(openTelemetry); + String query = "SELECT * FROM users"; assertThat(connection.prepareCall(query)).isInstanceOf(OpenTelemetryCallableStatement.class); @@ -219,7 +196,27 @@ void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { assertThat(connection.prepareCall(query, 0, 0, 0)) .isInstanceOf(OpenTelemetryCallableStatement.class); assertThat(((OpenTelemetryStatement) connection.prepareCall(query)).instrumenter) - .isEqualTo(instrumenter); + .isEqualTo(connection.statementInstrumenter); + + connection.close(); + } + + @Test + void testVerifyCommit() throws Exception { + OpenTelemetryConnection connection = getConnection(); + + testing.runWithSpan("parent", connection::commit); + transactionTraceAssertion(connection.getDbInfo(), "COMMIT"); + + connection.close(); + } + + @Test + void testVerifyRollback() throws Exception { + OpenTelemetryConnection connection = getConnection(); + + testing.runWithSpan("parent", () -> connection.rollback()); + transactionTraceAssertion(connection.getDbInfo(), "ROLLBACK"); connection.close(); } @@ -266,4 +263,42 @@ private static void jdbcTraceAssertion(DbInfo dbInfo, String query, String opera equalTo(SERVER_ADDRESS, dbInfo.getHost()), equalTo(SERVER_PORT, dbInfo.getPort())))); } + + @SuppressWarnings("deprecation") // old semconv + private static void transactionTraceAssertion(DbInfo dbInfo, String operation) { + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(operation) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + maybeStableDbSystemName(dbInfo.getSystem())), + equalTo(maybeStable(DB_NAME), dbInfo.getName()), + equalTo(maybeStable(DB_OPERATION), operation), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : dbInfo.getUser()), + equalTo( + DB_CONNECTION_STRING, + emitStableDatabaseSemconv() ? null : dbInfo.getShortUrl()), + equalTo(SERVER_ADDRESS, dbInfo.getHost()), + equalTo(SERVER_PORT, dbInfo.getPort())))); + } + + private static OpenTelemetryConnection getConnection() { + return getConnection(testing.getOpenTelemetry()); + } + + private static OpenTelemetryConnection getConnection(OpenTelemetry openTelemetry) { + Instrumenter statementInstrumenter = + createStatementInstrumenter(openTelemetry); + Instrumenter transactionInstrumenter = + createTransactionInstrumenter(openTelemetry, true); + DbInfo dbInfo = getDbInfo(); + return new OpenTelemetryConnection( + new TestConnection(), dbInfo, statementInstrumenter, transactionInstrumenter); + } } diff --git a/instrumentation/jdbc/metadata.yaml b/instrumentation/jdbc/metadata.yaml index e52ce68e1aef..23f5d943a8d2 100644 --- a/instrumentation/jdbc/metadata.yaml +++ b/instrumentation/jdbc/metadata.yaml @@ -11,6 +11,9 @@ configurations: - name: otel.instrumentation.common.db-statement-sanitizer.enabled description: Enables statement sanitization for database queries. default: true + - name: otel.instrumentation.jdbc.experimental.transaction.enabled + description: Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations. + default: false - name: otel.instrumentation.common.peer-service-mapping description: Used to specify a mapping from host names or IP addresses to peer services. default: "" diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java index f29513d01bc5..22c437605de6 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java @@ -55,6 +55,10 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { InstrumentationConfigUtil.isStatementSanitizationEnabled( configPropertiesProvider.getObject(), "otel.instrumentation.jdbc.statement-sanitizer.enabled")) + .setTransactionInstrumenterEnabled( + configPropertiesProvider + .getObject() + .getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false)) .build() .wrap(dataSource); } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index db72deca72bd..b4f7b9d91875 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -351,6 +351,12 @@ "description": "Enables the DB statement sanitization.", "defaultValue": true }, + { + "name": "otel.instrumentation.jdbc.experimental.transaction.enabled", + "type": "java.lang.Boolean", + "description": "Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations.", + "defaultValue": false + }, { "name": "otel.instrumentation.kafka.enabled", "type": "java.lang.Boolean",