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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,26 +62,35 @@ 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;
// 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<String> rawQueryTexts = getter.getRawQueryTexts(request);
Map<String, String> 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();
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -129,6 +142,19 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
}
}

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();
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<String> collection) {
StringBuilder builder = new StringBuilder();
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,24 @@ public SqlClientAttributesExtractorBuilder<REQUEST, RESPONSE> setStatementSaniti
return this;
}

/**
* Sets whether the {@code db.query.parameter.<key>} 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<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 All @@ -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 {
Expand Down Expand Up @@ -62,6 +64,22 @@ public Long getBatchSize(Map<String, Object> map) {
return read(map, "db.operation.batch.size", Long.class);
}

@Nullable
@Override
public Map<String, String> getQueryParameters(Map<String, Object> map) {
String parameterString = read(map, "db.query.parameter");

if (parameterString == null) {
return Collections.emptyMap();
}

Map<String, String> parameters = new HashMap<>();
for (String s : parameterString.split(";")) {
parameters.put(Integer.toString(parameters.size()), s);
}
return parameters;
}

protected String read(Map<String, Object> map, String key) {
return read(map, key, String.class);
}
Expand Down Expand Up @@ -387,4 +405,79 @@ 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
request.put("db.query.parameter", "'a';1");

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", 1L);
request.put("db.query.parameter", "1");

Context context = Context.root();

AttributesExtractor<Map<String, Object>, 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();
}
}
7 changes: 4 additions & 3 deletions instrumentation/jdbc/README.md
Original file line number Diff line number Diff line change
@@ -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.\<key\>.<br/><br/>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.<br/><br/>This option will disable otel.instrumentation.jdbc.statement-sanitizer |
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be added to metadata.yaml

11 changes: 11 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,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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ public final class JdbcSingletons {
private static final Instrumenter<DbRequest, Void> STATEMENT_INSTRUMENTER;
public static final Instrumenter<DataSource, DbInfo> 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.<DbRequest, Void>builder(
GlobalOpenTelemetry.get(),
Expand All @@ -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(
Expand Down
Loading
Loading