Added JDBC db.query.parameter span attributes (#13719)

Co-authored-by: Lauri Tulmin <ltulmin@splunk.com>
This commit is contained in:
Alix 2025-05-14 16:52:22 +02:00 committed by GitHub
parent 97e526017d
commit 8cde80aae2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1170 additions and 87 deletions

View File

@ -12,7 +12,9 @@ import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
import io.opentelemetry.semconv.AttributeKeyTemplate;
import java.util.Collection;
import java.util.Map;
/**
* Extractor of <a
@ -38,6 +40,8 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
AttributeKey.stringKey("db.collection.name");
private static final AttributeKey<Long> DB_OPERATION_BATCH_SIZE =
AttributeKey.longKey("db.operation.batch.size");
private static final AttributeKeyTemplate<String> DB_QUERY_PARAMETER =
AttributeKeyTemplate.stringKeyTemplate("db.query.parameter");
/** Creates the SQL client attributes extractor with default configuration. */
public static <REQUEST, RESPONSE> AttributesExtractor<REQUEST, RESPONSE> create(
@ -58,14 +62,18 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
private final AttributeKey<String> oldSemconvTableAttribute;
private final boolean statementSanitizationEnabled;
private final boolean captureQueryParameters;
SqlClientAttributesExtractor(
SqlClientAttributesGetter<REQUEST, RESPONSE> getter,
AttributeKey<String> oldSemconvTableAttribute,
boolean statementSanitizationEnabled) {
boolean statementSanitizationEnabled,
boolean captureQueryParameters) {
super(getter);
this.oldSemconvTableAttribute = oldSemconvTableAttribute;
this.statementSanitizationEnabled = statementSanitizationEnabled;
// capturing query parameters disables statement sanitization
this.statementSanitizationEnabled = !captureQueryParameters && statementSanitizationEnabled;
this.captureQueryParameters = captureQueryParameters;
}
@Override
@ -78,6 +86,9 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
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();
@ -95,8 +106,6 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
}
if (SemconvStability.emitStableDatabaseSemconv()) {
Long batchSize = getter.getBatchSize(request);
boolean isBatch = batchSize != null && batchSize > 1;
if (isBatch) {
internalSet(attributes, DB_OPERATION_BATCH_SIZE, batchSize);
}
@ -127,6 +136,20 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
}
}
}
Map<String, String> queryParameters = getter.getQueryParameters(request);
setQueryParameters(attributes, isBatch, queryParameters);
}
private void setQueryParameters(
AttributesBuilder attributes, boolean isBatch, Map<String, String> queryParameters) {
if (captureQueryParameters && !isBatch && queryParameters != null) {
for (Map.Entry<String, String> entry : queryParameters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
internalSet(attributes, DB_QUERY_PARAMETER.getAttributeKey(key), value);
}
}
}
// String.join is not available on android

View File

@ -20,6 +20,7 @@ public final class SqlClientAttributesExtractorBuilder<REQUEST, RESPONSE> {
final SqlClientAttributesGetter<REQUEST, RESPONSE> getter;
AttributeKey<String> oldSemconvTableAttribute = DB_SQL_TABLE;
boolean statementSanitizationEnabled = true;
boolean captureQueryParameters = false;
SqlClientAttributesExtractorBuilder(SqlClientAttributesGetter<REQUEST, RESPONSE> getter) {
this.getter = getter;
@ -48,12 +49,27 @@ public final class SqlClientAttributesExtractorBuilder<REQUEST, RESPONSE> {
return this;
}
/**
* Sets whether the query parameters should be captured as span attributes named {@code
* db.query.parameter.<key>}. Enabling this option disables the statement sanitization. Disabled
* by default.
*
* <p>WARNING: captured query parameters may contain sensitive information such as passwords,
* personally identifiable information or protected health info.
*/
@CanIgnoreReturnValue
public SqlClientAttributesExtractorBuilder<REQUEST, RESPONSE> setCaptureQueryParameters(
boolean captureQueryParameters) {
this.captureQueryParameters = captureQueryParameters;
return this;
}
/**
* Returns a new {@link SqlClientAttributesExtractor} with the settings of this {@link
* SqlClientAttributesExtractorBuilder}.
*/
public AttributesExtractor<REQUEST, RESPONSE> build() {
return new SqlClientAttributesExtractor<>(
getter, oldSemconvTableAttribute, statementSanitizationEnabled);
getter, oldSemconvTableAttribute, statementSanitizationEnabled, captureQueryParameters);
}
}

View File

@ -9,6 +9,8 @@ import static java.util.Collections.emptySet;
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 @@ public interface SqlClientAttributesGetter<REQUEST, RESPONSE>
default Long getBatchSize(REQUEST request) {
return null;
}
// TODO: make this required to implement
default Map<String, String> getQueryParameters(REQUEST request) {
return Collections.emptyMap();
}
}

View File

