Skip to content

Added JDBC db.query.parameter span attributes #13719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
32aeb83
Added db.operation.parameter span attributes
AlixBa Apr 14, 2025
d1ebd7e
add backward method on SqlStatementInfo for callers without parameters
AlixBa Apr 16, 2025
2d48543
fix SlickTest with span attributes
AlixBa Apr 16, 2025
26d635c
Merge branch 'open-telemetry:main' into add-operation-parameter-attri…
AlixBa Apr 17, 2025
b3365f1
Replace db.operation.parameter by db.query.parameter
AlixBa Apr 17, 2025
a785cd6
Change -D flag name; explicit Advices override
AlixBa Apr 28, 2025
235f221
Merge branch 'open-telemetry:main' into add-operation-parameter-attri…
AlixBa Apr 28, 2025
99ce35f
format with spotlessApply
AlixBa Apr 28, 2025
bf67533
fix tests not updated with latest changes
AlixBa Apr 28, 2025
9d6f08a
fix naming conventions & tests database initialization
AlixBa Apr 28, 2025
f6702f0
only store parameters if feature enabled & factorize agent advices & …
AlixBa Apr 30, 2025
07f8c5f
Merge branch 'main' into add-operation-parameter-attributes
AlixBa Apr 30, 2025
18eb6f8
send back emptyMap instead of null
AlixBa Apr 30, 2025
b22ffe6
update metadata.yaml with capture-query-parameters
AlixBa May 4, 2025
5b872e6
Merge branch 'main' into add-operation-parameter-attributes
AlixBa May 4, 2025
c4c877e
review
laurit May 9, 2025
949dbb7
Merge branch 'main' into add-operation-parameter-attributes
laurit May 9, 2025
afa52b8
simplify and fix test
laurit May 9, 2025
bd16245
simplify
laurit May 9, 2025
ceff3f5
merge
laurit May 14, 2025
ff936a4
update metadata
laurit May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a
Expand All @@ -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(
Expand All @@ -58,14 +62,18 @@ public static <REQUEST, RESPONSE> SqlClientAttributesExtractorBuilder<REQUEST, R

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
Expand All @@ -78,6 +86,9 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
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();
Expand All @@ -95,8 +106,6 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
}

if (SemconvStability.emitStableDatabaseSemconv()) {
Long batchSize = getter.getBatchSize(request);
boolean isBatch = batchSize != null && batchSize > 1;
if (isBatch) {
internalSet(attributes, DB_OPERATION_BATCH_SIZE, batchSize);
}
Expand Down Expand Up @@ -127,6 +136,20 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
}
}
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,12 +49,27 @@ public SqlClientAttributesExtractorBuilder<REQUEST, RESPONSE> setStatementSaniti
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -66,4 +68,9 @@ default Collection<String> getRawQueryTexts(REQUEST request) {
default Long getBatchSize(REQUEST request) {
return null;
}

// TODO: make this required to implement
default Map<String, String> getQueryParameters(REQUEST request) {
return Collections.emptyMap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,6 +63,14 @@ public Long getBatchSize(Map<String, Object> map) {
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);
}
Expand Down Expand Up @@ -387,4 +396,74 @@ void shouldIgnoreBatchSizeOne() {

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();
}
}
9 changes: 5 additions & 4 deletions instrumentation/jdbc/README.md
Original file line number Diff line number Diff line change
@@ -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. |
10 changes: 10 additions & 0 deletions instrumentation/jdbc/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ tasks {
test {
filter {
excludeTestsMatching("SlickTest")
excludeTestsMatching("PreparedStatementParametersTest")
}
jvmArgs("-Dotel.instrumentation.jdbc-datasource.enabled=true")
}

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")
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ 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();
AttributesExtractor<DbRequest, Void> peerServiceExtractor =
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(),
Expand All @@ -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(
Expand Down
Loading
Loading