diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java index 733a8a2eaee1..92eb3f260803 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java @@ -12,7 +12,9 @@ import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import io.opentelemetry.semconv.AttributeKeyTemplate; import java.util.Collection; +import java.util.Map; /** * Extractor of AttributeKey.stringKey("db.collection.name"); private static final AttributeKey DB_OPERATION_BATCH_SIZE = AttributeKey.longKey("db.operation.batch.size"); + private static final AttributeKeyTemplate DB_QUERY_PARAMETER = + AttributeKeyTemplate.stringKeyTemplate("db.query.parameter"); /** Creates the SQL client attributes extractor with default configuration. */ public static AttributesExtractor create( @@ -58,26 +62,35 @@ public static SqlClientAttributesExtractorBuilder oldSemconvTableAttribute; private final boolean statementSanitizationEnabled; + private final boolean captureQueryParameters; SqlClientAttributesExtractor( SqlClientAttributesGetter getter, AttributeKey oldSemconvTableAttribute, - boolean statementSanitizationEnabled) { + boolean statementSanitizationEnabled, + boolean captureQueryParameters) { super(getter); this.oldSemconvTableAttribute = oldSemconvTableAttribute; - this.statementSanitizationEnabled = statementSanitizationEnabled; + // captureQueryParameters disables statementSanitizationEnabled + this.statementSanitizationEnabled = !captureQueryParameters && statementSanitizationEnabled; + this.captureQueryParameters = captureQueryParameters; } @Override + @SuppressWarnings("AlreadyChecked") public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) { super.onStart(attributes, parentContext, request); Collection rawQueryTexts = getter.getRawQueryTexts(request); + Map queryParameters = getter.getQueryParameters(request); if (rawQueryTexts.isEmpty()) { return; } + Long batchSize = getter.getBatchSize(request); + boolean isBatch = batchSize != null && batchSize > 1; + if (SemconvStability.emitOldDatabaseSemconv()) { if (rawQueryTexts.size() == 1) { // for backcompat(?) String rawQueryText = rawQueryTexts.iterator().next(); @@ -91,12 +104,11 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST if (!SQL_CALL.equals(operation)) { internalSet(attributes, oldSemconvTableAttribute, sanitizedStatement.getMainIdentifier()); } + setQueryParameters(attributes, isBatch, queryParameters); } } if (SemconvStability.emitStableDatabaseSemconv()) { - Long batchSize = getter.getBatchSize(request); - boolean isBatch = batchSize != null && batchSize > 1; if (isBatch) { internalSet(attributes, DB_OPERATION_BATCH_SIZE, batchSize); } @@ -112,6 +124,7 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST if (!SQL_CALL.equals(operation)) { internalSet(attributes, DB_COLLECTION_NAME, sanitizedStatement.getMainIdentifier()); } + setQueryParameters(attributes, isBatch, queryParameters); } else { MultiQuery multiQuery = MultiQuery.analyze(getter.getRawQueryTexts(request), statementSanitizationEnabled); @@ -129,6 +142,19 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST } } + private void setQueryParameters( + AttributesBuilder attributes, boolean isBatch, Map queryParameters) { + if (captureQueryParameters && !isBatch && queryParameters != null) { + for (Map.Entry entry : queryParameters.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value != null) { + internalSet(attributes, DB_QUERY_PARAMETER.getAttributeKey(key), value); + } + } + } + } + // String.join is not available on android private static String join(String delimiter, Collection collection) { StringBuilder builder = new StringBuilder(); diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java index 43cbc63062bd..ecd2b89066d9 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorBuilder.java @@ -20,6 +20,7 @@ public final class SqlClientAttributesExtractorBuilder { final SqlClientAttributesGetter getter; AttributeKey oldSemconvTableAttribute = DB_SQL_TABLE; boolean statementSanitizationEnabled = true; + boolean captureQueryParameters = false; SqlClientAttributesExtractorBuilder(SqlClientAttributesGetter getter) { this.getter = getter; @@ -48,12 +49,24 @@ public SqlClientAttributesExtractorBuilder setStatementSaniti return this; } + /** + * Sets whether the {@code db.query.parameter.} attributes extracted by the constructed + * {@link SqlClientAttributesExtractor} should be opted-in. If set to {@code true}, all parameters + * from {@code PreparedStatement} will be exposed as attributes. Disabled by default. + */ + @CanIgnoreReturnValue + public SqlClientAttributesExtractorBuilder setCaptureQueryParameters( + boolean captureQueryParameters) { + this.captureQueryParameters = captureQueryParameters; + return this; + } + /** * Returns a new {@link SqlClientAttributesExtractor} with the settings of this {@link * SqlClientAttributesExtractorBuilder}. */ public AttributesExtractor build() { return new SqlClientAttributesExtractor<>( - getter, oldSemconvTableAttribute, statementSanitizationEnabled); + getter, oldSemconvTableAttribute, statementSanitizationEnabled, captureQueryParameters); } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java index 1b795363e7b9..bdf64e638a1c 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesGetter.java @@ -9,6 +9,8 @@ import static java.util.Collections.singleton; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import javax.annotation.Nullable; /** @@ -66,4 +68,9 @@ default Collection getRawQueryTexts(REQUEST request) { default Long getBatchSize(REQUEST request) { return null; } + + // TODO: make this required to implement + default Map getQueryParameters(REQUEST request) { + return Collections.emptyMap(); + } } diff --git a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java index d3cd7dc03220..2a42564a252c 100644 --- a/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java +++ b/instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractorTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.api.incubator.semconv.db; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_QUERY_PARAMETER; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.entry; @@ -22,6 +23,7 @@ import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; +import org.spockframework.util.Nullable; @SuppressWarnings("deprecation") // using deprecated semconv class SqlClientAttributesExtractorTest { @@ -62,6 +64,22 @@ public Long getBatchSize(Map map) { return read(map, "db.operation.batch.size", Long.class); } + @Nullable + @Override + public Map getQueryParameters(Map map) { + String parameterString = read(map, "db.query.parameter"); + + if (parameterString == null) { + return Collections.emptyMap(); + } + + Map parameters = new HashMap<>(); + for (String s : parameterString.split(";")) { + parameters.put(Integer.toString(parameters.size()), s); + } + return parameters; + } + protected String read(Map map, String key) { return read(map, key, String.class); } @@ -387,4 +405,79 @@ void shouldIgnoreBatchSizeOne() { assertThat(endAttributes.build().isEmpty()).isTrue(); } + + @Test + void shouldExtractQueryParameters() { + // given + Map request = new HashMap<>(); + request.put("db.name", "potatoes"); + // a query with prepared parameters and parameters to sanitize + request.put( + "db.statement", + "SELECT col FROM table WHERE field1=? AND field2='A' AND field3=? AND field4=2"); + // a prepared parameters map + request.put("db.query.parameter", "'a';1"); + + Context context = Context.root(); + + AttributesExtractor, Void> underTest = + SqlClientAttributesExtractor.builder(new TestAttributesGetter()) + .setCaptureQueryParameters(true) + .build(); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, context, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, context, request, null, null); + + String prefix = DB_QUERY_PARAMETER.getAttributeKey("").getKey(); + Attributes queryParameterAttributes = + startAttributes.removeIf(attribute -> !attribute.getKey().startsWith(prefix)).build(); + + // then + assertThat(queryParameterAttributes) + .containsOnly( + entry(DB_QUERY_PARAMETER.getAttributeKey("0"), "'a'"), + entry(DB_QUERY_PARAMETER.getAttributeKey("1"), "1")); + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } + + @Test + void shouldNotExtractQueryParametersForBatch() { + // given + Map request = new HashMap<>(); + request.put("db.name", "potatoes"); + request.put("db.statements", singleton("INSERT INTO potato VALUES(?)")); + request.put("db.operation.batch.size", 1L); + request.put("db.query.parameter", "1"); + + Context context = Context.root(); + + AttributesExtractor, Void> underTest = + SqlClientAttributesExtractor.create(new TestMultiAttributesGetter()); + + // when + AttributesBuilder startAttributes = Attributes.builder(); + underTest.onStart(startAttributes, context, request); + + AttributesBuilder endAttributes = Attributes.builder(); + underTest.onEnd(endAttributes, context, request, null, null); + + // then + if (SemconvStability.emitStableDatabaseSemconv() && SemconvStability.emitOldDatabaseSemconv()) { + assertThat(startAttributes.build()) + .doesNotContainKey(DB_QUERY_PARAMETER.getAttributeKey("0")); + } else if (SemconvStability.emitOldDatabaseSemconv()) { + assertThat(startAttributes.build()) + .doesNotContainKey(DB_QUERY_PARAMETER.getAttributeKey("0")); + } else if (SemconvStability.emitStableDatabaseSemconv()) { + assertThat(startAttributes.build()) + .doesNotContainKey(DB_QUERY_PARAMETER.getAttributeKey("0")); + } + + assertThat(endAttributes.build().isEmpty()).isTrue(); + } } diff --git a/instrumentation/jdbc/README.md b/instrumentation/jdbc/README.md index 7e3440fd8c01..0cc193697918 100644 --- a/instrumentation/jdbc/README.md +++ b/instrumentation/jdbc/README.md @@ -1,5 +1,6 @@ # Settings for the JDBC instrumentation -| System property | Type | Default | Description | -|---------------------------------------------------------|---------|---------|----------------------------------------| -| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| System property | Type | Default | Description | +|---------------------------------------------------------|---------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. | +| `otel.instrumentation.jdbc.capture-query-parameters` | Boolean | `false` | Enables the attribute db.query.parameter.\.

WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info. Exposing such info may result in substantial fines and penalties or criminal liability. Consult your peers, superiors and a legal counsel before enabling this option.

This option will disable otel.instrumentation.jdbc.statement-sanitizer | diff --git a/instrumentation/jdbc/javaagent/build.gradle.kts b/instrumentation/jdbc/javaagent/build.gradle.kts index ac4617836eaf..be106066c3a3 100644 --- a/instrumentation/jdbc/javaagent/build.gradle.kts +++ b/instrumentation/jdbc/javaagent/build.gradle.kts @@ -65,6 +65,7 @@ tasks { test { filter { excludeTestsMatching("SlickTest") + excludeTestsMatching("PreparedStatementParametersTest") } jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true") } @@ -72,6 +73,7 @@ tasks { val testStableSemconv by registering(Test::class) { filter { excludeTestsMatching("SlickTest") + excludeTestsMatching("PreparedStatementParametersTest") } jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true") jvmArgs("-Dotel.semconv-stability.opt-in=database") @@ -85,9 +87,18 @@ tasks { jvmArgs("-Dotel.semconv-stability.opt-in=database") } + val testCaptureParameters by registering(Test::class) { + filter { + includeTestsMatching("PreparedStatementParametersTest") + } + jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true") + jvmArgs("-Dotel.instrumentation.jdbc.capture-query-parameters=true") + } + check { dependsOn(testSlick) dependsOn(testStableSemconv) dependsOn(testSlickStableSemconv) + dependsOn(testCaptureParameters) } } diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcSingletons.java index e9c5290aa217..a71627290e84 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 @@ -29,11 +29,16 @@ public final class JdbcSingletons { private static final Instrumenter STATEMENT_INSTRUMENTER; public static final Instrumenter DATASOURCE_INSTRUMENTER = createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true); + public static final boolean CAPTURE_QUERY_PARAMETERS; static { JdbcAttributesGetter dbAttributesGetter = new JdbcAttributesGetter(); JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter(); + CAPTURE_QUERY_PARAMETERS = + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.jdbc.capture-query-parameters", false); + STATEMENT_INSTRUMENTER = Instrumenter.builder( GlobalOpenTelemetry.get(), @@ -46,6 +51,7 @@ public final class JdbcSingletons { .getBoolean( "otel.instrumentation.jdbc.statement-sanitizer.enabled", AgentCommonConfig.get().isStatementSanitizationEnabled())) + .setCaptureQueryParameters(CAPTURE_QUERY_PARAMETERS) .build()) .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) .addAttributesExtractor( diff --git a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java index a822ab98073e..407109cb8c89 100644 --- a/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java +++ b/instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java @@ -5,15 +5,18 @@ package io.opentelemetry.javaagent.instrumentation.jdbc; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcPreparedStatementStringifier.stringifyParameter; import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.CAPTURE_QUERY_PARAMETERS; import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.statementInstrumenter; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; @@ -24,8 +27,15 @@ import io.opentelemetry.javaagent.bootstrap.CallDepth; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.URL; +import java.sql.Date; import java.sql.PreparedStatement; +import java.sql.RowId; import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -53,6 +63,33 @@ public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( named("addBatch").and(takesNoArguments()).and(isPublic()), PreparedStatementInstrumentation.class.getName() + "$AddBatchAdvice"); + transformer.applyAdviceToMethod( + namedOneOf( + "setBoolean", + "setShort", + "setInt", + "setLong", + "setFloat", + "setDouble", + "setBigDecimal", + "setString", + "setDate", + "setTime", + "setTimestamp", + "setURL", + "setRowId", + "setNString") + .and(takesArgument(0, int.class)) + .and(takesArguments(2)) + .and(isPublic()), + PreparedStatementInstrumentation.class.getName() + "$SetParameter2Advice"); + transformer.applyAdviceToMethod( + namedOneOf("setDate", "setTime", "setTimestamp") + .and(takesArgument(0, int.class)) + .and(takesArgument(2, Calendar.class)) + .and(takesArguments(3)) + .and(isPublic()), + PreparedStatementInstrumentation.class.getName() + "$SetTimeParameter3Advice"); } @SuppressWarnings("unused") @@ -84,7 +121,8 @@ public static void onEnter( } Context parentContext = currentContext(); - request = DbRequest.create(statement); + Map parameters = JdbcData.parameters.get(statement); + request = DbRequest.create(statement, parameters); if (request == null || !statementInstrumenter().shouldStart(parentContext, request)) { return; @@ -120,4 +158,70 @@ public static void addBatch(@Advice.This PreparedStatement statement) { JdbcData.addPreparedStatementBatch(statement); } } + + @SuppressWarnings("unused") + public static class SetParameter2Advice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This PreparedStatement statement, + @Advice.Argument(0) int index, + @Advice.Argument(1) Object value) { + if (!CAPTURE_QUERY_PARAMETERS) { + return; + } + + String str = null; + + if (value instanceof Boolean) { + str = stringifyParameter((Boolean) value); + } else if (value instanceof Number) { + // Short, Int, Long, Float, Double, BigDecimal + str = stringifyParameter((Number) value); + } else if (value instanceof String) { + str = stringifyParameter((String) value); + } else if (value instanceof Date) { + str = stringifyParameter((Date) value); + } else if (value instanceof Time) { + str = stringifyParameter((Time) value); + } else if (value instanceof Timestamp) { + str = stringifyParameter((Timestamp) value); + } else if (value instanceof URL) { + str = stringifyParameter((URL) value); + } else if (value instanceof RowId) { + str = stringifyParameter((RowId) value); + } + + if (str != null) { + JdbcData.addParameter(statement, Integer.toString(index - 1), str); + } + } + } + + @SuppressWarnings("unused") + public static class SetTimeParameter3Advice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This PreparedStatement statement, + @Advice.Argument(0) int index, + @Advice.Argument(1) Object value, + @Advice.Argument(2) Calendar calendar) { + if (!CAPTURE_QUERY_PARAMETERS) { + return; + } + + String str = null; + + if (value instanceof Date) { + str = stringifyParameter((Date) value); + } else if (value instanceof Time) { + str = stringifyParameter((Time) value); + } else if (value instanceof Timestamp) { + str = stringifyParameter((Timestamp) value); + } + + if (str != null) { + JdbcData.addParameter(statement, Integer.toString(index - 1), str); + } + } + } } 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 93cf1711eaf0..11867af96764 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 @@ -25,6 +25,7 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import java.sql.PreparedStatement; import java.sql.Statement; +import java.util.Map; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -159,10 +160,11 @@ public static void onEnter( if (statement instanceof PreparedStatement) { Long batchSize = JdbcData.getPreparedStatementBatchSize((PreparedStatement) statement); String sql = JdbcData.preparedStatement.get((PreparedStatement) statement); + Map parameters = JdbcData.parameters.get((PreparedStatement) statement); if (sql == null) { return; } - request = DbRequest.create(statement, sql, batchSize); + request = DbRequest.create(statement, sql, batchSize, parameters); } else { JdbcData.StatementBatchInfo batchInfo = JdbcData.getStatementBatchInfo(statement); if (batchInfo == null) { diff --git a/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/PreparedStatementParametersTest.java b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/PreparedStatementParametersTest.java new file mode 100644 index 000000000000..719c055f82be --- /dev/null +++ b/instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/PreparedStatementParametersTest.java @@ -0,0 +1,825 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.jdbc.test; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_QUERY_PARAMETER; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Stream; +import org.apache.derby.jdbc.EmbeddedDriver; +import org.hsqldb.jdbc.JDBCDriver; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings("deprecation") // using deprecated semconv +class PreparedStatementParametersTest { + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final String dbName = "jdbcUnitTest"; + private static final String dbNameLower = dbName.toLowerCase(Locale.ROOT); + + private static final Map jdbcUrls = + ImmutableMap.of( + "h2", "jdbc:h2:mem:" + dbName, + "derby", "jdbc:derby:memory:" + dbName, + "hsqldb", "jdbc:hsqldb:mem:" + dbName); + private static final Map jdbcUserNames = Maps.newHashMap(); + private static final Properties connectionProps = new Properties(); + + static { + jdbcUserNames.put("derby", "APP"); + jdbcUserNames.put("h2", null); + jdbcUserNames.put("hsqldb", "SA"); + + connectionProps.put("databaseName", "someDb"); + connectionProps.put("OPEN_NEW", "true"); // So H2 doesn't complain about username/password. + connectionProps.put("create", "true"); + } + + @BeforeAll + static void setUp() {} + + @AfterAll + static void tearDown() {} + + static Stream preparedStatementStream() throws SQLException { + return Stream.of( + Arguments.of( + "h2", + new org.h2.Driver().connect(jdbcUrls.get("h2"), null), + null, + "SELECT 3, ?", + "SELECT 3, ?", + "SELECT " + dbNameLower, + "h2:mem:", + null), + Arguments.of( + "derby", + new EmbeddedDriver().connect(jdbcUrls.get("derby"), connectionProps), + "APP", + "SELECT 3 FROM SYSIBM.SYSDUMMY1 WHERE IBMREQD=? OR 1=1", + "SELECT 3 FROM SYSIBM.SYSDUMMY1 WHERE IBMREQD=? OR 1=1", + "SELECT SYSIBM.SYSDUMMY1", + "derby:memory:", + "SYSIBM.SYSDUMMY1"), + Arguments.of( + "hsqldb", + new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null), + "SA", + "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE USER_NAME=? OR 1=1", + "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE USER_NAME=? OR 1=1", + "SELECT INFORMATION_SCHEMA.SYSTEM_USERS", + "hsqldb:mem:", + "INFORMATION_SCHEMA.SYSTEM_USERS")); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testBooleanPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setBoolean(1, true); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "true")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testShortPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setShort(1, (short) 0); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testIntPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setInt(1, 0); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testLongPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setLong(1, 0); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testFloatPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setFloat(1, (float) 0.1); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0.1")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testDoublePreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setDouble(1, 0.1); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0.1")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testBigDecimalPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setBigDecimal(1, BigDecimal.ZERO); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testStringPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setString(1, "S"); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "S")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testDate2PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "CURDATE()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "CURDATE()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setDate(1, Date.valueOf("2000-01-01")); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "2000-01-01")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testDate3PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "CURDATE()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "CURDATE()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setDate(1, Date.valueOf("2000-01-01"), Calendar.getInstance()); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "2000-01-01")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testTime2PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "CURTIME()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "CURTIME()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setTime(1, Time.valueOf("00:00:00")); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "00:00:00")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testTime3PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "CURTIME()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "CURTIME()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setTime(1, Time.valueOf("00:00:00"), Calendar.getInstance()); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "00:00:00")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testTimestamp2PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "NOW()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "NOW()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setTimestamp(1, Timestamp.valueOf("2000-01-01 00:00:00")); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo( + DB_QUERY_PARAMETER.getAttributeKey("0"), + "2000-01-01 00:00:00.0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testTimestamp3PreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + String updatedColumn = query.replace("USER_NAME=?", "NOW()=?"); + String updatedColumnSanitized = sanitizedQuery.replace("USER_NAME=?", "NOW()=?"); + PreparedStatement statement = connection.prepareStatement(updatedColumn); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setTimestamp( + 1, Timestamp.valueOf("2000-01-01 00:00:00"), Calendar.getInstance()); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), updatedColumnSanitized), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo( + DB_QUERY_PARAMETER.getAttributeKey("0"), + "2000-01-01 00:00:00.0")))); + } + + @ParameterizedTest + @MethodSource("preparedStatementStream") + void testNstringPreparedStatementParameter( + String system, + Connection connection, + String username, + String query, + String sanitizedQuery, + String spanName, + String url, + String table) + throws SQLException { + if (!system.equalsIgnoreCase("derby")) { + PreparedStatement statement = connection.prepareStatement(query); + cleanup.deferCleanup(statement); + + ResultSet resultSet = + testing.runWithSpan( + "parent", + () -> { + statement.setNString(1, "S"); + statement.execute(); + return statement.getResultSet(); + }); + + resultSet.next(); + assertThat(resultSet.getInt(1)).isEqualTo(3); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(), + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)), + equalTo(maybeStable(DB_NAME), dbNameLower), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username), + equalTo( + DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url), + equalTo(maybeStable(DB_STATEMENT), sanitizedQuery), + equalTo(maybeStable(DB_OPERATION), "SELECT"), + equalTo(maybeStable(DB_SQL_TABLE), table), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "S")))); + } + } +} diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java index d50039ca9bdd..d09e00f86694 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java @@ -244,7 +244,11 @@ public Connection connect(String url, Properties info) throws SQLException { Instrumenter statementInstrumenter = JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + + boolean captureQueryParameters = JdbcInstrumenterFactory.captureQueryParameters(); + + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, captureQueryParameters); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java index cc857b823563..7bd9a614ec80 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java @@ -26,16 +26,22 @@ public static JdbcTelemetryBuilder builder(OpenTelemetry openTelemetry) { private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; + private final boolean captureQueryParameters; JdbcTelemetry( Instrumenter dataSourceInstrumenter, - Instrumenter statementInstrumenter) { + Instrumenter statementInstrumenter, + boolean captureQueryParameters) { this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; + this.captureQueryParameters = captureQueryParameters; } public DataSource wrap(DataSource dataSource) { return new OpenTelemetryDataSource( - dataSource, this.dataSourceInstrumenter, this.statementInstrumenter); + dataSource, + this.dataSourceInstrumenter, + this.statementInstrumenter, + this.captureQueryParameters); } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java index 825b29547334..00531b6d60e1 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java @@ -16,6 +16,7 @@ public final class JdbcTelemetryBuilder { private boolean dataSourceInstrumenterEnabled = true; private boolean statementInstrumenterEnabled = true; private boolean statementSanitizationEnabled = true; + private boolean captureQueryParameters = false; JdbcTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -39,6 +40,22 @@ public JdbcTelemetryBuilder setStatementInstrumenterEnabled(boolean enabled) { @CanIgnoreReturnValue public JdbcTelemetryBuilder setStatementSanitizationEnabled(boolean enabled) { this.statementSanitizationEnabled = enabled; + + if (enabled) { + this.captureQueryParameters = false; + } + + return this; + } + + @CanIgnoreReturnValue + public JdbcTelemetryBuilder setCaptureQueryParameters(boolean enabled) { + this.captureQueryParameters = enabled; + + if (enabled) { + this.statementSanitizationEnabled = false; + } + return this; } @@ -48,6 +65,10 @@ public JdbcTelemetry build() { JdbcInstrumenterFactory.createDataSourceInstrumenter( openTelemetry, dataSourceInstrumenterEnabled), JdbcInstrumenterFactory.createStatementInstrumenter( - openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled)); + openTelemetry, + statementInstrumenterEnabled, + statementSanitizationEnabled, + captureQueryParameters), + captureQueryParameters); } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/OpenTelemetryDataSource.java index deb0425ff223..45210b4b1b9a 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 @@ -48,6 +48,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable { private final Instrumenter dataSourceInstrumenter; private final Instrumenter statementInstrumenter; private volatile DbInfo cachedDbInfo; + private final boolean captureQueryParameters; /** * Create a OpenTelemetry DataSource wrapping another DataSource. @@ -71,6 +72,7 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) this.delegate = delegate; this.dataSourceInstrumenter = createDataSourceInstrumenter(openTelemetry, true); this.statementInstrumenter = createStatementInstrumenter(openTelemetry); + this.captureQueryParameters = false; } /** @@ -83,24 +85,28 @@ public OpenTelemetryDataSource(DataSource delegate, OpenTelemetry openTelemetry) OpenTelemetryDataSource( DataSource delegate, Instrumenter dataSourceInstrumenter, - Instrumenter statementInstrumenter) { + Instrumenter statementInstrumenter, + boolean captureQueryParameters) { this.delegate = delegate; this.dataSourceInstrumenter = dataSourceInstrumenter; this.statementInstrumenter = statementInstrumenter; + this.captureQueryParameters = captureQueryParameters; } @Override public Connection getConnection() throws SQLException { Connection connection = wrapCall(delegate::getConnection); DbInfo dbInfo = getDbInfo(connection); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, captureQueryParameters); } @Override public Connection getConnection(String username, String password) throws SQLException { Connection connection = wrapCall(() -> delegate.getConnection(username, password)); DbInfo dbInfo = getDbInfo(connection); - return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter); + return OpenTelemetryConnection.create( + connection, dbInfo, statementInstrumenter, captureQueryParameters); } @Override diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java index a5e8e0559178..f9568fccaa65 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/DbRequest.java @@ -15,6 +15,7 @@ import java.sql.Statement; import java.util.Collection; import java.util.Collections; +import java.util.Map; import javax.annotation.Nullable; /** @@ -25,23 +26,38 @@ public abstract class DbRequest { @Nullable - public static DbRequest create(PreparedStatement statement) { - return create(statement, JdbcData.preparedStatement.get(statement)); + public static DbRequest create( + PreparedStatement statement, Map preparedStatementParameters) { + return create( + statement, JdbcData.preparedStatement.get(statement), preparedStatementParameters); } @Nullable public static DbRequest create(Statement statement, String dbStatementString) { - return create(statement, dbStatementString, null); + return create(statement, dbStatementString, null, Collections.emptyMap()); + } + + @Nullable + public static DbRequest create( + Statement statement, + String dbStatementString, + Map preparedStatementParameters) { + return create(statement, dbStatementString, null, preparedStatementParameters); } @Nullable - public static DbRequest create(Statement statement, String dbStatementString, Long batchSize) { + public static DbRequest create( + Statement statement, + String dbStatementString, + Long batchSize, + Map preparedStatementParameters) { Connection connection = connectionFromStatement(statement); if (connection == null) { return null; } - return create(extractDbInfo(connection), dbStatementString, batchSize); + return create( + extractDbInfo(connection), dbStatementString, batchSize, preparedStatementParameters); } public static DbRequest create( @@ -51,19 +67,28 @@ public static DbRequest create( return null; } - return create(extractDbInfo(connection), queryTexts, batchSize); + return create(extractDbInfo(connection), queryTexts, batchSize, Collections.emptyMap()); } public static DbRequest create(DbInfo dbInfo, String queryText) { - return create(dbInfo, queryText, null); + return create(dbInfo, queryText, null, Collections.emptyMap()); } - public static DbRequest create(DbInfo dbInfo, String queryText, Long batchSize) { - return create(dbInfo, Collections.singletonList(queryText), batchSize); + public static DbRequest create( + DbInfo dbInfo, + String queryText, + Long batchSize, + Map preparedStatementParameters) { + return create( + dbInfo, Collections.singletonList(queryText), batchSize, preparedStatementParameters); } - public static DbRequest create(DbInfo dbInfo, Collection queryTexts, Long batchSize) { - return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize); + public static DbRequest create( + DbInfo dbInfo, + Collection queryTexts, + Long batchSize, + Map preparedStatementParameters) { + return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize, preparedStatementParameters); } public abstract DbInfo getDbInfo(); @@ -72,4 +97,7 @@ public static DbRequest create(DbInfo dbInfo, Collection queryTexts, Lon @Nullable public abstract Long getBatchSize(); + + @Nullable + public abstract Map getPreparedStatementParameters(); } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java index 74cd1c52d51a..3f687614c52d 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcAttributesGetter.java @@ -9,6 +9,8 @@ import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.sql.SQLException; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import javax.annotation.Nullable; /** @@ -62,4 +64,15 @@ public String getResponseStatus(@Nullable Void response, @Nullable Throwable err } return null; } + + @Override + public Map getQueryParameters(DbRequest request) { + Map parameters = request.getPreparedStatementParameters(); + + if (parameters == null) { + return Collections.emptyMap(); + } + + return parameters; + } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcData.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcData.java index 04f58860a5a5..d465e9299874 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcData.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcData.java @@ -13,6 +13,7 @@ import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.WeakHashMap; @@ -35,6 +36,8 @@ public final class JdbcData { private static final VirtualField preparedStatementBatch = VirtualField.find(PreparedStatement.class, PreparedStatementBatchInfo.class); + public static final VirtualField> parameters = + VirtualField.find(PreparedStatement.class, Map.class); private JdbcData() {} @@ -103,6 +106,18 @@ public static void close(Statement statement) { PreparedStatement prepared = (PreparedStatement) statement; preparedStatement.set(prepared, null); preparedStatementBatch.set(prepared, null); + parameters.set(prepared, null); + } + } + + public static void addParameter(PreparedStatement statement, String key, String value) { + if (value != null) { + Map parametersMap = parameters.get(statement); + if (parametersMap == null) { + parametersMap = new HashMap<>(); + parameters.set(statement, parametersMap); + } + parametersMap.put(key, value); } } diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java index 861e1dd11759..a72a753cd2a9 100644 --- a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcInstrumenterFactory.java @@ -29,6 +29,11 @@ public final class JdbcInstrumenterFactory { private static final JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter(); + public static boolean captureQueryParameters() { + return ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.jdbc.capture-query-parameters", false); + } + public static Instrumenter createStatementInstrumenter() { return createStatementInstrumenter(GlobalOpenTelemetry.get()); } @@ -39,11 +44,26 @@ public static Instrumenter createStatementInstrumenter( openTelemetry, true, ConfigPropertiesUtil.getBoolean( - "otel.instrumentation.common.db-statement-sanitizer.enabled", true)); + "otel.instrumentation.common.db-statement-sanitizer.enabled", true), + captureQueryParameters()); + } + + public static Instrumenter createStatementInstrumenter( + OpenTelemetry openTelemetry, boolean captureQueryParameters) { + return createStatementInstrumenter( + openTelemetry, + true, + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.common.db-statement-sanitizer.enabled", true), + ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.jdbc.capture-query-parameters", captureQueryParameters)); } public static Instrumenter createStatementInstrumenter( - OpenTelemetry openTelemetry, boolean enabled, boolean statementSanitizationEnabled) { + OpenTelemetry openTelemetry, + boolean enabled, + boolean statementSanitizationEnabled, + boolean captureQueryParameters) { return Instrumenter.builder( openTelemetry, INSTRUMENTATION_NAME, @@ -51,6 +71,7 @@ public static Instrumenter createStatementInstrumenter( .addAttributesExtractor( SqlClientAttributesExtractor.builder(dbAttributesGetter) .setStatementSanitizationEnabled(statementSanitizationEnabled) + .setCaptureQueryParameters(captureQueryParameters) .build()) .addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter)) .addOperationMetrics(DbClientMetrics.get()) diff --git a/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcPreparedStatementStringifier.java b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcPreparedStatementStringifier.java new file mode 100644 index 000000000000..ac28c2cc71b1 --- /dev/null +++ b/instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/JdbcPreparedStatementStringifier.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.jdbc.internal; + +import java.net.URL; +import java.sql.Date; +import java.sql.RowId; +import java.sql.Time; +import java.sql.Timestamp; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class JdbcPreparedStatementStringifier { + + private JdbcPreparedStatementStringifier() {} + + public static String stringifyParameter(String value) { + return value; + } + + public static String stringifyParameter(Number value) { + return value != null ? value.toString() : null; + } + + public static String stringifyParameter(boolean value) { + return value ? Boolean.TRUE.toString() : Boolean.FALSE.toString(); + } + + public static String stringifyParameter(Date value) { + return value != null ? value.toString() : null; + } + + public static String stringifyParameter(Time value) { + return value != null ? value.toString() : null; + } + + public static String stringifyParameter(Timestamp value) { + return value != null ? value.toString() : null; + } + + public static String stringifyParameter(URL value) { + return value != null ? value.toString() : null; + } + + public static String stringifyParameter(RowId value) { + return value != null ? value.toString() : null; + } +} 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 f2c25b172de7..7d6b1eab0920 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 @@ -51,8 +51,9 @@ public OpenTelemetryCallableStatement( OpenTelemetryConnection connection, DbInfo dbInfo, String query, - Instrumenter instrumenter) { - super(delegate, connection, dbInfo, query, instrumenter); + Instrumenter instrumenter, + boolean captureQueryParameters) { + super(delegate, connection, dbInfo, query, instrumenter, captureQueryParameters); } @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 8fe5ca26ab1d..0ec11c7c2331 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 @@ -52,12 +52,17 @@ public class OpenTelemetryConnection implements Connection { protected final Connection delegate; private final DbInfo dbInfo; protected final Instrumenter statementInstrumenter; + private final boolean captureQueryParameters; protected OpenTelemetryConnection( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + boolean captureQueryParameters) { this.delegate = delegate; this.dbInfo = dbInfo; this.statementInstrumenter = statementInstrumenter; + this.captureQueryParameters = captureQueryParameters; } // visible for testing @@ -71,11 +76,16 @@ static boolean hasJdbc43() { } public static Connection create( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + boolean captureQueryParameters) { if (hasJdbc43) { - return new OpenTelemetryConnectionJdbc43(delegate, dbInfo, statementInstrumenter); + return new OpenTelemetryConnectionJdbc43( + delegate, dbInfo, statementInstrumenter, captureQueryParameters); } - return new OpenTelemetryConnection(delegate, dbInfo, statementInstrumenter); + return new OpenTelemetryConnection( + delegate, dbInfo, statementInstrumenter, captureQueryParameters); } @Override @@ -103,7 +113,7 @@ public Statement createStatement( public PreparedStatement prepareStatement(String sql) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override @@ -112,7 +122,7 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override @@ -122,35 +132,35 @@ public PreparedStatement prepareStatement( PreparedStatement statement = delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, autoGeneratedKeys); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, columnIndexes); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { PreparedStatement statement = delegate.prepareStatement(sql, columnNames); return new OpenTelemetryPreparedStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override public CallableStatement prepareCall(String sql) throws SQLException { CallableStatement statement = delegate.prepareCall(sql); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override @@ -158,7 +168,7 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe throws SQLException { CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override @@ -168,7 +178,7 @@ public CallableStatement prepareCall( CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); return new OpenTelemetryCallableStatement<>( - statement, this, dbInfo, sql, statementInstrumenter); + statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters); } @Override @@ -393,8 +403,11 @@ public DbInfo getDbInfo() { // JDBC 4.3 static class OpenTelemetryConnectionJdbc43 extends OpenTelemetryConnection { OpenTelemetryConnectionJdbc43( - Connection delegate, DbInfo dbInfo, Instrumenter statementInstrumenter) { - super(delegate, dbInfo, statementInstrumenter); + Connection delegate, + DbInfo dbInfo, + Instrumenter statementInstrumenter, + boolean captureQueryParameters) { + super(delegate, dbInfo, statementInstrumenter, captureQueryParameters); } @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 eaabc1685cbc..64142b24c215 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 @@ -20,6 +20,8 @@ package io.opentelemetry.instrumentation.jdbc.internal; +import static io.opentelemetry.instrumentation.jdbc.internal.JdbcPreparedStatementStringifier.stringifyParameter; + import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import java.io.InputStream; @@ -43,18 +45,31 @@ import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; @SuppressWarnings("OverloadMethodsDeclarationOrder") class OpenTelemetryPreparedStatement extends OpenTelemetryStatement implements PreparedStatement { + private final boolean captureQueryParameters; + private final Map parameters; public OpenTelemetryPreparedStatement( S delegate, OpenTelemetryConnection connection, DbInfo dbInfo, String query, - Instrumenter instrumenter) { + Instrumenter instrumenter, + boolean captureQueryParameters) { super(delegate, connection, dbInfo, query, instrumenter); + this.captureQueryParameters = captureQueryParameters; + this.parameters = new HashMap<>(); + } + + private void putParameter(int index, String value) { + if (this.captureQueryParameters) { + parameters.put(Integer.toString(index - 1), value); + } } @Override @@ -87,6 +102,7 @@ public void setNull(int parameterIndex, int sqlType, String typeName) throws SQL @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { delegate.setBoolean(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override @@ -97,36 +113,43 @@ public void setByte(int parameterIndex, byte x) throws SQLException { @Override public void setShort(int parameterIndex, short x) throws SQLException { delegate.setShort(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setInt(int parameterIndex, int x) throws SQLException { delegate.setInt(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setLong(int parameterIndex, long x) throws SQLException { delegate.setLong(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { delegate.setFloat(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { delegate.setDouble(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { delegate.setBigDecimal(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setString(int parameterIndex, String x) throws SQLException { delegate.setString(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override @@ -138,35 +161,41 @@ public void setBytes(int parameterIndex, byte[] x) throws SQLException { @Override public void setDate(int parameterIndex, Date x) throws SQLException { delegate.setDate(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @SuppressWarnings("UngroupedOverloads") @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { delegate.setDate(parameterIndex, x, cal); + putParameter(parameterIndex, stringifyParameter(x)); } @SuppressWarnings("UngroupedOverloads") @Override public void setTime(int parameterIndex, Time x) throws SQLException { delegate.setTime(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { delegate.setTime(parameterIndex, x, cal); + putParameter(parameterIndex, stringifyParameter(x)); } @SuppressWarnings("UngroupedOverloads") @Override public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { delegate.setTimestamp(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @SuppressWarnings("UngroupedOverloads") @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { delegate.setTimestamp(parameterIndex, x, cal); + putParameter(parameterIndex, stringifyParameter(x)); } @SuppressWarnings("UngroupedOverloads") @@ -307,6 +336,7 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public void setURL(int parameterIndex, URL x) throws SQLException { delegate.setURL(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override @@ -317,11 +347,13 @@ public ParameterMetaData getParameterMetaData() throws SQLException { @Override public void setRowId(int parameterIndex, RowId x) throws SQLException { delegate.setRowId(parameterIndex, x); + putParameter(parameterIndex, stringifyParameter(x)); } @Override public void setNString(int parameterIndex, String value) throws SQLException { delegate.setNString(parameterIndex, value); + putParameter(parameterIndex, stringifyParameter(value)); } @SuppressWarnings("UngroupedOverloads") @@ -361,6 +393,7 @@ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException @Override public void clearParameters() throws SQLException { delegate.clearParameters(); + parameters.clear(); } @Override @@ -368,8 +401,15 @@ public int[] executeBatch() throws SQLException { return wrapBatchCall(delegate::executeBatch); } + @Override + protected T wrapCall(String sql, ThrowingSupplier callable) + throws E { + DbRequest request = DbRequest.create(dbInfo, sql, null, parameters); + return wrapCall(request, callable); + } + private T wrapBatchCall(ThrowingSupplier callable) throws E { - DbRequest request = DbRequest.create(dbInfo, query, batchSize); + DbRequest request = DbRequest.create(dbInfo, query, batchSize, parameters); return wrapCall(request, callable); } 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 78e4c782eb46..05a8af3b2ef2 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 @@ -385,7 +385,7 @@ protected T wrapCall(DbRequest request, ThrowingSupplie } private T wrapBatchCall(ThrowingSupplier callable) throws E { - DbRequest request = DbRequest.create(dbInfo, batchCommands, batchSize); + DbRequest request = DbRequest.create(dbInfo, batchCommands, batchSize, null); return wrapCall(request, callable); } } diff --git a/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java b/instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnectionTest.java index adbadfbd05d7..a045b8b0588a 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 @@ -15,6 +15,7 @@ import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_CONNECTION_STRING; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_QUERY_PARAMETER; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SQL_TABLE; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; @@ -29,10 +30,22 @@ import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URI; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +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.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -47,7 +60,7 @@ void testVerifyCreateStatement() throws SQLException { createStatementInstrumenter(testing.getOpenTelemetry()); DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, false); String query = "SELECT * FROM users"; Statement statement = connection.createStatement(); @@ -69,7 +82,7 @@ void testVerifyCreateStatementReturnsOtelWrapper() throws Exception { OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); Instrumenter instrumenter = createStatementInstrumenter(ot); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter, false); assertThat(connection.createStatement()).isInstanceOf(OpenTelemetryStatement.class); assertThat(connection.createStatement(0, 0)).isInstanceOf(OpenTelemetryStatement.class); @@ -86,7 +99,7 @@ void testVerifyPrepareStatement() throws SQLException { createStatementInstrumenter(testing.getOpenTelemetry()); DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, false); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareStatement(query); @@ -111,7 +124,7 @@ void testVerifyPrepareStatementUpdate() throws SQLException { createStatementInstrumenter(testing.getOpenTelemetry()); DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, false); String query = "UPDATE users SET name = name"; PreparedStatement statement = connection.prepareStatement(query); @@ -134,7 +147,7 @@ void testVerifyPrepareStatementQuery() throws SQLException { createStatementInstrumenter(testing.getOpenTelemetry()); DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, false); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareStatement(query); @@ -158,7 +171,7 @@ void testVerifyPrepareStatementReturnsOtelWrapper() throws Exception { OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); Instrumenter instrumenter = createStatementInstrumenter(ot); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter, false); String query = "SELECT * FROM users"; assertThat(connection.prepareStatement(query)) @@ -186,7 +199,7 @@ void testVerifyPrepareCall() throws SQLException { createStatementInstrumenter(testing.getOpenTelemetry()); DbInfo dbInfo = getDbInfo(); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter); + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, false); String query = "SELECT * FROM users"; PreparedStatement statement = connection.prepareCall(query); @@ -208,7 +221,7 @@ void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { OpenTelemetry ot = OpenTelemetry.propagating(ContextPropagators.noop()); Instrumenter instrumenter = createStatementInstrumenter(ot); OpenTelemetryConnection connection = - new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter); + new OpenTelemetryConnection(new TestConnection(), DbInfo.DEFAULT, instrumenter, false); String query = "SELECT * FROM users"; assertThat(connection.prepareCall(query)).isInstanceOf(OpenTelemetryCallableStatement.class); @@ -224,6 +237,69 @@ void testVerifyPrepareCallReturnsOtelWrapper() throws Exception { connection.close(); } + // https://github.com/open-telemetry/semantic-conventions/pull/2093 + @SuppressWarnings("deprecation") + @Test + void testVerifyPrepareStatementParameters() throws SQLException, MalformedURLException { + Instrumenter instrumenter = + createStatementInstrumenter(testing.getOpenTelemetry(), true); + DbInfo dbInfo = getDbInfo(); + OpenTelemetryConnection connection = + new OpenTelemetryConnection(new TestConnection(), dbInfo, instrumenter, true); + 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); + // doesn't need to match the number of placeholders in this context + statement.setBoolean(1, true); + statement.setShort(2, (short) 1); + statement.setInt(3, 2); + statement.setLong(4, 3); + statement.setFloat(5, 4); + statement.setDouble(6, 5.5); + statement.setBigDecimal(7, BigDecimal.valueOf(6)); + statement.setString(8, "S"); + statement.setDate(9, Date.valueOf("2000-01-01")); + statement.setDate(10, Date.valueOf("2000-01-02"), Calendar.getInstance()); + statement.setTime(11, Time.valueOf("00:00:00")); + statement.setTime(12, Time.valueOf("00:00:01"), Calendar.getInstance()); + statement.setTimestamp(13, Timestamp.valueOf("2000-01-01 00:00:00")); + statement.setTimestamp(14, Timestamp.valueOf("2000-01-01 00:00:01"), Calendar.getInstance()); + statement.setURL(15, URI.create("http://localhost:8080").toURL()); + statement.setNString(16, "S"); + + testing.runWithSpan( + "parent", + () -> { + ResultSet resultSet = statement.executeQuery(); + assertThat(resultSet).isInstanceOf(OpenTelemetryResultSet.class); + assertThat(resultSet.getStatement()).isEqualTo(statement); + }); + + jdbcTraceAssertion( + dbInfo, + sanitized, + "SELECT", + equalTo(DB_QUERY_PARAMETER.getAttributeKey("0"), "true"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("1"), "1"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("2"), "2"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("3"), "3"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("4"), "4.0"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("5"), "5.5"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("6"), "6"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("7"), "S"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("8"), "2000-01-01"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("9"), "2000-01-02"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("10"), "00:00:00"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("11"), "00:00:01"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("12"), "2000-01-01 00:00:00.0"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("13"), "2000-01-01 00:00:01.0"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("14"), "http://localhost:8080"), + equalTo(DB_QUERY_PARAMETER.getAttributeKey("15"), "S")); + + statement.close(); + connection.close(); + } + private static DbInfo getDbInfo() { return DbInfo.builder() .system("my_system") @@ -242,7 +318,22 @@ private static void jdbcTraceAssertion(DbInfo dbInfo, String query) { } @SuppressWarnings("deprecation") // old semconv - private static void jdbcTraceAssertion(DbInfo dbInfo, String query, String operation) { + private static void jdbcTraceAssertion( + DbInfo dbInfo, String query, String operation, AttributeAssertion... assertions) { + List baseAttributeAssertions = + Arrays.asList( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(dbInfo.getSystem())), + equalTo(maybeStable(DB_NAME), dbInfo.getName()), + equalTo(DB_USER, emitStableDatabaseSemconv() ? null : dbInfo.getUser()), + equalTo( + DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : dbInfo.getShortUrl()), + equalTo(maybeStable(DB_STATEMENT), query), + equalTo(maybeStable(DB_OPERATION), operation), + equalTo(maybeStable(DB_SQL_TABLE), "users"), + equalTo(SERVER_ADDRESS, dbInfo.getHost()), + equalTo(SERVER_PORT, dbInfo.getPort())); + + List additionAttributeAssertions = Arrays.asList(assertions); testing.waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( @@ -252,18 +343,9 @@ private static void jdbcTraceAssertion(DbInfo dbInfo, String query, String opera .hasKind(SpanKind.CLIENT) .hasParent(trace.getSpan(0)) .hasAttributesSatisfyingExactly( - equalTo( - maybeStable(DB_SYSTEM), - maybeStableDbSystemName(dbInfo.getSystem())), - equalTo(maybeStable(DB_NAME), dbInfo.getName()), - equalTo(DB_USER, emitStableDatabaseSemconv() ? null : dbInfo.getUser()), - equalTo( - DB_CONNECTION_STRING, - emitStableDatabaseSemconv() ? null : dbInfo.getShortUrl()), - equalTo(maybeStable(DB_STATEMENT), query), - equalTo(maybeStable(DB_OPERATION), operation), - equalTo(maybeStable(DB_SQL_TABLE), "users"), - equalTo(SERVER_ADDRESS, dbInfo.getHost()), - equalTo(SERVER_PORT, dbInfo.getPort())))); + Stream.concat( + baseAttributeAssertions.stream(), + additionAttributeAssertions.stream()) + .collect(Collectors.toList())))); } } diff --git a/instrumentation/jdbc/metadata.yaml b/instrumentation/jdbc/metadata.yaml index e52ce68e1aef..a0d7884cb21b 100644 --- a/instrumentation/jdbc/metadata.yaml +++ b/instrumentation/jdbc/metadata.yaml @@ -14,3 +14,12 @@ configurations: - name: otel.instrumentation.common.peer-service-mapping description: Used to specify a mapping from host names or IP addresses to peer services. default: "" + - name: otel.instrumentation.jdbc.capture-query-parameters + description: > + Enables the attribute db.query.parameter.. + WARNING: captured query parameters may contain sensitive information such as passwords, + personally identifiable information or protected health info. + Exposing such info may result in substantial fines and penalties or criminal liability. + Consult your peers, superiors and a legal counsel before enabling this option. + This option will disable otel.instrumentation.jdbc.statement-sanitizer + default: false 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 9721c5580f60..8eb3806d0347 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 @@ -26,12 +26,16 @@ import java.sql.Time; import java.sql.Timestamp; import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; class TestPreparedStatement extends TestStatement implements PreparedStatement { private boolean hasResultSet = true; + Map parameters; TestPreparedStatement(Connection connection) { super(connection); + this.parameters = new HashMap<>(); } @Override @@ -147,7 +151,9 @@ public void setDouble(int parameterIndex, double x) throws SQLException {} public void setFloat(int parameterIndex, float x) throws SQLException {} @Override - public void setInt(int parameterIndex, int x) throws SQLException {} + public void setInt(int parameterIndex, int x) throws SQLException { + parameters.put(Integer.toString(parameterIndex), Integer.toString(x)); + } @Override public void setLong(int parameterIndex, long x) throws SQLException {} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java index f29513d01bc5..da759ffd048d 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java @@ -55,6 +55,10 @@ public Object postProcessAfterInitialization(Object bean, String beanName) { InstrumentationConfigUtil.isStatementSanitizationEnabled( configPropertiesProvider.getObject(), "otel.instrumentation.jdbc.statement-sanitizer.enabled")) + .setCaptureQueryParameters( + configPropertiesProvider + .getObject() + .getBoolean("otel.instrumentation.jdbc.capture-query-parameters", false)) .build() .wrap(dataSource); } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index db72deca72bd..acf51bcfdcad 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -351,6 +351,12 @@ "description": "Enables the DB statement sanitization.", "defaultValue": true }, + { + "name": "otel.instrumentation.jdbc.capture-query-parameters", + "type": "java.lang.Boolean", + "description": "Enables the attribute db.query.parameter.\\.

WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info. Exposing such info may result in substantial fines and penalties or criminal liability. Consult your peers, superiors and a legal counsel before enabling this option.

This option will disable otel.instrumentation.jdbc.statement-sanitizer", + "defaultValue": false + }, { "name": "otel.instrumentation.kafka.enabled", "type": "java.lang.Boolean",