@ -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;
@ -62,6 +63,14 @@ class SqlClientAttributesExtractorTest {
return read(map, "db.operation.batch.size", Long.class);
}
@SuppressWarnings("unchecked")
@Override
public Map<String, String> getQueryParameters(Map<String, Object> map) {
Map<String, String> parameters =
(Map<String, String>) read(map, "db.query.parameter", Map.class);
return parameters != null ? parameters : Collections.emptyMap();
}
protected String read(Map<String, Object> map, String key) {
return read(map, key, String.class);
}
@ -387,4 +396,74 @@ class SqlClientAttributesExtractorTest {
assertThat(endAttributes.build().isEmpty()).isTrue();
}
@Test
void shouldExtractQueryParameters() {
// given
Map<String, Object> 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
Map<String, String> parameterMap = new HashMap<>();
parameterMap.put("0", "'a'");
parameterMap.put("1", "1");
request.put("db.query.parameter", parameterMap);
Context context = Context.root();
AttributesExtractor<Map<String, Object>, 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<String, Object> request = new HashMap<>();
request.put("db.name", "potatoes");
request.put("db.statements", singleton("INSERT INTO potato VALUES(?)"));
request.put("db.operation.batch.size", 2L);
request.put("db.query.parameter", Collections.singletonMap("0", "1"));
Context context = Context.root();
AttributesExtractor<Map<String, Object>, Void> underTest =
SqlClientAttributesExtractor.builder(new TestMultiAttributesGetter())
.setCaptureQueryParameters(true)
.build();
// when
AttributesBuilder startAttributes = Attributes.builder();
underTest.onStart(startAttributes, context, request);
AttributesBuilder endAttributes = Attributes.builder();
underTest.onEnd(endAttributes, context, request, null, null);
// then
assertThat(startAttributes.build()).doesNotContainKey(DB_QUERY_PARAMETER.getAttributeKey("0"));
assertThat(endAttributes.build().isEmpty()).isTrue();
}
}

View File

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

View File

@ -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,10 +87,18 @@ tasks {
jvmArgs("-Dotel.semconv-stability.opt-in=database")
}
val testCaptureParameters by registering(Test::class) {
filter {
includeTestsMatching("PreparedStatementParametersTest")
}
jvmArgs("-Dotel.instrumentation.jdbc.capture-query-parameters=true")
}
check {
dependsOn(testSlick)
dependsOn(testStableSemconv)
dependsOn(testSlickStableSemconv)
dependsOn(testCaptureParameters)
}
}

View File

@ -25,6 +25,7 @@ public final class JdbcSingletons {
private static final Instrumenter<DbRequest, Void> TRANSACTION_INSTRUMENTER;
public static final Instrumenter<DataSource, DbInfo> DATASOURCE_INSTRUMENTER =
createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true);
public static final boolean CAPTURE_QUERY_PARAMETERS;
static {
JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter();
@ -32,6 +33,10 @@ public final class JdbcSingletons {
PeerServiceAttributesExtractor.create(
netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver());
CAPTURE_QUERY_PARAMETERS =
AgentInstrumentationConfig.get()
.getBoolean("otel.instrumentation.jdbc.capture-query-parameters", false);
STATEMENT_INSTRUMENTER =
JdbcInstrumenterFactory.createStatementInstrumenter(
GlobalOpenTelemetry.get(),
@ -40,7 +45,8 @@ public final class JdbcSingletons {
AgentInstrumentationConfig.get()
.getBoolean(
"otel.instrumentation.jdbc.statement-sanitizer.enabled",
AgentCommonConfig.get().isStatementSanitizationEnabled()));
AgentCommonConfig.get().isStatementSanitizationEnabled()),
CAPTURE_QUERY_PARAMETERS);
TRANSACTION_INSTRUMENTER =
JdbcInstrumenterFactory.createTransactionInstrumenter(

View File

@ -8,12 +8,14 @@ package io.opentelemetry.javaagent.instrumentation.jdbc;
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.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 +26,15 @@ import io.opentelemetry.instrumentation.jdbc.internal.JdbcData;
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 +62,36 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
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");
transformer.applyAdviceToMethod(
named("clearParameters").and(takesNoArguments()).and(isPublic()),
PreparedStatementInstrumentation.class.getName() + "$ClearParametersAdvice");
}
@SuppressWarnings("unused")
@ -84,7 +123,8 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
}
Context parentContext = currentContext();
request = DbRequest.create(statement);
Map<String, String> parameters = JdbcData.getParameters(statement);
request = DbRequest.create(statement, parameters);
if (request == null || !statementInstrumenter().shouldStart(parentContext, request)) {
return;
@ -120,4 +160,68 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
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
// Short, Int, Long, Float, Double, BigDecimal
|| value instanceof Number
|| value instanceof String
|| value instanceof Date
|| value instanceof Time
|| value instanceof Timestamp
|| value instanceof URL
|| value instanceof RowId) {
str = value.toString();
}
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 || value instanceof Time || value instanceof Timestamp) {
str = value.toString();
}
if (str != null) {
JdbcData.addParameter(statement, Integer.toString(index - 1), str);
}
}
}
@SuppressWarnings("unused")
public static class ClearParametersAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void clearBatch(@Advice.This PreparedStatement statement) {
JdbcData.clearParameters(statement);
}
}
}

View File

