Skip to content

Commit 8cd11e4

Browse files
authored
Don't cache sanitization results for large sql statements (open-telemetry#13353)
1 parent 95cc300 commit 8cd11e4

File tree

10 files changed

+214
-18
lines changed

10 files changed

+214
-18
lines changed

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesGetter.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ default String getDbSystem(REQUEST request) {
2424

2525
@Deprecated
2626
@Nullable
27-
String getUser(REQUEST request);
27+
default String getUser(REQUEST request) {
28+
return null;
29+
}
2830

2931
/**
3032
* @deprecated Use {@link #getDbNamespace(Object)} instead.
@@ -43,7 +45,9 @@ default String getDbNamespace(REQUEST request) {
4345

4446
@Deprecated
4547
@Nullable
46-
String getConnectionString(REQUEST request);
48+
default String getConnectionString(REQUEST request) {
49+
return null;
50+
}
4751

4852
@Nullable
4953
default String getResponseStatus(@Nullable RESPONSE response, @Nullable Throwable error) {

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientSpanNameExtractor.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ public String extract(REQUEST request) {
8484
private static final class SqlClientSpanNameExtractor<REQUEST>
8585
extends DbClientSpanNameExtractor<REQUEST> {
8686

87-
// a dedicated sanitizer just for extracting the operation and identifier name
88-
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
89-
9087
private final SqlClientAttributesGetter<REQUEST, ?> getter;
9188

9289
private SqlClientSpanNameExtractor(SqlClientAttributesGetter<REQUEST, ?> getter) {
@@ -106,13 +103,15 @@ public String extract(REQUEST request) {
106103
if (rawQueryTexts.size() > 1) { // for backcompat(?)
107104
return computeSpanName(namespace, null, null);
108105
}
109-
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryTexts.iterator().next());
106+
SqlStatementInfo sanitizedStatement =
107+
SqlStatementSanitizerUtil.sanitize(rawQueryTexts.iterator().next());
110108
return computeSpanName(
111109
namespace, sanitizedStatement.getOperation(), sanitizedStatement.getMainIdentifier());
112110
}
113111

114112
if (rawQueryTexts.size() == 1) {
115-
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryTexts.iterator().next());
113+
SqlStatementInfo sanitizedStatement =
114+
SqlStatementSanitizerUtil.sanitize(rawQueryTexts.iterator().next());
116115
String operation = sanitizedStatement.getOperation();
117116
if (isBatch(request)) {
118117
operation = "BATCH " + operation;

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/MultiQuery.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.Set;
1111

1212
class MultiQuery {
13-
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
1413

1514
private final String mainIdentifier;
1615
private final String operation;
@@ -28,7 +27,7 @@ static MultiQuery analyze(
2827
UniqueValue uniqueOperation = new UniqueValue();
2928
Set<String> uniqueStatements = new LinkedHashSet<>();
3029
for (String rawQueryText : rawQueryTexts) {
31-
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
30+
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
3231
String mainIdentifier = sanitizedStatement.getMainIdentifier();
3332
uniqueMainIdentifier.set(mainIdentifier);
3433
String operation = sanitizedStatement.getOperation();

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ public static <REQUEST, RESPONSE> SqlClientAttributesExtractorBuilder<REQUEST, R
5555
}
5656

5757
private static final String SQL_CALL = "CALL";
58-
// sanitizer is also used to extract operation and table name, so we have it always enabled here
59-
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
6058

6159
private final AttributeKey<String> oldSemconvTableAttribute;
6260
private final boolean statementSanitizationEnabled;
@@ -83,7 +81,7 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
8381
if (SemconvStability.emitOldDatabaseSemconv()) {
8482
if (rawQueryTexts.size() == 1) { // for backcompat(?)
8583
String rawQueryText = rawQueryTexts.iterator().next();
86-
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
84+
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
8785
String operation = sanitizedStatement.getOperation();
8886
internalSet(
8987
attributes,
@@ -104,7 +102,7 @@ public void onStart(AttributesBuilder attributes, Context parentContext, REQUEST
104102
}
105103
if (rawQueryTexts.size() == 1) {
106104
String rawQueryText = rawQueryTexts.iterator().next();
107-
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
105+
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
108106
String operation = sanitizedStatement.getOperation();
109107
internalSet(
110108
attributes,

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizer.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public final class SqlStatementSanitizer {
2121

2222
private static final Cache<CacheKey, SqlStatementInfo> sqlToStatementInfoCache =
2323
Cache.bounded(1000);
24+
private static final int LARGE_STATEMENT_THRESHOLD = 10 * 1024;
2425

2526
public static SqlStatementSanitizer create(boolean statementSanitizationEnabled) {
2627
return new SqlStatementSanitizer(statementSanitizationEnabled);
@@ -40,12 +41,24 @@ public SqlStatementInfo sanitize(@Nullable String statement, SqlDialect dialect)
4041
if (!statementSanitizationEnabled || statement == null) {
4142
return SqlStatementInfo.create(statement, null, null);
4243
}
44+
// sanitization result will not be cached for statements larger than the threshold to avoid
45+
// cache growing too large
46+
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13180
47+
if (statement.length() > LARGE_STATEMENT_THRESHOLD) {
48+
return sanitizeImpl(statement, dialect);
49+
}
4350
return sqlToStatementInfoCache.computeIfAbsent(
44-
CacheKey.create(statement, dialect),
45-
k -> {
46-
supportability.incrementCounter(SQL_STATEMENT_SANITIZER_CACHE_MISS);
47-
return AutoSqlSanitizer.sanitize(statement, dialect);
48-
});
51+
CacheKey.create(statement, dialect), k -> sanitizeImpl(statement, dialect));
52+
}
53+
54+
private static SqlStatementInfo sanitizeImpl(@Nullable String statement, SqlDialect dialect) {
55+
supportability.incrementCounter(SQL_STATEMENT_SANITIZER_CACHE_MISS);
56+
return AutoSqlSanitizer.sanitize(statement, dialect);
57+
}
58+
59+
// visible for tests
60+
static boolean isCached(String statement) {
61+
return sqlToStatementInfoCache.get(CacheKey.create(statement, SqlDialect.DEFAULT)) != null;
4962
}
5063

5164
@AutoValue
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db;
7+
8+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
9+
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
/**
14+
* Helper class for sanitizing sql that keeps sanitization results in {@link InstrumenterContext} so
15+
* that each statement would be sanitized only once for given {@link Instrumenter} call.
16+
*/
17+
class SqlStatementSanitizerUtil {
18+
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
19+
20+
static SqlStatementInfo sanitize(String queryText) {
21+
Map<String, SqlStatementInfo> map =
22+
InstrumenterContext.computeIfAbsent("sanitized-sql-map", unused -> new HashMap<>());
23+
return map.computeIfAbsent(queryText, sanitizer::sanitize);
24+
}
25+
26+
private SqlStatementSanitizerUtil() {}
27+
}

instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlStatementSanitizerTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,27 @@ public void longInStatementDoesntCauseStackOverflow() {
137137
assertThat(sanitized).isEqualTo("select col from table where col in (?)");
138138
}
139139

140+
@Test
141+
public void largeStatementCached() {
142+
// test that short statement is cached
143+
String shortStatement = "SELECT * FROM TABLE WHERE FIELD = 1234";
144+
String sanitizedShort =
145+
SqlStatementSanitizer.create(true).sanitize(shortStatement).getFullStatement();
146+
assertThat(sanitizedShort).doesNotContain("1234");
147+
assertThat(SqlStatementSanitizer.isCached(shortStatement)).isTrue();
148+
149+
// test that large statement is not cached
150+
StringBuffer s = new StringBuffer();
151+
for (int i = 0; i < 10000; i++) {
152+
s.append("SELECT * FROM TABLE WHERE FIELD = 1234 AND ");
153+
}
154+
String largeStatement = s.toString();
155+
String sanitizedLarge =
156+
SqlStatementSanitizer.create(true).sanitize(largeStatement).getFullStatement();
157+
assertThat(sanitizedLarge).doesNotContain("1234");
158+
assertThat(SqlStatementSanitizer.isCached(largeStatement)).isFalse();
159+
}
160+
140161
static class SqlArgs implements ArgumentsProvider {
141162

142163
@Override

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.opentelemetry.context.ContextKey;
1515
import io.opentelemetry.instrumentation.api.internal.HttpRouteState;
1616
import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess;
17+
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
1718
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
1819
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
1920
import java.time.Instant;
@@ -164,6 +165,14 @@ Context startAndEnd(
164165
}
165166

166167
private Context doStart(Context parentContext, REQUEST request, @Nullable Instant startTime) {
168+
try {
169+
return doStartImpl(parentContext, request, startTime);
170+
} finally {
171+
InstrumenterContext.reset();
172+
}
173+
}
174+
175+
private Context doStartImpl(Context parentContext, REQUEST request, @Nullable Instant startTime) {
167176
SpanKind spanKind = spanKindExtractor.extract(request);
168177
SpanBuilder spanBuilder =
169178
tracer.spanBuilder(spanNameExtractor.extract(request)).setSpanKind(spanKind);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.internal;
7+
8+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
9+
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
10+
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
import java.util.function.Function;
14+
15+
/**
16+
* Helper class for sharing computed values between different {@link AttributesExtractor}s and
17+
* {@link SpanNameExtractor} called in the start phase of the {@link Instrumenter}.
18+
*
19+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
20+
* at any time.
21+
*/
22+
public final class InstrumenterContext {
23+
private static final ThreadLocal<InstrumenterContext> instrumenterContext =
24+
new ThreadLocal<InstrumenterContext>() {
25+
@Override
26+
protected InstrumenterContext initialValue() {
27+
return new InstrumenterContext();
28+
}
29+
};
30+
31+
private final Map<String, Object> map = new HashMap<>();
32+
33+
private InstrumenterContext() {}
34+
35+
@SuppressWarnings("unchecked")
36+
public static <T> T computeIfAbsent(String key, Function<String, T> function) {
37+
return (T) get().computeIfAbsent(key, function);
38+
}
39+
40+
// visible for testing
41+
static Map<String, Object> get() {
42+
return instrumenterContext.get().map;
43+
}
44+
45+
public static void reset() {
46+
instrumenterContext.remove();
47+
}
48+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.internal;
7+
8+
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.common.AttributesBuilder;
13+
import io.opentelemetry.context.Context;
14+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
15+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor;
16+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter;
17+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo;
18+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
19+
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
20+
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
21+
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.Map;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.RegisterExtension;
27+
28+
class InstrumenterContextTest {
29+
@RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
30+
31+
@SuppressWarnings({"unchecked", "deprecation"}) // using deprecated DB_SQL_TABLE
32+
@Test
33+
void testSqlSanitizer() {
34+
String testQuery = "SELECT name FROM test WHERE id = 1";
35+
SqlClientAttributesGetter<Object, Void> getter =
36+
new SqlClientAttributesGetter<Object, Void>() {
37+
38+
@Override
39+
public Collection<String> getRawQueryTexts(Object request) {
40+
return Collections.singletonList(testQuery);
41+
}
42+
};
43+
SpanNameExtractor<Object> spanNameExtractor = DbClientSpanNameExtractor.create(getter);
44+
AttributesExtractor<Object, Void> attributesExtractor =
45+
SqlClientAttributesExtractor.create(getter);
46+
47+
InstrumenterContext.reset();
48+
cleanup.deferCleanup(InstrumenterContext::reset);
49+
50+
assertThat(InstrumenterContext.get()).isEmpty();
51+
assertThat(spanNameExtractor.extract(null)).isEqualTo("SELECT test");
52+
// verify that sanitized statement was cached, see SqlStatementSanitizerUtil
53+
assertThat(InstrumenterContext.get()).containsKey("sanitized-sql-map");
54+
Map<String, SqlStatementInfo> sanitizedMap =
55+
(Map<String, SqlStatementInfo>) InstrumenterContext.get().get("sanitized-sql-map");
56+
assertThat(sanitizedMap).containsKey(testQuery);
57+
58+
// replace cached sanitization result to verify it is used
59+
sanitizedMap.put(
60+
testQuery,
61+
SqlStatementInfo.create("SELECT name2 FROM test2 WHERE id = ?", "SELECT", "test2"));
62+
{
63+
AttributesBuilder builder = Attributes.builder();
64+
attributesExtractor.onStart(builder, Context.root(), null);
65+
assertThat(builder.build().get(maybeStable(DbIncubatingAttributes.DB_SQL_TABLE)))
66+
.isEqualTo("test2");
67+
}
68+
69+
// clear cached value to see whether it gets recomputed correctly
70+
sanitizedMap.clear();
71+
{
72+
AttributesBuilder builder = Attributes.builder();
73+
attributesExtractor.onStart(builder, Context.root(), null);
74+
assertThat(builder.build().get(maybeStable(DbIncubatingAttributes.DB_SQL_TABLE)))
75+
.isEqualTo("test");
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)