diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java index 2bcde2706295..abf1485fd3c4 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/internal/CommonConfig.java @@ -34,6 +34,7 @@ public final class CommonConfig { private final Set knownHttpRequestMethods; private final EnduserConfig enduserConfig; private final boolean statementSanitizationEnabled; + private final boolean sqlCommenterEnabled; private final boolean emitExperimentalHttpClientTelemetry; private final boolean emitExperimentalHttpServerTelemetry; private final boolean redactQueryParameters; @@ -87,6 +88,9 @@ public CommonConfig(InstrumentationConfig config) { new ArrayList<>(HttpConstants.KNOWN_METHODS))); statementSanitizationEnabled = config.getBoolean("otel.instrumentation.common.db-statement-sanitizer.enabled", true); + sqlCommenterEnabled = + config.getBoolean( + "otel.instrumentation.common.experimental.db-sqlcommenter.enabled", false); emitExperimentalHttpClientTelemetry = config.getBoolean("otel.instrumentation.http.client.emit-experimental-telemetry", false); redactQueryParameters = @@ -138,6 +142,10 @@ public boolean isStatementSanitizationEnabled() { return statementSanitizationEnabled; } + public boolean isSqlCommenterEnabled() { + return sqlCommenterEnabled; + } + public boolean shouldEmitExperimentalHttpClientTelemetry() { return emitExperimentalHttpClientTelemetry; } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtil.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtil.java new file mode 100644 index 000000000000..a1f510d544bb --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtil.java @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.db.internal; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import javax.annotation.Nullable; + +/** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class SqlCommenterUtil { + + /** + * Append comment containing tracing information at the end of the query. See sqlcommenter for the description of the + * algorithm. + */ + public static String processQuery(String query) { + if (!Span.current().getSpanContext().isValid()) { + return query; + } + // skip queries that contain comments + if (containsSqlComment(query)) { + return query; + } + + class State { + @Nullable String traceparent; + @Nullable String tracestate; + } + + State state = new State(); + + W3CTraceContextPropagator.getInstance() + .inject( + Context.current(), + state, + (carrier, key, value) -> { + if (carrier == null) { + return; + } + if ("traceparent".equals(key)) { + carrier.traceparent = value; + } else if ("tracestate".equals(key)) { + carrier.tracestate = value; + } + }); + try { + // we know that the traceparent doesn't contain anything that needs to be encoded + query += " /*traceparent='" + state.traceparent + "'"; + if (state.tracestate != null) { + query += ", tracestate=" + serialize(state.tracestate); + } + query += "*/"; + } catch (UnsupportedEncodingException exception) { + // this exception should never happen as UTF-8 encoding is always available + } + return query; + } + + private static boolean containsSqlComment(String query) { + return query.contains("--") || query.contains("/*"); + } + + private static String serialize(String value) throws UnsupportedEncodingException { + // specification requires percent encoding, here we use the java build in url encoder that + // encodes space as '+' instead of '%20' as required + String result = URLEncoder.encode(value, "UTF-8").replace("+", "%20"); + // specification requires escaping ' with a backslash, we skip this because URLEncoder already + // encodes the ' + return "'" + result + "'"; + } + + private SqlCommenterUtil() {} +} diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtilTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtilTest.java new file mode 100644 index 000000000000..45aecad5cd2a --- /dev/null +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/internal/SqlCommenterUtilTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.semconv.db.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlCommenterUtilTest { + + @ParameterizedTest + @ValueSource(strings = {"SELECT /**/ 1", "SELECT 1 --", "SELECT '/*'"}) + void skipQueriesWithComments(String query) { + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + TraceState.getDefault()))); + + try (Scope ignore = parent.makeCurrent()) { + assertThat(SqlCommenterUtil.processQuery(query)).isEqualTo(query); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sqlCommenter(boolean hasTraceState) { + TraceState state = + hasTraceState ? TraceState.builder().put("test", "test'").build() : TraceState.getDefault(); + Context parent = + Context.root() + .with( + Span.wrap( + SpanContext.create( + "ff01020304050600ff0a0b0c0d0e0f00", + "090a0b0c0d0e0f00", + TraceFlags.getSampled(), + state))); + + try (Scope ignore = parent.makeCurrent()) { + assertThat(SqlCommenterUtil.processQuery("SELECT 1")) + .isEqualTo( + hasTraceState + ? "SELECT 1 /*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01', tracestate='test%3Dtest%27'*/" + : "SELECT 1 /*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01'*/"); + } + } +} diff --git a/instrumentation/jdbc/README.md b/instrumentation/jdbc/README.md index 428c78075f11..b3ee2762e1bf 100644 --- a/instrumentation/jdbc/README.md +++ b/instrumentation/jdbc/README.md @@ -1,7 +1,8 @@ # Settings for the JDBC instrumentation -| System property | Type | Default | Description | -|-------------------------------------------------------------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | -| `otel.instrumentation.jdbc.experimental.capture-query-parameters` | Boolean | `false` | Enable the capture of query parameters as span attributes. Enabling this option disables the statement sanitization.

WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info. | -| `otel.instrumentation.jdbc.experimental.transaction.enabled` | Boolean | `false` | Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations. | +| System property | Type | Default | Description | +|-------------------------------------------------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| `otel.instrumentation.jdbc.experimental.capture-query-parameters` | Boolean | `false` | Enable the capture of query parameters as span attributes. Enabling this option disables the statement sanitization.

WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info. | +| `otel.instrumentation.jdbc.experimental.transaction.enabled` | Boolean | `false` | Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations. | +| `otel.instrumentation.jdbc.experimental.sqlcommenter.enabled` | Boolean | `false` | Enables augmenting queries with a comment containing the tracing information. See [sqlcommenter](https://google.github.io/sqlcommenter/) for more info. WARNING: augmenting queries with tracing context will make query texts unique, which may have adverse impact on database performance. Consult with database experts before enabling. | diff --git a/instrumentation/jdbc/javaagent/build.gradle.kts b/instrumentation/jdbc/javaagent/build.gradle.kts index a03356066f59..81b457ad2271 100644 --- a/instrumentation/jdbc/javaagent/build.gradle.kts +++ b/instrumentation/jdbc/javaagent/build.gradle.kts @@ -66,12 +66,21 @@ tasks { include("**/SlickTest.*") } + val testSqlCommenter by registering(Test::class) { + filter { + includeTestsMatching("SqlCommenterTest") + } + include("**/SqlCommenterTest.*") + jvmArgs("-Dotel.instrumentation.jdbc.experimental.sqlcommenter.enabled=true") + } + val testStableSemconv by registering(Test::class) { testClassesDirs = sourceSets.test.get().output.classesDirs classpath = sourceSets.test.get().runtimeClasspath filter { excludeTestsMatching("SlickTest") + excludeTestsMatching("SqlCommenterTest") excludeTestsMatching("PreparedStatementParametersTest") } jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true") @@ -102,13 +111,18 @@ tasks { test { filter { excludeTestsMatching("SlickTest") + excludeTestsMatching("SqlCommenterTest") excludeTestsMatching("PreparedStatementParametersTest") } jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true") } check { - dependsOn(testSlick, testStableSemconv, testSlickStableSemconv, testCaptureParameters) + dependsOn(testSlick) + dependsOn(testSqlCommenter) + dependsOn(testStableSemconv) + dependsOn(testSlickStableSemconv) + dependsOn(testCaptureParameters) } } 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 76474f2930f6..ca3e19f5e1d3 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 @@ -18,9 +18,12 @@ import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcData; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.sql.Connection; @@ -28,6 +31,8 @@ import java.util.Locale; import javax.annotation.Nullable; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -48,7 +53,7 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( nameStartsWith("prepare") .and(takesArgument(0, String.class)) - // Also include CallableStatement, which is a sub type of PreparedStatement + // Also include CallableStatement, which is a subtype of PreparedStatement .and(returns(implementsInterface(named("java.sql.PreparedStatement")))), ConnectionInstrumentation.class.getName() + "$PrepareAdvice"); transformer.applyAdviceToMethod( @@ -59,14 +64,72 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class PrepareAdvice { - @Advice.OnMethodExit(suppress = Throwable.class) + public static final class PrepareContext implements ImplicitContextKeyed { + + private static final ContextKey KEY = + ContextKey.named("jdbc-prepare-context"); + + private final String originalSql; + + private PrepareContext(String originalSql) { + this.originalSql = originalSql; + } + + public String get() { + return originalSql; + } + + @Nullable + public static PrepareContext get(Context context) { + return context.get(KEY); + } + + public static Context init(Context context, String originalSql) { + if (context.get(KEY) != null) { + return context; + } + return context.with(new PrepareContext(originalSql)); + } + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } + } + + @AssignReturned.ToArguments(@ToArgument(value = 0, index = 0)) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Object[] processSql(@Advice.Argument(0) String sql) { + Context context = Java8BytecodeBridge.currentContext(); + if (PrepareContext.get(context) == null) { + // process sql only in the outermost prepare call and save the original sql in context + String processSql = JdbcSingletons.processSql(sql); + Scope scope = PrepareContext.init(context, sql).makeCurrent(); + return new Object[] {processSql, scope}; + } else { + return new Object[] {sql, null}; + } + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void addDbInfo( - @Advice.Argument(0) String sql, @Advice.Return PreparedStatement statement) { - if (JdbcSingletons.isWrapper(statement, PreparedStatement.class)) { + @Advice.Return PreparedStatement statement, + @Advice.Enter Object[] enterResult, + @Advice.Thrown Throwable error) { + Context context = Java8BytecodeBridge.currentContext(); + PrepareContext prepareContext = PrepareContext.get(context); + Scope scope = (Scope) enterResult[1]; + if (scope != null) { + scope.close(); + } + if (error != null + || prepareContext == null + || JdbcSingletons.isWrapper(statement, PreparedStatement.class)) { return; } - JdbcData.preparedStatement.set(statement, sql); + String originalSql = prepareContext.get(); + JdbcData.preparedStatement.set(statement, originalSql); } } 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 4476a9e2b104..4ecd74a7738d 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,6 +8,7 @@ import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenterUtil; import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -28,6 +29,11 @@ public final class JdbcSingletons { private static final Instrumenter TRANSACTION_INSTRUMENTER; public static final Instrumenter DATASOURCE_INSTRUMENTER = createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true); + private static final boolean SQLCOMMENTER_ENABLED = + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.jdbc.experimental.sqlcommenter.enabled", + AgentCommonConfig.get().isSqlCommenterEnabled()); public static final boolean CAPTURE_QUERY_PARAMETERS; static { @@ -97,5 +103,9 @@ private static boolean isWrapperInternal(T object, Class return false; } + public static String processSql(String sql) { + return SQLCOMMENTER_ENABLED ? SqlCommenterUtil.processQuery(sql) : sql; + } + private JdbcSingletons() {} } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java index 9a192535ca87..d82a1b3abaee 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java @@ -22,6 +22,8 @@ import java.sql.Statement; import javax.annotation.Nullable; import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.AssignReturned; +import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -59,21 +61,25 @@ public void transform(TypeTransformer transformer) { @SuppressWarnings("unused") public static class StatementAdvice { - @Nullable + @AssignReturned.ToArguments(@ToArgument(value = 0, index = 1)) @Advice.OnMethodEnter(suppress = Throwable.class) - public static JdbcAdviceScope onEnter( + public static Object[] onEnter( @Advice.Argument(0) String sql, @Advice.This Statement statement) { if (JdbcSingletons.isWrapper(statement, Statement.class)) { - return null; + return new Object[] {null, sql}; } - return JdbcAdviceScope.startStatement(CallDepth.forClass(Statement.class), sql, statement); + String processedSql = JdbcSingletons.processSql(sql); + return new Object[] { + JdbcAdviceScope.startStatement(CallDepth.forClass(Statement.class), sql, statement), + processedSql + }; } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) public static void stopSpan( - @Advice.Thrown @Nullable Throwable throwable, - @Advice.Enter @Nullable JdbcAdviceScope adviceScope) { + @Advice.Thrown @Nullable Throwable throwable, @Advice.Enter Object[] enterResult) { + JdbcAdviceScope adviceScope = (JdbcAdviceScope) enterResult[0]; if (adviceScope != null) { adviceScope.end(throwable); } @@ -83,16 +89,19 @@ public static void stopSpan( @SuppressWarnings("unused") public static class AddBatchAdvice { - @Advice.OnMethodExit(suppress = Throwable.class) - public static void addBatch(@Advice.This Statement statement, @Advice.Argument(0) String sql) { + @AssignReturned.ToArguments(@ToArgument(0)) + @Advice.OnMethodEnter(suppress = Throwable.class) + public static String addBatch( + @Advice.This Statement statement, @Advice.Argument(0) String sql) { if (statement instanceof PreparedStatement) { - return; + return sql; } if (JdbcSingletons.isWrapper(statement, Statement.class)) { - return; + return sql; } JdbcData.addStatementBatch(statement, sql); + return JdbcSingletons.processSql(sql); } } 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 b57f1a96a447..e369d0854bca 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 @@ -1614,6 +1614,23 @@ void testPreparedBatch(String system, Connection connection, String username, St emitStableDatabaseSemconv() ? 2L : null)))); } + // test that sqlcommenter is not enabled by default + @Test + void testSqlCommenterNotEnabled() throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + Statement statement = connection.createStatement(); + + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + String query = "SELECT 1"; + testing.runWithSpan("parent", () -> statement.execute(query)); + + assertThat(executedSql).hasSize(1); + assertThat(executedSql.get(0)).isEqualTo(query); + } + @ParameterizedTest @MethodSource("transactionOperationsStream") void testCommitTransaction(String system, Connection connection, String username, String url) diff --git a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/SqlCommenterTest.java b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/SqlCommenterTest.java new file mode 100644 index 000000000000..71a6843c0b23 --- /dev/null +++ b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/SqlCommenterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import io.opentelemetry.instrumentation.jdbc.TestConnection; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class SqlCommenterTest { + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void testSqlCommenterStatement() throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + Statement statement = connection.createStatement(); + + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + String query = "SELECT 1"; + testing.runWithSpan("parent", () -> statement.execute(query)); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("SELECT dbname").hasParent(trace.getSpan(0)))); + + assertThat(executedSql).hasSize(1); + assertThat(executedSql.get(0)).contains(query).contains("traceparent"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSqlCommenterStatementUpdate(boolean largeUpdate) throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + Statement statement = connection.createStatement(); + + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + String query = "INSERT INTO test VALUES(1)"; + testing.runWithSpan( + "parent", + () -> { + if (largeUpdate) { + statement.executeLargeUpdate(query); + } else { + statement.execute(query); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("INSERT dbname.test").hasParent(trace.getSpan(0)))); + + assertThat(executedSql).hasSize(1); + assertThat(executedSql.get(0)).contains(query).contains("traceparent"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSqlCommenterStatementBatch(boolean largeUpdate) throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + Statement statement = connection.createStatement(); + + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + testing.runWithSpan( + "parent", + () -> { + statement.addBatch("INSERT INTO test VALUES(1)"); + statement.addBatch("INSERT INTO test VALUES(2)"); + if (largeUpdate) { + statement.executeLargeBatch(); + } else { + statement.executeBatch(); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> + span.hasName( + SemconvStability.emitStableDatabaseSemconv() + ? "BATCH INSERT dbname.test" + : "dbname") + .hasParent(trace.getSpan(0)))); + + assertThat(executedSql).hasSize(2); + assertThat(executedSql.get(0)).contains("INSERT INTO test VALUES(1)").contains("traceparent"); + assertThat(executedSql.get(1)).contains("INSERT INTO test VALUES(2)").contains("traceparent"); + } + + @Test + void testSqlCommenterPreparedStatement() throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + + String query = "SELECT 1"; + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + statement.execute(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("SELECT dbname").hasParent(trace.getSpan(0)))); + + assertThat(executedSql).hasSize(1); + assertThat(executedSql.get(0)).contains(query).contains("traceparent"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSqlCommenterPreparedStatementUpdate(boolean largeUpdate) throws SQLException { + List executedSql = new ArrayList<>(); + Connection connection = new TestConnection(executedSql::add); + + String query = "INSERT INTO test VALUES(1)"; + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + cleanup.deferCleanup(connection); + + if (largeUpdate) { + statement.executeLargeUpdate(); + } else { + statement.executeUpdate(); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> span.hasName("INSERT dbname.test").hasParent(trace.getSpan(0)))); + + assertThat(executedSql).hasSize(1); + assertThat(executedSql.get(0)).contains(query).contains("traceparent"); + } +} 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 ae3b9e08601c..34a96c2caa0f 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 @@ -24,6 +24,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcConnectionUrlParser; @@ -61,6 +62,13 @@ public final class OpenTelemetryDriver implements Driver { private static final AtomicBoolean REGISTERED = new AtomicBoolean(); private static final List DRIVER_CANDIDATES = new CopyOnWriteArrayList<>(); + // XXX value proeprty? + private static final boolean sqlCommenterEnabled = + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.jdbc.experimental.sqlcommenter.enabled", + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.common.experimental.db-sqlcommenter.enabled", false)); + static { try { int[] version = parseInstrumentationVersion(); @@ -244,12 +252,18 @@ public Connection connect(String url, Properties info) throws SQLException { Instrumenter statementInstrumenter = JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry); + boolean captureQueryParameters = JdbcInstrumenterFactory.captureQueryParameters(); Instrumenter transactionInstrumenter = JdbcInstrumenterFactory.createTransactionInstrumenter(openTelemetry); return OpenTelemetryConnection.create( - connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + connection, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @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 a37aa0b2e665..2e60d2521236 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 @@ -28,16 +28,19 @@ public static JdbcTelemetryBuilder builder(OpenTelemetry openTelemetry) { private final Instrumenter statementInstrumenter; private final Instrumenter transactionInstrumenter; private final boolean captureQueryParameters; + private final boolean sqlCommenterEnabled; JdbcTelemetry( Instrumenter dataSourceInstrumenter, Instrumenter statementInstrumenter, Instrumenter transactionInstrumenter, - boolean captureQueryParameters) { + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; this.transactionInstrumenter = transactionInstrumenter; this.captureQueryParameters = captureQueryParameters; + this.sqlCommenterEnabled = sqlCommenterEnabled; } public DataSource wrap(DataSource dataSource) { @@ -46,6 +49,7 @@ public DataSource wrap(DataSource dataSource) { this.dataSourceInstrumenter, this.statementInstrumenter, this.transactionInstrumenter, - this.captureQueryParameters); + this.captureQueryParameters, + this.sqlCommenterEnabled); } } 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 564773bfd7f9..f1b7006d4b1f 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 @@ -8,6 +8,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.jdbc.datasource.internal.Experimental; import io.opentelemetry.instrumentation.jdbc.internal.DbRequest; import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; @@ -22,6 +23,12 @@ public final class JdbcTelemetryBuilder { private boolean statementSanitizationEnabled = true; private boolean transactionInstrumenterEnabled = false; private boolean captureQueryParameters = false; + private boolean sqlCommenterEnabled = false; + + static { + Experimental.internalSetEnableSqlCommenter( + (builder, sqlCommenterEnabled) -> builder.sqlCommenterEnabled = sqlCommenterEnabled); + } JdbcTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -87,6 +94,7 @@ public JdbcTelemetry build() { dataSourceInstrumenter, statementInstrumenter, transactionInstrumenter, - captureQueryParameters); + captureQueryParameters, + sqlCommenterEnabled); } } 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 08e1708040db..5efb2e0bd67c 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 @@ -49,8 +49,9 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable { private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; private final Instrumenter transactionInstrumenter; - private volatile DbInfo cachedDbInfo; private final boolean captureQueryParameters; + private final boolean sqlCommenterEnabled; + private volatile DbInfo cachedDbInfo; /** * Create a OpenTelemetry DataSource wrapping another DataSource. @@ -76,6 +77,7 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) this.statementInstrumenter = createStatementInstrumenter(openTelemetry); this.transactionInstrumenter = createTransactionInstrumenter(openTelemetry, false); this.captureQueryParameters = false; + this.sqlCommenterEnabled = false; } /** @@ -84,18 +86,22 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) * @param delegate the DataSource to wrap * @param dataSourceInstrumenter the DataSource Instrumenter to use * @param statementInstrumenter the Statement Instrumenter to use + * @param sqlCommenterEnabled whether to augment sql query with comment containing the tracing + * information */ OpenTelemetryDataSource( DataSource delegate, Instrumenter dataSourceInstrumenter, Instrumenter statementInstrumenter, Instrumenter transactionInstrumenter, - boolean captureQueryParameters) { + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { this.delegate = delegate; this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; this.transactionInstrumenter = transactionInstrumenter; this.captureQueryParameters = captureQueryParameters; + this.sqlCommenterEnabled = sqlCommenterEnabled; } @Override @@ -103,7 +109,12 @@ public Connection getConnection() throws SQLException { Connection connection = wrapCall(delegate::getConnection); DbInfo dbInfo = getDbInfo(connection); return OpenTelemetryConnection.create( - connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + connection, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override @@ -111,7 +122,12 @@ public Connection getConnection(String username, String password) throws SQLExce Connection connection = wrapCall(() -> delegate.getConnection(username, password)); DbInfo dbInfo = getDbInfo(connection); return OpenTelemetryConnection.create( - connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + connection, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/internal/Experimental.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/internal/Experimental.java new file mode 100644 index 000000000000..b3fc57f9c9f6 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/internal/Experimental.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.datasource.internal; + +import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetryBuilder; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class Experimental { + + @Nullable private static volatile BiConsumer setEnableSqlCommenter; + + /** + * Sets whether to augment sql query with comment containing the tracing information. See sqlcommenter for more info. + * + *

WARNING: augmenting queries with tracing context will make query texts unique, which may + * have adverse impact on database performance. Consult with database experts before enabling. + */ + public static void setEnableSqlCommenter( + JdbcTelemetryBuilder builder, boolean sqlCommenterEnabled) { + if (setEnableSqlCommenter != null) { + setEnableSqlCommenter.accept(builder, sqlCommenterEnabled); + } + } + + public static void internalSetEnableSqlCommenter( + BiConsumer setEnableSqlCommenter) { + Experimental.setEnableSqlCommenter = setEnableSqlCommenter; + } + + private Experimental() {} +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java index 7d6b1eab0920..17761f62504a 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryCallableStatement.java @@ -52,8 +52,16 @@ public OpenTelemetryCallableStatement( DbInfo dbInfo, String query, Instrumenter instrumenter, - boolean captureQueryParameters) { - super(delegate, connection, dbInfo, query, instrumenter, captureQueryParameters); + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { + super( + delegate, + connection, + dbInfo, + query, + instrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override 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 66964f866d37..226bcf0bcda2 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 @@ -22,6 +22,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenterUtil; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.sql.Array; @@ -56,18 +57,21 @@ public class OpenTelemetryConnection implements Connection { protected final Instrumenter statementInstrumenter; protected final Instrumenter transactionInstrumenter; private final boolean captureQueryParameters; + protected final boolean sqlCommenterEnabled; protected OpenTelemetryConnection( Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter, Instrumenter transactionInstrumenter, - boolean captureQueryParameters) { + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { this.delegate = delegate; this.dbInfo = dbInfo; this.statementInstrumenter = statementInstrumenter; this.transactionInstrumenter = transactionInstrumenter; this.captureQueryParameters = captureQueryParameters; + this.sqlCommenterEnabled = sqlCommenterEnabled; } // visible for testing @@ -85,26 +89,43 @@ public static Connection create( DbInfo dbInfo, Instrumenter statementInstrumenter, Instrumenter transactionInstrumenter, - boolean captureQueryParameters) { + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { if (hasJdbc43) { return new OpenTelemetryConnectionJdbc43( - delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + delegate, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } return new OpenTelemetryConnection( - delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + delegate, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); + } + + private String processQuery(String sql) { + return sqlCommenterEnabled ? SqlCommenterUtil.processQuery(sql) : sql; } @Override public Statement createStatement() throws SQLException { Statement statement = delegate.createStatement(); - return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>( + statement, this, dbInfo, statementInstrumenter, sqlCommenterEnabled); } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency); - return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>( + statement, this, dbInfo, statementInstrumenter, sqlCommenterEnabled); } @Override @@ -112,79 +133,146 @@ public Statement createStatement( int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { Statement statement = delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); - return new OpenTelemetryStatement<>(statement, this, dbInfo, statementInstrumenter); + return new OpenTelemetryStatement<>( + statement, this, dbInfo, statementInstrumenter, sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { - PreparedStatement statement = delegate.prepareStatement(sql); + String processedSql = processQuery(sql); + PreparedStatement statement = delegate.prepareStatement(processedSql); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + String processedSql = processQuery(sql); PreparedStatement statement = - delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); + delegate.prepareStatement(processedSql, resultSetType, resultSetConcurrency); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + String processedSql = processQuery(sql); PreparedStatement statement = - delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + delegate.prepareStatement( + processedSql, resultSetType, resultSetConcurrency, resultSetHoldability); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { - PreparedStatement statement = delegate.prepareStatement(sql, autoGeneratedKeys); + String processedSql = processQuery(sql); + PreparedStatement statement = delegate.prepareStatement(processedSql, autoGeneratedKeys); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { - PreparedStatement statement = delegate.prepareStatement(sql, columnIndexes); + String processedSql = processQuery(sql); + PreparedStatement statement = delegate.prepareStatement(processedSql, columnIndexes); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - PreparedStatement statement = delegate.prepareStatement(sql, columnNames); + String processedSql = processQuery(sql); + PreparedStatement statement = delegate.prepareStatement(processedSql, columnNames); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public CallableStatement prepareCall(String sql) throws SQLException { - CallableStatement statement = delegate.prepareCall(sql); + String processedSql = processQuery(sql); + CallableStatement statement = delegate.prepareCall(processedSql); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { - CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency); + String processedSql = processQuery(sql); + CallableStatement statement = + delegate.prepareCall(processedSql, resultSetType, resultSetConcurrency); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + String processedSql = processQuery(sql); CallableStatement statement = - delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + delegate.prepareCall( + processedSql, resultSetType, resultSetConcurrency, resultSetHoldability); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); + statement, + this, + dbInfo, + sql, + statementInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @Override @@ -413,9 +501,15 @@ static class OpenTelemetryConnectionJdbc43 extends OpenTelemetryConnection { DbInfo dbInfo, Instrumenter statementInstrumenter, Instrumenter transactionInstrumenter, - boolean captureQueryParameters) { + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { super( - delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters); + delegate, + dbInfo, + statementInstrumenter, + transactionInstrumenter, + captureQueryParameters, + sqlCommenterEnabled); } @SuppressWarnings("Since15") diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java index a5b4e7f6520c..05cfcfa15a9b 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryPreparedStatement.java @@ -58,8 +58,9 @@ public OpenTelemetryPreparedStatement( DbInfo dbInfo, String query, Instrumenter instrumenter, - boolean captureQueryParameters) { - super(delegate, connection, dbInfo, query, instrumenter); + boolean captureQueryParameters, + boolean sqlCommenterEnabled) { + super(delegate, connection, dbInfo, query, instrumenter, sqlCommenterEnabled); this.captureQueryParameters = captureQueryParameters; this.parameters = new HashMap<>(); } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java index 759f2d7192fb..56551fd90b81 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java @@ -22,6 +22,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenterUtil; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.sql.Connection; @@ -40,6 +41,7 @@ class OpenTelemetryStatement implements Statement { protected final DbInfo dbInfo; protected final String query; protected final Instrumenter instrumenter; + protected final boolean sqlCommenterEnabled; private final List batchCommands = new ArrayList<>(); protected long batchSize; @@ -48,8 +50,9 @@ class OpenTelemetryStatement implements Statement { S delegate, OpenTelemetryConnection connection, DbInfo dbInfo, - Instrumenter instrumenter) { - this(delegate, connection, dbInfo, null, instrumenter); + Instrumenter instrumenter, + boolean sqlCommenterEnabled) { + this(delegate, connection, dbInfo, null, instrumenter, sqlCommenterEnabled); } OpenTelemetryStatement( @@ -57,57 +60,72 @@ class OpenTelemetryStatement implements Statement { OpenTelemetryConnection connection, DbInfo dbInfo, String query, - Instrumenter instrumenter) { + Instrumenter instrumenter, + boolean sqlCommenterEnabled) { this.delegate = delegate; this.connection = connection; this.dbInfo = dbInfo; this.query = query; this.instrumenter = instrumenter; + this.sqlCommenterEnabled = sqlCommenterEnabled; + } + + private String processQuery(String sql) { + return sqlCommenterEnabled ? SqlCommenterUtil.processQuery(sql) : sql; } @Override public ResultSet executeQuery(String sql) throws SQLException { - return wrapCall(sql, () -> delegate.executeQuery(sql)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeQuery(processedSql)); } @Override public int executeUpdate(String sql) throws SQLException { - return wrapCall(sql, () -> delegate.executeUpdate(sql)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeUpdate(processedSql)); } @Override public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - return wrapCall(sql, () -> delegate.executeUpdate(sql, autoGeneratedKeys)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeUpdate(processedSql, autoGeneratedKeys)); } @Override public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - return wrapCall(sql, () -> delegate.executeUpdate(sql, columnIndexes)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeUpdate(processedSql, columnIndexes)); } @Override public int executeUpdate(String sql, String[] columnNames) throws SQLException { - return wrapCall(sql, () -> delegate.executeUpdate(sql, columnNames)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeUpdate(processedSql, columnNames)); } @Override public boolean execute(String sql) throws SQLException { - return wrapCall(sql, () -> delegate.execute(sql)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.execute(processedSql)); } @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - return wrapCall(sql, () -> delegate.execute(sql, autoGeneratedKeys)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.execute(processedSql, autoGeneratedKeys)); } @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { - return wrapCall(sql, () -> delegate.execute(sql, columnIndexes)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.execute(processedSql, columnIndexes)); } @Override public boolean execute(String sql, String[] columnNames) throws SQLException { - return wrapCall(sql, () -> delegate.execute(sql, columnNames)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.execute(processedSql, columnNames)); } @Override @@ -229,7 +247,8 @@ public int getResultSetType() throws SQLException { @Override public void addBatch(String sql) throws SQLException { - delegate.addBatch(sql); + String processedSql = processQuery(sql); + delegate.addBatch(processedSql); batchCommands.add(sql); batchSize++; } @@ -315,22 +334,26 @@ public long[] executeLargeBatch() throws SQLException { @Override public long executeLargeUpdate(String sql) throws SQLException { - return wrapCall(sql, () -> delegate.executeLargeUpdate(sql)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeLargeUpdate(processedSql)); } @Override public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - return wrapCall(sql, () -> delegate.executeLargeUpdate(sql, autoGeneratedKeys)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeLargeUpdate(processedSql, autoGeneratedKeys)); } @Override public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { - return wrapCall(sql, () -> delegate.executeLargeUpdate(sql, columnIndexes)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeLargeUpdate(processedSql, columnIndexes)); } @Override public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { - return wrapCall(sql, () -> delegate.executeLargeUpdate(sql, columnNames)); + String processedSql = processQuery(sql); + return wrapCall(sql, () -> delegate.executeLargeUpdate(processedSql, columnNames)); } // JDBC 4.3 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 eb30c43c791f..6fab871f1413 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 @@ -24,6 +24,8 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; @@ -42,31 +44,46 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class OpenTelemetryConnectionTest { @RegisterExtension private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); - @Test - void testVerifyCreateStatement() throws SQLException { - OpenTelemetryConnection connection = getConnection(); + private static final List executedSql = new ArrayList<>(); + + @BeforeEach + void resetTest() { + executedSql.clear(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testVerifyCreateStatement(boolean sqlCommenterEnabled) throws SQLException { + OpenTelemetryConnection connection = getConnection(sqlCommenterEnabled); String query = "SELECT * FROM users"; Statement statement = connection.createStatement(); - testing.runWithSpan( - "parent", - () -> { - assertThat(statement.execute(query)).isTrue(); - }); + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + assertThat(statement.execute(query)).isTrue(); + return Span.current().getSpanContext(); + }); + assertExecutedSql(executedSql, query, sqlCommenterEnabled, spanContext); jdbcTraceAssertion(connection.getDbInfo(), query); statement.close(); @@ -88,63 +105,75 @@ void testVerifyCreateStatementReturnsOtelWrapper() throws Exception { connection.close(); } - @Test - void testVerifyPrepareStatement() throws SQLException { - OpenTelemetryConnection connection = getConnection(); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testVerifyPrepareStatement(boolean sqlCommenterEnabled) throws SQLException { + OpenTelemetryConnection connection = getConnection(sqlCommenterEnabled); String query = "SELECT * FROM users"; - PreparedStatement statement = connection.prepareStatement(query); - - testing.runWithSpan( - "parent", - () -> { - assertThat(statement.execute()).isTrue(); - ResultSet resultSet = statement.getResultSet(); - assertThat(resultSet).isInstanceOf(OpenTelemetryResultSet.class); - assertThat(resultSet.getStatement()).isEqualTo(statement); - }); + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareStatement(query); + assertThat(statement.execute()).isTrue(); + ResultSet resultSet = statement.getResultSet(); + assertThat(resultSet).isInstanceOf(OpenTelemetryResultSet.class); + assertThat(resultSet.getStatement()).isEqualTo(statement); + statement.close(); + return Span.current().getSpanContext(); + }); + + assertExecutedSql(executedSql, query, sqlCommenterEnabled, spanContext); jdbcTraceAssertion(connection.getDbInfo(), query); - statement.close(); connection.close(); } - @Test - void testVerifyPrepareStatementUpdate() throws SQLException { - OpenTelemetryConnection connection = getConnection(); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testVerifyPrepareStatementUpdate(boolean sqlCommenterEnabled) throws SQLException { + OpenTelemetryConnection connection = getConnection(sqlCommenterEnabled); String query = "UPDATE users SET name = name"; - PreparedStatement statement = connection.prepareStatement(query); - - testing.runWithSpan( - "parent", - () -> { - statement.executeUpdate(); - assertThat(statement.getResultSet()).isNull(); - }); + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareStatement(query); + statement.executeUpdate(); + assertThat(statement.getResultSet()).isNull(); + statement.close(); + return Span.current().getSpanContext(); + }); + + assertExecutedSql(executedSql, query, sqlCommenterEnabled, spanContext); jdbcTraceAssertion(connection.getDbInfo(), query, "UPDATE"); - statement.close(); connection.close(); } - @Test - void testVerifyPrepareStatementQuery() throws SQLException { - OpenTelemetryConnection connection = getConnection(); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testVerifyPrepareStatementQuery(boolean sqlCommenterEnabled) throws SQLException { + OpenTelemetryConnection connection = getConnection(sqlCommenterEnabled); String query = "SELECT * FROM users"; - PreparedStatement statement = connection.prepareStatement(query); - - testing.runWithSpan( - "parent", - () -> { - ResultSet resultSet = statement.executeQuery(); - assertThat(resultSet).isInstanceOf(OpenTelemetryResultSet.class); - assertThat(resultSet.getStatement()).isEqualTo(statement); - }); + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareStatement(query); + ResultSet resultSet = statement.executeQuery(); + assertThat(resultSet).isInstanceOf(OpenTelemetryResultSet.class); + assertThat(resultSet.getStatement()).isEqualTo(statement); + statement.close(); + return Span.current().getSpanContext(); + }); + + assertExecutedSql(executedSql, query, sqlCommenterEnabled, spanContext); jdbcTraceAssertion(connection.getDbInfo(), query); - statement.close(); connection.close(); } @@ -175,21 +204,25 @@ void testVerifyPrepareStatementReturnsOtelWrapper() throws Exception { connection.close(); } - @Test - void testVerifyPrepareCall() throws SQLException { - OpenTelemetryConnection connection = getConnection(); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testVerifyPrepareCall(boolean sqlCommenterEnabled) throws SQLException { + OpenTelemetryConnection connection = getConnection(sqlCommenterEnabled); String query = "SELECT * FROM users"; - PreparedStatement statement = connection.prepareCall(query); - - testing.runWithSpan( - "parent", - () -> { - assertThat(statement.execute()).isTrue(); - }); + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + PreparedStatement statement = connection.prepareCall(query); + assertThat(statement.execute()).isTrue(); + statement.close(); + return Span.current().getSpanContext(); + }); + + assertExecutedSql(executedSql, query, sqlCommenterEnabled, spanContext); jdbcTraceAssertion(connection.getDbInfo(), query); - statement.close(); connection.close(); } @@ -246,7 +279,7 @@ void testVerifyPrepareStatementParameters() throws SQLException, MalformedURLExc DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = new OpenTelemetryConnection( - new TestConnection(), dbInfo, instrumenter, transactionInstrumenter, true); + new TestConnection(), dbInfo, instrumenter, transactionInstrumenter, true, false); String query = "SELECT * FROM users WHERE id=? AND age=3"; String sanitized = "SELECT * FROM users WHERE id=? AND age=3"; PreparedStatement statement = connection.prepareStatement(query); @@ -374,17 +407,48 @@ private static void transactionTraceAssertion(DbInfo dbInfo, String operation) { equalTo(SERVER_PORT, dbInfo.getPort())))); } + private static void assertExecutedSql( + List executedSql, + String query, + boolean sqlCommenterEnabled, + SpanContext spanContext) { + assertThat(executedSql).hasSize(1); + if (sqlCommenterEnabled) { + assertThat(executedSql.get(0)) + .contains(query) + .contains("traceparent") + .contains(spanContext.getTraceId()) + .contains(spanContext.getSpanId()); + } else { + assertThat(executedSql.get(0)).isEqualTo(query); + } + } + private static OpenTelemetryConnection getConnection() { - return getConnection(testing.getOpenTelemetry()); + return getConnection(false); + } + + private static OpenTelemetryConnection getConnection(boolean sqlCommenterEnabled) { + return getConnection(testing.getOpenTelemetry(), sqlCommenterEnabled); } private static OpenTelemetryConnection getConnection(OpenTelemetry openTelemetry) { + return getConnection(openTelemetry, false); + } + + private static OpenTelemetryConnection getConnection( + OpenTelemetry openTelemetry, boolean sqlCommenterEnabled) { Instrumenter statementInstrumenter = createStatementInstrumenter(openTelemetry); Instrumenter transactionInstrumenter = createTransactionInstrumenter(openTelemetry, true); DbInfo dbInfo = getDbInfo(); return new OpenTelemetryConnection( - new TestConnection(), dbInfo, statementInstrumenter, transactionInstrumenter, false); + new TestConnection(executedSql::add), + dbInfo, + statementInstrumenter, + transactionInstrumenter, + false, + sqlCommenterEnabled); } } diff --git a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestConnection.java b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestConnection.java index 2815a1c0946b..d32c8d29134a 100644 --- a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestConnection.java +++ b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestConnection.java @@ -24,13 +24,17 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** A JDBC connection class that optionally throws an exception in the constructor, used to test */ public class TestConnection implements Connection { private String url; + Consumer sqlConsumer = unused -> {}; - public TestConnection() { - this(false); + public TestConnection() {} + + public TestConnection(Consumer sqlConsumer) { + this.sqlConsumer = sqlConsumer; } public TestConnection(boolean throwException) { @@ -78,19 +82,19 @@ public SQLXML createSQLXML() throws SQLException { @Override public Statement createStatement() throws SQLException { - return new TestStatement(this); + return new TestStatement(this, sqlConsumer); } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { - return new TestStatement(this); + return new TestStatement(this, sqlConsumer); } @Override public Statement createStatement( int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { - return new TestStatement(this); + return new TestStatement(this, sqlConsumer); } @Override @@ -183,12 +187,14 @@ public String nativeSQL(String sql) throws SQLException { @Override public CallableStatement prepareCall(String sql) throws SQLException { + sqlConsumer.accept(sql); return new TestCallableStatement(this); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + sqlConsumer.accept(sql); return new TestCallableStatement(this); } @@ -196,17 +202,20 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + sqlConsumer.accept(sql); return new TestCallableStatement(this); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } @@ -214,21 +223,25 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res public PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + sqlConsumer.accept(sql); return new TestPreparedStatement(this); } diff --git a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestPreparedStatement.java b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestPreparedStatement.java index 8eb3806d0347..9850972a19a2 100644 --- a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestPreparedStatement.java +++ b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestPreparedStatement.java @@ -226,4 +226,9 @@ public void setURL(int parameterIndex, URL x) throws SQLException {} @Override @Deprecated public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {} + + @Override + public long executeLargeUpdate() throws SQLException { + return 0; + } } diff --git a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestStatement.java b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestStatement.java index 893f47257353..c006283ceda9 100644 --- a/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestStatement.java +++ b/instrumentation/jdbc/testing/src/main/java/io/opentelemetry/instrumentation/jdbc/TestStatement.java @@ -10,12 +10,20 @@ import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.function.Consumer; class TestStatement implements Statement { private final Connection connection; + private final Consumer sqlConsumer; TestStatement(Connection connection) { this.connection = connection; + this.sqlConsumer = unused -> {}; + } + + TestStatement(Connection connection, Consumer sqlConsumer) { + this.connection = connection; + this.sqlConsumer = sqlConsumer; } protected boolean hasResultSet() { @@ -23,7 +31,9 @@ protected boolean hasResultSet() { } @Override - public void addBatch(String sql) throws SQLException {} + public void addBatch(String sql) throws SQLException { + sqlConsumer.accept(sql); + } @Override public void cancel() throws SQLException {} @@ -42,21 +52,25 @@ public void closeOnCompletion() throws SQLException {} @Override public boolean execute(String sql) throws SQLException { + sqlConsumer.accept(sql); return true; } @Override public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + sqlConsumer.accept(sql); return true; } @Override public boolean execute(String sql, int[] columnIndexes) throws SQLException { + sqlConsumer.accept(sql); return true; } @Override public boolean execute(String sql, String[] columnNames) throws SQLException { + sqlConsumer.accept(sql); return true; } @@ -67,26 +81,31 @@ public int[] executeBatch() throws SQLException { @Override public ResultSet executeQuery(String sql) throws SQLException { + sqlConsumer.accept(sql); return new TestResultSet(this); } @Override public int executeUpdate(String sql) throws SQLException { + sqlConsumer.accept(sql); return 0; } @Override public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + sqlConsumer.accept(sql); return 0; } @Override public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + sqlConsumer.accept(sql); return 0; } @Override public int executeUpdate(String sql, String[] columnNames) throws SQLException { + sqlConsumer.accept(sql); return 0; } @@ -209,6 +228,35 @@ public void setPoolable(boolean poolable) throws SQLException {} @Override public void setQueryTimeout(int seconds) throws SQLException {} + @Override + public long executeLargeUpdate(String sql) throws SQLException { + sqlConsumer.accept(sql); + return 0; + } + + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + sqlConsumer.accept(sql); + return 0; + } + + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + sqlConsumer.accept(sql); + return 0; + } + + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + sqlConsumer.accept(sql); + return 0; + } + + @Override + public long[] executeLargeBatch() throws SQLException { + return new long[0]; + } + @Override public T unwrap(Class iface) throws SQLException { return null; diff --git a/instrumentation/r2dbc-1.0/README.md b/instrumentation/r2dbc-1.0/README.md index bffe333ef136..0dff7bc88db9 100644 --- a/instrumentation/r2dbc-1.0/README.md +++ b/instrumentation/r2dbc-1.0/README.md @@ -1,5 +1,6 @@ -# Settings for the JDBC instrumentation +# Settings for the R2DBC instrumentation -| System property | Type | Default | Description | -|----------------------------------------------------------|---------|---------|----------------------------------------| -| `otel.instrumentation.r2dbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| System property | Type | Default | Description | +|----------------------------------------------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.r2dbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| `otel.instrumentation.r2dbc.experimental.sqlcommenter.enabled` | Boolean | `false` | Enables augmenting queries with a comment containing the tracing information. See [sqlcommenter](https://google.github.io/sqlcommenter/) for more info. WARNING: augmenting queries with tracing context will make query texts unique, which may have adverse impact on database performance. Consult with database experts before enabling. | diff --git a/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java b/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java index e733a77f8a0e..b443d2eded13 100644 --- a/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java +++ b/instrumentation/r2dbc-1.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/r2dbc/v1_0/R2dbcSingletons.java @@ -8,24 +8,36 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.R2dbcTelemetry; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.R2dbcTelemetryBuilder; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.internal.Experimental; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.shaded.internal.R2dbcNetAttributesGetter; import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig; import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; public final class R2dbcSingletons { - private static final R2dbcTelemetry TELEMETRY = - R2dbcTelemetry.builder(GlobalOpenTelemetry.get()) - .setStatementSanitizationEnabled( - AgentInstrumentationConfig.get() - .getBoolean( - "otel.instrumentation.r2dbc.statement-sanitizer.enabled", - AgentCommonConfig.get().isStatementSanitizationEnabled())) - .addAttributesExtractor( - PeerServiceAttributesExtractor.create( - R2dbcNetAttributesGetter.INSTANCE, - AgentCommonConfig.get().getPeerServiceResolver())) - .build(); + private static final R2dbcTelemetry TELEMETRY; + + static { + R2dbcTelemetryBuilder builder = + R2dbcTelemetry.builder(GlobalOpenTelemetry.get()) + .setStatementSanitizationEnabled( + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.r2dbc.statement-sanitizer.enabled", + AgentCommonConfig.get().isStatementSanitizationEnabled())) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create( + R2dbcNetAttributesGetter.INSTANCE, + AgentCommonConfig.get().getPeerServiceResolver())); + Experimental.setEnableSqlCommenter( + builder, + AgentInstrumentationConfig.get() + .getBoolean( + "otel.instrumentation.r2dbc.experimental.sqlcommenter.enabled", + AgentCommonConfig.get().isSqlCommenterEnabled())); + TELEMETRY = builder.build(); + } public static R2dbcTelemetry telemetry() { return TELEMETRY; diff --git a/instrumentation/r2dbc-1.0/library/build.gradle.kts b/instrumentation/r2dbc-1.0/library/build.gradle.kts index 76592a4ec28b..7649b6867eac 100644 --- a/instrumentation/r2dbc-1.0/library/build.gradle.kts +++ b/instrumentation/r2dbc-1.0/library/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { testImplementation(project(":instrumentation:r2dbc-1.0:testing")) testImplementation(project(":instrumentation:reactor:reactor-3.1:library")) + testImplementation("org.testcontainers:testcontainers") } tasks { diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetry.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetry.java index bdeecbcf8d49..b1609037f0d5 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetry.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetry.java @@ -8,8 +8,10 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.DbExecution; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.R2dbcSqlCommenterUtil; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.TraceProxyListener; import io.r2dbc.proxy.ProxyConnectionFactory; +import io.r2dbc.proxy.callback.ProxyConfig; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; @@ -29,15 +31,20 @@ public static R2dbcTelemetryBuilder builder(OpenTelemetry openTelemetry) { } private final Instrumenter instrumenter; + private final boolean sqlCommenterEnabled; - R2dbcTelemetry(Instrumenter instrumenter) { + R2dbcTelemetry(Instrumenter instrumenter, boolean sqlCommenterEnabled) { this.instrumenter = instrumenter; + this.sqlCommenterEnabled = sqlCommenterEnabled; } public ConnectionFactory wrapConnectionFactory( ConnectionFactory originalFactory, ConnectionFactoryOptions factoryOptions) { - return ProxyConnectionFactory.builder(originalFactory) - .listener(new TraceProxyListener(instrumenter, factoryOptions)) - .build(); + ProxyConfig proxyConfig = new ProxyConfig(); + if (sqlCommenterEnabled) { + R2dbcSqlCommenterUtil.configure(proxyConfig); + } + proxyConfig.addListener(new TraceProxyListener(instrumenter, factoryOptions)); + return ProxyConnectionFactory.builder(originalFactory, proxyConfig).build(); } } diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java index a746eb38c163..76630c6ff818 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/R2dbcTelemetryBuilder.java @@ -10,6 +10,7 @@ import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.DbExecution; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.Experimental; import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.R2dbcInstrumenterBuilder; import java.util.function.Function; @@ -20,6 +21,12 @@ public final class R2dbcTelemetryBuilder { private boolean statementSanitizationEnabled = true; private Function, ? extends SpanNameExtractor> spanNameExtractorTransformer = Function.identity(); + private boolean sqlCommenterEnabled; + + static { + Experimental.internalSetEnableSqlCommenter( + (builder, sqlCommenterEnabled) -> builder.sqlCommenterEnabled = sqlCommenterEnabled); + } R2dbcTelemetryBuilder(OpenTelemetry openTelemetry) { instrumenterBuilder = new R2dbcInstrumenterBuilder(openTelemetry); @@ -57,6 +64,7 @@ public R2dbcTelemetryBuilder setSpanNameExtractor( */ public R2dbcTelemetry build() { return new R2dbcTelemetry( - instrumenterBuilder.build(spanNameExtractorTransformer, statementSanitizationEnabled)); + instrumenterBuilder.build(spanNameExtractorTransformer, statementSanitizationEnabled), + sqlCommenterEnabled); } } diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java index 95284c985c33..df8029df6149 100644 --- a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/DbExecution.java @@ -67,7 +67,13 @@ public DbExecution(QueryExecutionInfo queryInfo, ConnectionFactoryOptions factor host != null ? "//" + host : "", port != null ? ":" + port : ""); this.rawQueryText = - queryInfo.getQueries().stream().map(QueryInfo::getQuery).collect(Collectors.joining(";\n")); + queryInfo.getQueries().stream() + .map(QueryInfo::getQuery) + .map( + query -> + R2dbcSqlCommenterUtil.getOriginalQuery(queryInfo.getConnectionInfo(), query)) + .collect(Collectors.joining(";\n")); + R2dbcSqlCommenterUtil.clearQueries(queryInfo.getConnectionInfo()); } public Integer getPort() { diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/Experimental.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/Experimental.java new file mode 100644 index 000000000000..9ba9e8fb6c14 --- /dev/null +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/Experimental.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.r2dbc.v1_0.internal; + +import io.opentelemetry.instrumentation.r2dbc.v1_0.R2dbcTelemetryBuilder; +import java.util.function.BiConsumer; +import javax.annotation.Nullable; + +/** + * This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public final class Experimental { + + @Nullable + private static volatile BiConsumer setEnableSqlCommenter; + + /** + * Sets whether to augment sql query with comment containing the tracing information. See sqlcommenter for more info. + * + *

WARNING: augmenting queries with tracing context will make query texts unique, which may + * have adverse impact on database performance. Consult with database experts before enabling. + */ + public static void setEnableSqlCommenter( + R2dbcTelemetryBuilder builder, boolean sqlCommenterEnabled) { + if (setEnableSqlCommenter != null) { + setEnableSqlCommenter.accept(builder, sqlCommenterEnabled); + } + } + + public static void internalSetEnableSqlCommenter( + BiConsumer setEnableSqlCommenter) { + Experimental.setEnableSqlCommenter = setEnableSqlCommenter; + } + + private Experimental() {} +} diff --git a/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlCommenterUtil.java b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlCommenterUtil.java new file mode 100644 index 000000000000..a90c8ed5e5ff --- /dev/null +++ b/instrumentation/r2dbc-1.0/library/src/main/java/io/opentelemetry/instrumentation/r2dbc/v1_0/internal/R2dbcSqlCommenterUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.r2dbc.v1_0.internal; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenterUtil; +import io.r2dbc.proxy.callback.ProxyConfig; +import io.r2dbc.proxy.core.ConnectionInfo; +import io.r2dbc.proxy.core.StatementInfo; +import io.r2dbc.proxy.core.ValueStore; +import io.r2dbc.proxy.listener.BindParameterConverter; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class R2dbcSqlCommenterUtil { + private static final String KEY_ORIGINAL_QUERY_MAP = "originalQueryMap"; + + public static void configure(ProxyConfig proxyConfig) { + proxyConfig.setBindParameterConverter( + new BindParameterConverter() { + @Override + public String onCreateStatement(String query, StatementInfo info) { + String modifiedQuery = SqlCommenterUtil.processQuery(query); + if (!modifiedQuery.equals(query)) { + // We store mapping from the modified query to original query on the connection + // the assumption here is that since the connection is not thread safe it won't be + // used concurrently. + storeQuery(info.getConnectionInfo(), modifiedQuery, query); + } + return modifiedQuery; + } + }); + } + + @SuppressWarnings("unchecked") + private static Map getOriginalQueryMap(ValueStore valueStore) { + return valueStore.get(KEY_ORIGINAL_QUERY_MAP, Map.class); + } + + private static void storeQuery( + ConnectionInfo connectionInfo, String modifiedQuery, String originalQuery) { + ValueStore valueStore = connectionInfo.getValueStore(); + Map queryMap = getOriginalQueryMap(valueStore); + if (queryMap == null) { + queryMap = new HashMap<>(); + valueStore.put(KEY_ORIGINAL_QUERY_MAP, queryMap); + } + queryMap.put(modifiedQuery, originalQuery); + } + + static String getOriginalQuery(ConnectionInfo connectionInfo, String query) { + Map queryMap = getOriginalQueryMap(connectionInfo.getValueStore()); + if (queryMap == null) { + return query; + } + return queryMap.getOrDefault(query, query); + } + + static void clearQueries(ConnectionInfo connectionInfo) { + connectionInfo.getValueStore().remove(KEY_ORIGINAL_QUERY_MAP); + } + + private R2dbcSqlCommenterUtil() {} +} diff --git a/instrumentation/r2dbc-1.0/library/src/test/java/io/opentelemetry/instrumentation/r2dbc/v1_0/SqlCommenterTest.java b/instrumentation/r2dbc-1.0/library/src/test/java/io/opentelemetry/instrumentation/r2dbc/v1_0/SqlCommenterTest.java new file mode 100644 index 000000000000..282c7a1d502c --- /dev/null +++ b/instrumentation/r2dbc-1.0/library/src/test/java/io/opentelemetry/instrumentation/r2dbc/v1_0/SqlCommenterTest.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.r2dbc.v1_0; + +import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT; +import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; +import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; +import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; +import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; +import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; +import static io.r2dbc.spi.ConnectionFactoryOptions.USER; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.r2dbc.v1_0.internal.Experimental; +import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.r2dbc.proxy.ProxyConnectionFactory; +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.QueryInfo; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import reactor.core.publisher.Mono; + +class SqlCommenterTest { + private static final Logger logger = LoggerFactory.getLogger(SqlCommenterTest.class); + + @RegisterExtension + private static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private static final ContextPropagationOperator tracingOperator = + ContextPropagationOperator.create(); + + private static final String USER_DB = "SA"; + private static final String PW_DB = "password123"; + private static final String DB = "tempdb"; + + private static Integer port; + private static GenericContainer container; + + @BeforeAll + static void setup() { + tracingOperator.registerOnEachOperator(); + + container = + new GenericContainer<>("mariadb:10.3.6") + .withEnv("MYSQL_ROOT_PASSWORD", PW_DB) + .withEnv("MYSQL_USER", USER_DB) + .withEnv("MYSQL_PASSWORD", PW_DB) + .withEnv("MYSQL_DATABASE", DB) + .withExposedPorts(3306) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + + container.start(); + port = container.getMappedPort(3306); + } + + @AfterAll + static void stop() { + container.stop(); + tracingOperator.resetOnEachOperator(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSqlCommenter(boolean sqlCommenterEnabled) { + ConnectionFactoryOptions options = + ConnectionFactoryOptions.builder() + .option(DRIVER, "mariadb") + .option(HOST, container.getHost()) + .option(PORT, port) + .option(USER, USER_DB) + .option(PASSWORD, PW_DB) + .option(DATABASE, DB) + .option(CONNECT_TIMEOUT, Duration.ofSeconds(30)) + .build(); + ConnectionFactory original = ConnectionFactories.find(options); + + List queries = new ArrayList<>(); + + R2dbcTelemetryBuilder builder = R2dbcTelemetry.builder(testing.getOpenTelemetry()); + Experimental.setEnableSqlCommenter(builder, sqlCommenterEnabled); + ConnectionFactory connectionFactory = + builder + .build() + .wrapConnectionFactory( + ProxyConnectionFactory.builder(original) + .listener( + new ProxyExecutionListener() { + @Override + public void beforeQuery(QueryExecutionInfo execInfo) { + for (QueryInfo queryInfo : execInfo.getQueries()) { + queries.add(queryInfo.getQuery()); + } + } + }) + .build(), + options); + + SpanContext spanContext = + testing.runWithSpan( + "parent", + () -> { + Mono.from(connectionFactory.create()) + .flatMapMany( + connection -> + Mono.from(connection.createStatement("SELECT 3").execute()) + // Subscribe to the Statement.execute() + .flatMapMany(result -> result.map((row, metadata) -> "")) + .concatWith(Mono.from(connection.close()).cast(String.class))) + .doFinally(e -> testing.runWithSpan("child", () -> {})) + .blockLast(Duration.ofMinutes(1)); + return Span.current().getSpanContext(); + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT " + DB) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)), + span -> + span.hasName("child").hasKind(SpanKind.INTERNAL).hasParent(trace.getSpan(0)))); + + assertThat(queries).hasSize(1); + if (sqlCommenterEnabled) { + assertThat(queries.get(0)) + .contains("SELECT 3") + .contains("traceparent") + .contains(spanContext.getTraceId()) + .contains(spanContext.getSpanId()); + } else { + assertThat(queries.get(0)).isEqualTo("SELECT 3"); + } + } +} 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 a1a615a31fba..ad11fa09c991 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 @@ -8,6 +8,8 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetry; +import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetryBuilder; +import io.opentelemetry.instrumentation.jdbc.datasource.internal.Experimental; import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.io.PrintWriter; @@ -57,7 +59,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { && !isRoutingDatasource(bean) && !ScopedProxyUtils.isScopedTarget(beanName)) { DataSource dataSource = (DataSource) bean; - DataSource otelDataSource = + JdbcTelemetryBuilder builder = JdbcTelemetry.builder(openTelemetryProvider.getObject()) .setStatementSanitizationEnabled( InstrumentationConfigUtil.isStatementSanitizationEnabled( @@ -72,9 +74,13 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { configPropertiesProvider .getObject() .getBoolean( - "otel.instrumentation.jdbc.experimental.transaction.enabled", false)) - .build() - .wrap(dataSource); + "otel.instrumentation.jdbc.experimental.transaction.enabled", false)); + Experimental.setEnableSqlCommenter( + builder, + configPropertiesProvider + .getObject() + .getBoolean("otel.instrumentation.jdbc.experimental.sqlcommenter.enabled", false)); + DataSource otelDataSource = builder.build().wrap(dataSource); // wrap instrumented data source into a proxy that unwraps to the original data source // see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13512 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 e92a74381443..56c379c261cc 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 @@ -369,6 +369,12 @@ "description": "Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations.", "defaultValue": false }, + { + "name": "otel.instrumentation.jdbc.experimental.sqlcommenter.enabled", + "type": "java.lang.Boolean", + "description": "Enables augmenting queries with a comment containing the tracing information. See [sqlcommenter](https://google.github.io/sqlcommenter/) for more info. WARNING: augmenting queries with tracing context will make query texts unique, which may have adverse impact on database performance. Consult with database experts before enabling.", + "defaultValue": false + }, { "name": "otel.instrumentation.kafka.enabled", "type": "java.lang.Boolean",