@ -25,6 +25,7 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
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;
@ -157,12 +158,13 @@ public class StatementInstrumentation implements TypeInstrumentation {
Context parentContext = currentContext();
if (statement instanceof PreparedStatement) {
Long batchSize = JdbcData.getPreparedStatementBatchSize((PreparedStatement) statement);
String sql = JdbcData.preparedStatement.get((PreparedStatement) statement);
if (sql == null) {
return;
}
request = DbRequest.create(statement, sql, batchSize);
Long batchSize = JdbcData.getPreparedStatementBatchSize((PreparedStatement) statement);
Map<String, String> parameters = JdbcData.getParameters((PreparedStatement) statement);
request = DbRequest.create(statement, sql, batchSize, parameters);
} else {
JdbcData.StatementBatchInfo batchInfo = JdbcData.getStatementBatchInfo(statement);
if (batchInfo == null) {

View File

@ -0,0 +1,556 @@
/*
* 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.Assumptions;
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<String, String> jdbcUrls =
ImmutableMap.of(
"h2", "jdbc:h2:mem:" + dbName,
"derby", "jdbc:derby:memory:" + dbName,
"hsqldb", "jdbc:hsqldb:mem:" + dbName);
private static final Map<String, String> 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");
}
static Stream<Arguments> 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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setBoolean(1, true),
"true");
}
@ParameterizedTest
@MethodSource("preparedStatementStream")
void testShortPreparedStatementParameter(
String system,
Connection connection,
String username,
String query,
String sanitizedQuery,
String spanName,
String url,
String table)
throws SQLException {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setShort(1, (short) 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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setInt(1, 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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setLong(1, 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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setFloat(1, 0.1f),
"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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setDouble(1, 0.1),
"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 {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setBigDecimal(1, BigDecimal.ZERO),
"0");
}
@ParameterizedTest
@MethodSource("preparedStatementStream")
void testStringPreparedStatementParameter(
String system,
Connection connection,
String username,
String query,
String sanitizedQuery,
String spanName,
String url,
String table)
throws SQLException {
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setString(1, "S"),
"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 updatedQuery = query.replace("USER_NAME=?", "CURDATE()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "CURDATE()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement -> statement.setDate(1, Date.valueOf("2000-01-01")),
"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 updatedQuery = query.replace("USER_NAME=?", "CURDATE()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "CURDATE()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement -> statement.setDate(1, Date.valueOf("2000-01-01"), Calendar.getInstance()),
"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 updatedQuery = query.replace("USER_NAME=?", "CURTIME()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "CURTIME()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement -> statement.setTime(1, Time.valueOf("00:00:00")),
"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 updatedQuery = query.replace("USER_NAME=?", "CURTIME()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "CURTIME()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement -> statement.setTime(1, Time.valueOf("00:00:00"), Calendar.getInstance()),
"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 updatedQuery = query.replace("USER_NAME=?", "NOW()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "NOW()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement -> statement.setTimestamp(1, Timestamp.valueOf("2000-01-01 00:00:00")),
"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 updatedQuery = query.replace("USER_NAME=?", "NOW()=?");
String updatedQuerySanitized = sanitizedQuery.replace("USER_NAME=?", "NOW()=?");
test(
system,
connection,
username,
updatedQuery,
updatedQuerySanitized,
spanName,
url,
table,
statement ->
statement.setTimestamp(
1, Timestamp.valueOf("2000-01-01 00:00:00"), Calendar.getInstance()),
"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 {
Assumptions.assumeFalse(system.equalsIgnoreCase("derby"));
test(
system,
connection,
username,
query,
sanitizedQuery,
spanName,
url,
table,
statement -> statement.setNString(1, "S"),
"S");
}
private static void test(
String system,
Connection connection,
String username,
String query,
String sanitizedQuery,
String spanName,
String url,
String table,
ThrowingConsumer<PreparedStatement, SQLException> setParameter,
String expectedParameterValue)
throws SQLException {
PreparedStatement statement = connection.prepareStatement(query);
cleanup.deferCleanup(statement);
ResultSet resultSet =
testing.runWithSpan(
"parent",
() -> {
setParameter.accept(statement);
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"), expectedParameterValue))));
}
public interface ThrowingConsumer<T, E extends Throwable> {
void accept(T t) throws E;
}
}

View File

@ -244,10 +244,12 @@ public final class OpenTelemetryDriver implements Driver {
Instrumenter<DbRequest, Void> statementInstrumenter =
JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry);
boolean captureQueryParameters = JdbcInstrumenterFactory.captureQueryParameters();
Instrumenter<DbRequest, Void> transactionInstrumenter =
JdbcInstrumenterFactory.createTransactionInstrumenter(openTelemetry);
return OpenTelemetryConnection.create(
connection, dbInfo, statementInstrumenter, transactionInstrumenter);
connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
@Override

View File

@ -27,14 +27,17 @@ public final class JdbcTelemetry {
private final Instrumenter<DataSource, DbInfo> dataSourceInstrumenter;
private final Instrumenter<DbRequest, Void> statementInstrumenter;
private final Instrumenter<DbRequest, Void> transactionInstrumenter;
private final boolean captureQueryParameters;
JdbcTelemetry(
Instrumenter<DataSource, DbInfo> dataSourceInstrumenter,
Instrumenter<DbRequest, Void> statementInstrumenter,
Instrumenter<DbRequest, Void> transactionInstrumenter) {
Instrumenter<DbRequest, Void> transactionInstrumenter,
boolean captureQueryParameters) {
this.dataSourceInstrumenter = dataSourceInstrumenter;
this.statementInstrumenter = statementInstrumenter;
this.transactionInstrumenter = transactionInstrumenter;
this.captureQueryParameters = captureQueryParameters;
}
public DataSource wrap(DataSource dataSource) {
@ -42,6 +45,7 @@ public final class JdbcTelemetry {
dataSource,
this.dataSourceInstrumenter,
this.statementInstrumenter,
this.transactionInstrumenter);
this.transactionInstrumenter,
this.captureQueryParameters);
}
}

View File

@ -7,7 +7,11 @@ package io.opentelemetry.instrumentation.jdbc.datasource;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.jdbc.internal.DbRequest;
import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory;
import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo;
import javax.sql.DataSource;
/** A builder of {@link JdbcTelemetry}. */
public final class JdbcTelemetryBuilder {
@ -17,6 +21,7 @@ public final class JdbcTelemetryBuilder {
private boolean statementInstrumenterEnabled = true;
private boolean statementSanitizationEnabled = true;
private boolean transactionInstrumenterEnabled = false;
private boolean captureQueryParameters = false;
JdbcTelemetryBuilder(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
@ -50,14 +55,38 @@ public final class JdbcTelemetryBuilder {
return this;
}
/**
* Configures whether parameters are captured for JDBC Statements. Enabling this option disables
* the statement sanitization. Disabled by default.
*
* <p>WARNING: captured query parameters may contain sensitive information such as passwords,
* personally identifiable information or protected health info.
*/
@CanIgnoreReturnValue
public JdbcTelemetryBuilder setCaptureQueryParameters(boolean enabled) {
this.captureQueryParameters = enabled;
return this;
}
/** Returns a new {@link JdbcTelemetry} with the settings of this {@link JdbcTelemetryBuilder}. */
public JdbcTelemetry build() {
return new JdbcTelemetry(
Instrumenter<DataSource, DbInfo> dataSourceInstrumenter =
JdbcInstrumenterFactory.createDataSourceInstrumenter(
openTelemetry, dataSourceInstrumenterEnabled),
openTelemetry, dataSourceInstrumenterEnabled);
Instrumenter<DbRequest, Void> statementInstrumenter =
JdbcInstrumenterFactory.createStatementInstrumenter(
openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled),
openTelemetry,
statementInstrumenterEnabled,
statementSanitizationEnabled,
captureQueryParameters);
Instrumenter<DbRequest, Void> transactionInstrumenter =
JdbcInstrumenterFactory.createTransactionInstrumenter(
openTelemetry, transactionInstrumenterEnabled));
openTelemetry, transactionInstrumenterEnabled);
return new JdbcTelemetry(
dataSourceInstrumenter,
statementInstrumenter,
transactionInstrumenter,
captureQueryParameters);
}
}

View File

@ -50,6 +50,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable {
private final Instrumenter<DbRequest, Void> statementInstrumenter;
private final Instrumenter<DbRequest, Void> transactionInstrumenter;
private volatile DbInfo cachedDbInfo;
private final boolean captureQueryParameters;
/**
* Create a OpenTelemetry DataSource wrapping another DataSource.
@ -74,6 +75,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable {
this.dataSourceInstrumenter = createDataSourceInstrumenter(openTelemetry, true);
this.statementInstrumenter = createStatementInstrumenter(openTelemetry);
this.transactionInstrumenter = createTransactionInstrumenter(openTelemetry, false);
this.captureQueryParameters = false;
}
/**
@ -87,11 +89,13 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable {
DataSource delegate,
Instrumenter<DataSource, DbInfo> dataSourceInstrumenter,
Instrumenter<DbRequest, Void> statementInstrumenter,
Instrumenter<DbRequest, Void> transactionInstrumenter) {
Instrumenter<DbRequest, Void> transactionInstrumenter,
boolean captureQueryParameters) {
this.delegate = delegate;
this.dataSourceInstrumenter = dataSourceInstrumenter;
this.statementInstrumenter = statementInstrumenter;
this.transactionInstrumenter = transactionInstrumenter;
this.captureQueryParameters = captureQueryParameters;
}
@Override
@ -99,7 +103,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable {
Connection connection = wrapCall(delegate::getConnection);
DbInfo dbInfo = getDbInfo(connection);
return OpenTelemetryConnection.create(
connection, dbInfo, statementInstrumenter, transactionInstrumenter);
connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
@Override
@ -107,7 +111,7 @@ public class OpenTelemetryDataSource implements DataSource, AutoCloseable {
Connection connection = wrapCall(() -> delegate.getConnection(username, password));
DbInfo dbInfo = getDbInfo(connection);
return OpenTelemetryConnection.create(
connection, dbInfo, statementInstrumenter, transactionInstrumenter);
connection, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
@Override

View File

@ -7,6 +7,7 @@ package io.opentelemetry.instrumentation.jdbc.internal;
import static io.opentelemetry.instrumentation.jdbc.internal.JdbcUtils.connectionFromStatement;
import static io.opentelemetry.instrumentation.jdbc.internal.JdbcUtils.extractDbInfo;
import static java.util.Collections.emptyMap;
import com.google.auto.value.AutoValue;
import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo;
@ -15,6 +16,7 @@ import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import javax.annotation.Nullable;
/**
@ -25,23 +27,38 @@ import javax.annotation.Nullable;
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<String, String> 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, emptyMap());
}
@Nullable
public static DbRequest create(Statement statement, String dbStatementString, Long batchSize) {
private static DbRequest create(
Statement statement,
String dbStatementString,
Map<String, String> preparedStatementParameters) {
return create(statement, dbStatementString, null, preparedStatementParameters);
}
@Nullable
public static DbRequest create(
Statement statement,
String dbStatementString,
Long batchSize,
Map<String, String> 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,24 +68,38 @@ public abstract class DbRequest {
return null;
}
return create(extractDbInfo(connection), queryTexts, batchSize);
return create(extractDbInfo(connection), queryTexts, batchSize, emptyMap());
}
public static DbRequest create(DbInfo dbInfo, String queryText) {
return create(dbInfo, queryText, null);
}
public static DbRequest create(DbInfo dbInfo, String queryText, Long batchSize) {
return create(dbInfo, Collections.singletonList(queryText), batchSize);
}
public static DbRequest create(DbInfo dbInfo, Collection<String> queryTexts, Long batchSize) {
return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize, null);
return create(dbInfo, queryText, null, emptyMap());
}
public static DbRequest create(
DbInfo dbInfo, Collection<String> queryTexts, Long batchSize, String operation) {
return new AutoValue_DbRequest(dbInfo, queryTexts, batchSize, operation);
DbInfo dbInfo,
String queryText,
Long batchSize,
Map<String, String> preparedStatementParameters) {
return create(
dbInfo, Collections.singletonList(queryText), batchSize, preparedStatementParameters);
}
public static DbRequest create(
DbInfo dbInfo,
Collection<String> queryTexts,
Long batchSize,
Map<String, String> preparedStatementParameters) {
return create(dbInfo, queryTexts, batchSize, null, preparedStatementParameters);
}
private static DbRequest create(
DbInfo dbInfo,
Collection<String> queryTexts,
Long batchSize,
String operation,
Map<String, String> preparedStatementParameters) {
return new AutoValue_DbRequest(
dbInfo, queryTexts, batchSize, operation, preparedStatementParameters);
}
@Nullable
@ -82,7 +113,7 @@ public abstract class DbRequest {
}
public static DbRequest createTransaction(DbInfo dbInfo, String operation) {
return create(dbInfo, Collections.emptyList(), null, operation);
return create(dbInfo, Collections.emptyList(), null, operation, emptyMap());
}
public abstract DbInfo getDbInfo();
@ -95,4 +126,6 @@ public abstract class DbRequest {
// used for transaction instrumentation
@Nullable
public abstract String getOperation();
public abstract Map<String, String> getPreparedStatementParameters();
}

View File

@ -9,6 +9,7 @@ import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttrib
import io.opentelemetry.instrumentation.jdbc.internal.dbinfo.DbInfo;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Map;
import javax.annotation.Nullable;
final class JdbcAttributesGetter implements SqlClientAttributesGetter<DbRequest, Void> {
@ -58,4 +59,9 @@ final class JdbcAttributesGetter implements SqlClientAttributesGetter<DbRequest,
}
return null;
}
@Override
public Map<String, String> getQueryParameters(DbRequest request) {
return request.getPreparedStatementParameters();
}
}

View File

@ -13,6 +13,8 @@ import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
@ -35,6 +37,8 @@ public final class JdbcData {
private static final VirtualField<PreparedStatement, PreparedStatementBatchInfo>
preparedStatementBatch =
VirtualField.find(PreparedStatement.class, PreparedStatementBatchInfo.class);
private static final VirtualField<PreparedStatement, Map<String, String>> parameters =
VirtualField.find(PreparedStatement.class, Map.class);
private JdbcData() {}
@ -103,9 +107,32 @@ public final class JdbcData {
PreparedStatement prepared = (PreparedStatement) statement;
preparedStatement.set(prepared, null);
preparedStatementBatch.set(prepared, null);
parameters.set(prepared, null);
}
}
public static Map<String, String> getParameters(PreparedStatement statement) {
Map<String, String> parametersMap = parameters.get(statement);
return parametersMap != null ? parametersMap : Collections.emptyMap();
}
public static void addParameter(PreparedStatement statement, String key, String value) {
if (value == null) {
return;
}
Map<String, String> parametersMap = parameters.get(statement);
if (parametersMap == null) {
parametersMap = new HashMap<>();
parameters.set(statement, parametersMap);
}
parametersMap.put(key, value);
}
public static void clearParameters(PreparedStatement statement) {
parameters.set(statement, null);
}
/**
* This class is internal and is hence not for public use. Its APIs are unstable and can change at
* any time.

View File

@ -32,26 +32,42 @@ public final class JdbcInstrumenterFactory {
private static final JdbcNetworkAttributesGetter netAttributesGetter =
new JdbcNetworkAttributesGetter();
public static Instrumenter<DbRequest, Void> createStatementInstrumenter(
OpenTelemetry openTelemetry) {
return createStatementInstrumenter(
openTelemetry,
true,
ConfigPropertiesUtil.getBoolean(
"otel.instrumentation.common.db-statement-sanitizer.enabled", true));
public static boolean captureQueryParameters() {
return ConfigPropertiesUtil.getBoolean(
"otel.instrumentation.jdbc.capture-query-parameters", false);
}
public static Instrumenter<DbRequest, Void> createStatementInstrumenter(
OpenTelemetry openTelemetry, boolean enabled, boolean statementSanitizationEnabled) {
OpenTelemetry openTelemetry) {
return createStatementInstrumenter(openTelemetry, captureQueryParameters());
}
static Instrumenter<DbRequest, Void> createStatementInstrumenter(
OpenTelemetry openTelemetry, boolean captureQueryParameters) {
return createStatementInstrumenter(
openTelemetry, emptyList(), enabled, statementSanitizationEnabled);
openTelemetry,
emptyList(),
true,
ConfigPropertiesUtil.getBoolean(
"otel.instrumentation.common.db-statement-sanitizer.enabled", true),
captureQueryParameters);
}
public static Instrumenter<DbRequest, Void> createStatementInstrumenter(
OpenTelemetry openTelemetry,
boolean enabled,
boolean statementSanitizationEnabled,
boolean captureQueryParameters) {
return createStatementInstrumenter(
openTelemetry, emptyList(), enabled, statementSanitizationEnabled, captureQueryParameters);
}
public static Instrumenter<DbRequest, Void> createStatementInstrumenter(
OpenTelemetry openTelemetry,
List<AttributesExtractor<DbRequest, Void>> extractors,
boolean enabled,
boolean statementSanitizationEnabled) {
boolean statementSanitizationEnabled,
boolean captureQueryParameters) {
return Instrumenter.<DbRequest, Void>builder(
openTelemetry,
INSTRUMENTATION_NAME,
@ -59,6 +75,7 @@ public final class JdbcInstrumenterFactory {
.addAttributesExtractor(
SqlClientAttributesExtractor.builder(dbAttributesGetter)
.setStatementSanitizationEnabled(statementSanitizationEnabled)
.setCaptureQueryParameters(captureQueryParameters)
.build())
.addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter))
.addAttributesExtractors(extractors)

View File

@ -51,8 +51,9 @@ class OpenTelemetryCallableStatement<S extends CallableStatement>
OpenTelemetryConnection connection,
DbInfo dbInfo,
String query,
Instrumenter<DbRequest, Void> instrumenter) {
super(delegate, connection, dbInfo, query, instrumenter);
Instrumenter<DbRequest, Void> instrumenter,
boolean captureQueryParameters) {
super(delegate, connection, dbInfo, query, instrumenter, captureQueryParameters);
}
@Override

View File

@ -55,16 +55,19 @@ public class OpenTelemetryConnection implements Connection {
private final DbInfo dbInfo;
protected final Instrumenter<DbRequest, Void> statementInstrumenter;
protected final Instrumenter<DbRequest, Void> transactionInstrumenter;
private final boolean captureQueryParameters;
protected OpenTelemetryConnection(
Connection delegate,
DbInfo dbInfo,
Instrumenter<DbRequest, Void> statementInstrumenter,
Instrumenter<DbRequest, Void> transactionInstrumenter) {
Instrumenter<DbRequest, Void> transactionInstrumenter,
boolean captureQueryParameters) {
this.delegate = delegate;
this.dbInfo = dbInfo;
this.statementInstrumenter = statementInstrumenter;
this.transactionInstrumenter = transactionInstrumenter;
this.captureQueryParameters = captureQueryParameters;
}
// visible for testing
@ -81,13 +84,14 @@ public class OpenTelemetryConnection implements Connection {
Connection delegate,
DbInfo dbInfo,
Instrumenter<DbRequest, Void> statementInstrumenter,
Instrumenter<DbRequest, Void> transactionInstrumenter) {
Instrumenter<DbRequest, Void> transactionInstrumenter,
boolean captureQueryParameters) {
if (hasJdbc43) {
return new OpenTelemetryConnectionJdbc43(
delegate, dbInfo, statementInstrumenter, transactionInstrumenter);
delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
return new OpenTelemetryConnection(
delegate, dbInfo, statementInstrumenter, transactionInstrumenter);
delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
@Override
@ -115,7 +119,7 @@ public class OpenTelemetryConnection implements Connection {
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
@ -124,7 +128,7 @@ public class OpenTelemetryConnection implements Connection {
PreparedStatement statement =
delegate.prepareStatement(sql, resultSetType, resultSetConcurrency);
return new OpenTelemetryPreparedStatement<>(
statement, this, dbInfo, sql, statementInstrumenter);
statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters);
}
@Override
@ -134,35 +138,35 @@ public class OpenTelemetryConnection implements Connection {
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
@ -170,7 +174,7 @@ public class OpenTelemetryConnection implements Connection {
throws SQLException {
CallableStatement statement = delegate.prepareCall(sql, resultSetType, resultSetConcurrency);
return new OpenTelemetryCallableStatement<>(
statement, this, dbInfo, sql, statementInstrumenter);
statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters);
}
@Override
@ -180,7 +184,7 @@ public class OpenTelemetryConnection implements Connection {
CallableStatement statement =
delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
return new OpenTelemetryCallableStatement<>(
statement, this, dbInfo, sql, statementInstrumenter);
statement, this, dbInfo, sql, statementInstrumenter, captureQueryParameters);
}
@Override
@ -408,8 +412,10 @@ public class OpenTelemetryConnection implements Connection {
Connection delegate,
DbInfo dbInfo,
Instrumenter<DbRequest, Void> statementInstrumenter,
Instrumenter<DbRequest, Void> transactionInstrumenter) {
super(delegate, dbInfo, statementInstrumenter, transactionInstrumenter);
Instrumenter<DbRequest, Void> transactionInstrumenter,
boolean captureQueryParameters) {
super(
delegate, dbInfo, statementInstrumenter, transactionInstrumenter, captureQueryParameters);
}
@SuppressWarnings("Since15")

View File

@ -43,18 +43,31 @@ import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("OverloadMethodsDeclarationOrder")
class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTelemetryStatement<S>
implements PreparedStatement {
private final boolean captureQueryParameters;
private final Map<String, String> parameters;
public OpenTelemetryPreparedStatement(
S delegate,
OpenTelemetryConnection connection,
DbInfo dbInfo,
String query,
Instrumenter<DbRequest, Void> instrumenter) {
Instrumenter<DbRequest, Void> instrumenter,
boolean captureQueryParameters) {
super(delegate, connection, dbInfo, query, instrumenter);
this.captureQueryParameters = captureQueryParameters;
this.parameters = new HashMap<>();
}
private void putParameter(int index, Object value) {
if (this.captureQueryParameters && value != null) {
parameters.put(Integer.toString(index - 1), value.toString());
}
}
@Override
@ -87,46 +100,55 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
@Override
public void setBoolean(int parameterIndex, boolean x) throws SQLException {
delegate.setBoolean(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setByte(int parameterIndex, byte x) throws SQLException {
delegate.setByte(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setShort(int parameterIndex, short x) throws SQLException {
delegate.setShort(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setInt(int parameterIndex, int x) throws SQLException {
delegate.setInt(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setLong(int parameterIndex, long x) throws SQLException {
delegate.setLong(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setFloat(int parameterIndex, float x) throws SQLException {
delegate.setFloat(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setDouble(int parameterIndex, double x) throws SQLException {
delegate.setDouble(parameterIndex, x);
putParameter(parameterIndex, String.valueOf(x));
}
@Override
public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
delegate.setBigDecimal(parameterIndex, x);
putParameter(parameterIndex, x);
}
@Override
public void setString(int parameterIndex, String x) throws SQLException {
delegate.setString(parameterIndex, x);
putParameter(parameterIndex, x);
}
@Override
@ -138,35 +160,41 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
@Override
public void setDate(int parameterIndex, Date x) throws SQLException {
delegate.setDate(parameterIndex, x);
putParameter(parameterIndex, x);
}
@SuppressWarnings("UngroupedOverloads")
@Override
public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
delegate.setDate(parameterIndex, x, cal);
putParameter(parameterIndex, x);
}
@SuppressWarnings("UngroupedOverloads")
@Override
public void setTime(int parameterIndex, Time x) throws SQLException {
delegate.setTime(parameterIndex, x);
putParameter(parameterIndex, x);
}
@Override
public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
delegate.setTime(parameterIndex, x, cal);
putParameter(parameterIndex, x);
}
@SuppressWarnings("UngroupedOverloads")
@Override
public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
delegate.setTimestamp(parameterIndex, x);
putParameter(parameterIndex, x);
}
@SuppressWarnings("UngroupedOverloads")
@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
delegate.setTimestamp(parameterIndex, x, cal);
putParameter(parameterIndex, x);
}
@SuppressWarnings("UngroupedOverloads")
@ -307,6 +335,7 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
@Override
public void setURL(int parameterIndex, URL x) throws SQLException {
delegate.setURL(parameterIndex, x);
putParameter(parameterIndex, x);
}
@Override
@ -317,11 +346,13 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
@Override
public void setRowId(int parameterIndex, RowId x) throws SQLException {
delegate.setRowId(parameterIndex, x);
putParameter(parameterIndex, x);
}
@Override
public void setNString(int parameterIndex, String value) throws SQLException {
delegate.setNString(parameterIndex, value);
putParameter(parameterIndex, value);
}
@SuppressWarnings("UngroupedOverloads")
@ -361,6 +392,7 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
@Override
public void clearParameters() throws SQLException {
delegate.clearParameters();
parameters.clear();
}
@Override
@ -368,8 +400,15 @@ class OpenTelemetryPreparedStatement<S extends PreparedStatement> extends OpenTe
return wrapBatchCall(delegate::executeBatch);
}
@Override
protected <T, E extends Exception> T wrapCall(String sql, ThrowingSupplier<T, E> callable)
throws E {
DbRequest request = DbRequest.create(dbInfo, sql, null, parameters);
return wrapCall(request, callable);
}
private <T, E extends Exception> T wrapBatchCall(ThrowingSupplier<T, E> callable) throws E {
DbRequest request = DbRequest.create(dbInfo, query, batchSize);
DbRequest request = DbRequest.create(dbInfo, query, batchSize, parameters);
return wrapCall(request, callable);
}

View File

@ -30,6 +30,7 @@ import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class OpenTelemetryStatement<S extends Statement> implements Statement {
@ -385,7 +386,7 @@ class OpenTelemetryStatement<S extends Statement> implements Statement {
}
private <T, E extends Exception> T wrapBatchCall(ThrowingSupplier<T, E> callable) throws E {
DbRequest request = DbRequest.create(dbInfo, batchCommands, batchSize);
DbRequest request = DbRequest.create(dbInfo, batchCommands, batchSize, Collections.emptyMap());
return wrapCall(request, callable);
}
}

View File

@ -16,6 +16,7 @@ import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT;
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;
@ -30,10 +31,22 @@ import io.opentelemetry.instrumentation.jdbc.TestConnection;
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;
@ -221,6 +234,73 @@ class OpenTelemetryConnectionTest {
connection.close();
}
// https://github.com/open-telemetry/semantic-conventions/pull/2093
@SuppressWarnings("deprecation")
@Test
void testVerifyPrepareStatementParameters() throws SQLException, MalformedURLException {
OpenTelemetry openTelemetry = testing.getOpenTelemetry();
Instrumenter<DbRequest, Void> instrumenter =
createStatementInstrumenter(testing.getOpenTelemetry(), true);
Instrumenter<DbRequest, Void> transactionInstrumenter =
createTransactionInstrumenter(openTelemetry, false);
DbInfo dbInfo = getDbInfo();
OpenTelemetryConnection connection =
new OpenTelemetryConnection(
new TestConnection(), dbInfo, instrumenter, transactionInstrumenter, 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")
@ -239,7 +319,22 @@ class OpenTelemetryConnectionTest {
}
@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<AttributeAssertion> 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<AttributeAssertion> additionAttributeAssertions = Arrays.asList(assertions);
testing.waitAndAssertTraces(
trace ->
trace.hasSpansSatisfyingExactly(
@ -249,19 +344,10 @@ class OpenTelemetryConnectionTest {
.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()))));
}
@SuppressWarnings("deprecation") // old semconv
@ -299,6 +385,6 @@ class OpenTelemetryConnectionTest {
createTransactionInstrumenter(openTelemetry, true);
DbInfo dbInfo = getDbInfo();
return new OpenTelemetryConnection(
new TestConnection(), dbInfo, statementInstrumenter, transactionInstrumenter);
new TestConnection(), dbInfo, statementInstrumenter, transactionInstrumenter, false);
}
}

View File

@ -22,6 +22,14 @@ configurations:
description: Used to specify a mapping from host names or IP addresses to peer services.
type: map
default: ""
- name: otel.instrumentation.jdbc.capture-query-parameters
description: >
Sets whether the query parameters should be captured as span attributes named
<code>db.query.parameter.&lt;key&gt;</code>. Enabling this option disables the statement
sanitization.<p>WARNING: captured query parameters may contain sensitive information such as
passwords, personally identifiable information or protected health info.
type: boolean
default: false
- name: otel.instrumentation.jdbc-datasource.enabled
description: Enables instrumentation of JDBC datasource connections.
type: boolean

View File

@ -26,12 +26,16 @@ import java.sql.SQLXML;
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<String, String> parameters;
TestPreparedStatement(Connection connection) {
super(connection);
this.parameters = new HashMap<>();
}
@Override
@ -147,7 +151,9 @@ class TestPreparedStatement extends TestStatement implements PreparedStatement {
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 {}

View File

@ -55,6 +55,10 @@ final class DataSourcePostProcessor implements BeanPostProcessor, Ordered {
InstrumentationConfigUtil.isStatementSanitizationEnabled(
configPropertiesProvider.getObject(),
"otel.instrumentation.jdbc.statement-sanitizer.enabled"))
.setCaptureQueryParameters(
configPropertiesProvider
.getObject()
.getBoolean("otel.instrumentation.jdbc.capture-query-parameters", false))
.setTransactionInstrumenterEnabled(
configPropertiesProvider
.getObject()

View File

@ -351,6 +351,12 @@
"description": "Enables the DB statement sanitization.",
"defaultValue": true
},
{
"name": "otel.instrumentation.jdbc.capture-query-parameters",
"type": "java.lang.Boolean",
"description": "Sets whether the query parameters should be captured as span attributes named <code>db.query.parameter.&lt;key&gt;</code>. Enabling this option disables the statement sanitization.<p>WARNING: captured query parameters may contain sensitive information such as passwords, personally identifiable information or protected health info.",
"defaultValue": false
},
{
"name": "otel.instrumentation.jdbc.experimental.transaction.enabled",
"type": "java.lang.Boolean",