Don't cache sanitization results for large sql statements (#13353)
This commit is contained in:
parent
95cc300125
commit
8cd11e4688
|
@ -24,7 +24,9 @@ public interface DbClientCommonAttributesGetter<REQUEST, RESPONSE> {
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@Nullable
|
@Nullable
|
||||||
String getUser(REQUEST request);
|
default String getUser(REQUEST request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link #getDbNamespace(Object)} instead.
|
* @deprecated Use {@link #getDbNamespace(Object)} instead.
|
||||||
|
@ -43,7 +45,9 @@ public interface DbClientCommonAttributesGetter<REQUEST, RESPONSE> {
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@Nullable
|
@Nullable
|
||||||
String getConnectionString(REQUEST request);
|
default String getConnectionString(REQUEST request) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
default String getResponseStatus(@Nullable RESPONSE response, @Nullable Throwable error) {
|
default String getResponseStatus(@Nullable RESPONSE response, @Nullable Throwable error) {
|
||||||
|
|
|
@ -84,9 +84,6 @@ public abstract class DbClientSpanNameExtractor<REQUEST> implements SpanNameExtr
|
||||||
private static final class SqlClientSpanNameExtractor<REQUEST>
|
private static final class SqlClientSpanNameExtractor<REQUEST>
|
||||||
extends DbClientSpanNameExtractor<REQUEST> {
|
extends DbClientSpanNameExtractor<REQUEST> {
|
||||||
|
|
||||||
// a dedicated sanitizer just for extracting the operation and identifier name
|
|
||||||
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
|
|
||||||
|
|
||||||
private final SqlClientAttributesGetter<REQUEST, ?> getter;
|
private final SqlClientAttributesGetter<REQUEST, ?> getter;
|
||||||
|
|
||||||
private SqlClientSpanNameExtractor(SqlClientAttributesGetter<REQUEST, ?> getter) {
|
private SqlClientSpanNameExtractor(SqlClientAttributesGetter<REQUEST, ?> getter) {
|
||||||
|
@ -106,13 +103,15 @@ public abstract class DbClientSpanNameExtractor<REQUEST> implements SpanNameExtr
|
||||||
if (rawQueryTexts.size() > 1) { // for backcompat(?)
|
if (rawQueryTexts.size() > 1) { // for backcompat(?)
|
||||||
return computeSpanName(namespace, null, null);
|
return computeSpanName(namespace, null, null);
|
||||||
}
|
}
|
||||||
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryTexts.iterator().next());
|
SqlStatementInfo sanitizedStatement =
|
||||||
|
SqlStatementSanitizerUtil.sanitize(rawQueryTexts.iterator().next());
|
||||||
return computeSpanName(
|
return computeSpanName(
|
||||||
namespace, sanitizedStatement.getOperation(), sanitizedStatement.getMainIdentifier());
|
namespace, sanitizedStatement.getOperation(), sanitizedStatement.getMainIdentifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawQueryTexts.size() == 1) {
|
if (rawQueryTexts.size() == 1) {
|
||||||
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryTexts.iterator().next());
|
SqlStatementInfo sanitizedStatement =
|
||||||
|
SqlStatementSanitizerUtil.sanitize(rawQueryTexts.iterator().next());
|
||||||
String operation = sanitizedStatement.getOperation();
|
String operation = sanitizedStatement.getOperation();
|
||||||
if (isBatch(request)) {
|
if (isBatch(request)) {
|
||||||
operation = "BATCH " + operation;
|
operation = "BATCH " + operation;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
class MultiQuery {
|
class MultiQuery {
|
||||||
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
|
|
||||||
|
|
||||||
private final String mainIdentifier;
|
private final String mainIdentifier;
|
||||||
private final String operation;
|
private final String operation;
|
||||||
|
@ -28,7 +27,7 @@ class MultiQuery {
|
||||||
UniqueValue uniqueOperation = new UniqueValue();
|
UniqueValue uniqueOperation = new UniqueValue();
|
||||||
Set<String> uniqueStatements = new LinkedHashSet<>();
|
Set<String> uniqueStatements = new LinkedHashSet<>();
|
||||||
for (String rawQueryText : rawQueryTexts) {
|
for (String rawQueryText : rawQueryTexts) {
|
||||||
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
|
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
|
||||||
String mainIdentifier = sanitizedStatement.getMainIdentifier();
|
String mainIdentifier = sanitizedStatement.getMainIdentifier();
|
||||||
uniqueMainIdentifier.set(mainIdentifier);
|
uniqueMainIdentifier.set(mainIdentifier);
|
||||||
String operation = sanitizedStatement.getOperation();
|
String operation = sanitizedStatement.getOperation();
|
||||||
|
|
|
@ -55,8 +55,6 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String SQL_CALL = "CALL";
|
private static final String SQL_CALL = "CALL";
|
||||||
// sanitizer is also used to extract operation and table name, so we have it always enabled here
|
|
||||||
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
|
|
||||||
|
|
||||||
private final AttributeKey<String> oldSemconvTableAttribute;
|
private final AttributeKey<String> oldSemconvTableAttribute;
|
||||||
private final boolean statementSanitizationEnabled;
|
private final boolean statementSanitizationEnabled;
|
||||||
|
@ -83,7 +81,7 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
|
||||||
if (SemconvStability.emitOldDatabaseSemconv()) {
|
if (SemconvStability.emitOldDatabaseSemconv()) {
|
||||||
if (rawQueryTexts.size() == 1) { // for backcompat(?)
|
if (rawQueryTexts.size() == 1) { // for backcompat(?)
|
||||||
String rawQueryText = rawQueryTexts.iterator().next();
|
String rawQueryText = rawQueryTexts.iterator().next();
|
||||||
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
|
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
|
||||||
String operation = sanitizedStatement.getOperation();
|
String operation = sanitizedStatement.getOperation();
|
||||||
internalSet(
|
internalSet(
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -104,7 +102,7 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
|
||||||
}
|
}
|
||||||
if (rawQueryTexts.size() == 1) {
|
if (rawQueryTexts.size() == 1) {
|
||||||
String rawQueryText = rawQueryTexts.iterator().next();
|
String rawQueryText = rawQueryTexts.iterator().next();
|
||||||
SqlStatementInfo sanitizedStatement = sanitizer.sanitize(rawQueryText);
|
SqlStatementInfo sanitizedStatement = SqlStatementSanitizerUtil.sanitize(rawQueryText);
|
||||||
String operation = sanitizedStatement.getOperation();
|
String operation = sanitizedStatement.getOperation();
|
||||||
internalSet(
|
internalSet(
|
||||||
attributes,
|
attributes,
|
||||||
|
|
|
@ -21,6 +21,7 @@ public final class SqlStatementSanitizer {
|
||||||
|
|
||||||
private static final Cache<CacheKey, SqlStatementInfo> sqlToStatementInfoCache =
|
private static final Cache<CacheKey, SqlStatementInfo> sqlToStatementInfoCache =
|
||||||
Cache.bounded(1000);
|
Cache.bounded(1000);
|
||||||
|
private static final int LARGE_STATEMENT_THRESHOLD = 10 * 1024;
|
||||||
|
|
||||||
public static SqlStatementSanitizer create(boolean statementSanitizationEnabled) {
|
public static SqlStatementSanitizer create(boolean statementSanitizationEnabled) {
|
||||||
return new SqlStatementSanitizer(statementSanitizationEnabled);
|
return new SqlStatementSanitizer(statementSanitizationEnabled);
|
||||||
|
@ -40,12 +41,24 @@ public final class SqlStatementSanitizer {
|
||||||
if (!statementSanitizationEnabled || statement == null) {
|
if (!statementSanitizationEnabled || statement == null) {
|
||||||
return SqlStatementInfo.create(statement, null, null);
|
return SqlStatementInfo.create(statement, null, null);
|
||||||
}
|
}
|
||||||
|
// sanitization result will not be cached for statements larger than the threshold to avoid
|
||||||
|
// cache growing too large
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13180
|
||||||
|
if (statement.length() > LARGE_STATEMENT_THRESHOLD) {
|
||||||
|
return sanitizeImpl(statement, dialect);
|
||||||
|
}
|
||||||
return sqlToStatementInfoCache.computeIfAbsent(
|
return sqlToStatementInfoCache.computeIfAbsent(
|
||||||
CacheKey.create(statement, dialect),
|
CacheKey.create(statement, dialect), k -> sanitizeImpl(statement, dialect));
|
||||||
k -> {
|
}
|
||||||
supportability.incrementCounter(SQL_STATEMENT_SANITIZER_CACHE_MISS);
|
|
||||||
return AutoSqlSanitizer.sanitize(statement, dialect);
|
private static SqlStatementInfo sanitizeImpl(@Nullable String statement, SqlDialect dialect) {
|
||||||
});
|
supportability.incrementCounter(SQL_STATEMENT_SANITIZER_CACHE_MISS);
|
||||||
|
return AutoSqlSanitizer.sanitize(statement, dialect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for tests
|
||||||
|
static boolean isCached(String statement) {
|
||||||
|
return sqlToStatementInfoCache.get(CacheKey.create(statement, SqlDialect.DEFAULT)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AutoValue
|
@AutoValue
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.instrumentation.api.incubator.semconv.db;
|
||||||
|
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||||
|
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for sanitizing sql that keeps sanitization results in {@link InstrumenterContext} so
|
||||||
|
* that each statement would be sanitized only once for given {@link Instrumenter} call.
|
||||||
|
*/
|
||||||
|
class SqlStatementSanitizerUtil {
|
||||||
|
private static final SqlStatementSanitizer sanitizer = SqlStatementSanitizer.create(true);
|
||||||
|
|
||||||
|
static SqlStatementInfo sanitize(String queryText) {
|
||||||
|
Map<String, SqlStatementInfo> map =
|
||||||
|
InstrumenterContext.computeIfAbsent("sanitized-sql-map", unused -> new HashMap<>());
|
||||||
|
return map.computeIfAbsent(queryText, sanitizer::sanitize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqlStatementSanitizerUtil() {}
|
||||||
|
}
|
|
@ -137,6 +137,27 @@ public class SqlStatementSanitizerTest {
|
||||||
assertThat(sanitized).isEqualTo("select col from table where col in (?)");
|
assertThat(sanitized).isEqualTo("select col from table where col in (?)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void largeStatementCached() {
|
||||||
|
// test that short statement is cached
|
||||||
|
String shortStatement = "SELECT * FROM TABLE WHERE FIELD = 1234";
|
||||||
|
String sanitizedShort =
|
||||||
|
SqlStatementSanitizer.create(true).sanitize(shortStatement).getFullStatement();
|
||||||
|
assertThat(sanitizedShort).doesNotContain("1234");
|
||||||
|
assertThat(SqlStatementSanitizer.isCached(shortStatement)).isTrue();
|
||||||
|
|
||||||
|
// test that large statement is not cached
|
||||||
|
StringBuffer s = new StringBuffer();
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
s.append("SELECT * FROM TABLE WHERE FIELD = 1234 AND ");
|
||||||
|
}
|
||||||
|
String largeStatement = s.toString();
|
||||||
|
String sanitizedLarge =
|
||||||
|
SqlStatementSanitizer.create(true).sanitize(largeStatement).getFullStatement();
|
||||||
|
assertThat(sanitizedLarge).doesNotContain("1234");
|
||||||
|
assertThat(SqlStatementSanitizer.isCached(largeStatement)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
static class SqlArgs implements ArgumentsProvider {
|
static class SqlArgs implements ArgumentsProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -14,6 +14,7 @@ import io.opentelemetry.context.Context;
|
||||||
import io.opentelemetry.context.ContextKey;
|
import io.opentelemetry.context.ContextKey;
|
||||||
import io.opentelemetry.instrumentation.api.internal.HttpRouteState;
|
import io.opentelemetry.instrumentation.api.internal.HttpRouteState;
|
||||||
import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess;
|
import io.opentelemetry.instrumentation.api.internal.InstrumenterAccess;
|
||||||
|
import io.opentelemetry.instrumentation.api.internal.InstrumenterContext;
|
||||||
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
|
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
|
||||||
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
|
import io.opentelemetry.instrumentation.api.internal.SupportabilityMetrics;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -164,6 +165,14 @@ public class Instrumenter<REQUEST, RESPONSE> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Context doStart(Context parentContext, REQUEST request, @Nullable Instant startTime) {
|
private Context doStart(Context parentContext, REQUEST request, @Nullable Instant startTime) {
|
||||||
|
try {
|
||||||
|
return doStartImpl(parentContext, request, startTime);
|
||||||
|
} finally {
|
||||||
|
InstrumenterContext.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Context doStartImpl(Context parentContext, REQUEST request, @Nullable Instant startTime) {
|
||||||
SpanKind spanKind = spanKindExtractor.extract(request);
|
SpanKind spanKind = spanKindExtractor.extract(request);
|
||||||
SpanBuilder spanBuilder =
|
SpanBuilder spanBuilder =
|
||||||
tracer.spanBuilder(spanNameExtractor.extract(request)).setSpanKind(spanKind);
|
tracer.spanBuilder(spanNameExtractor.extract(request)).setSpanKind(spanKind);
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.instrumentation.api.internal;
|
||||||
|
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for sharing computed values between different {@link AttributesExtractor}s and
|
||||||
|
* {@link SpanNameExtractor} called in the start phase of the {@link Instrumenter}.
|
||||||
|
*
|
||||||
|
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
|
||||||
|
* at any time.
|
||||||
|
*/
|
||||||
|
public final class InstrumenterContext {
|
||||||
|
private static final ThreadLocal<InstrumenterContext> instrumenterContext =
|
||||||
|
new ThreadLocal<InstrumenterContext>() {
|
||||||
|
@Override
|
||||||
|
protected InstrumenterContext initialValue() {
|
||||||
|
return new InstrumenterContext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Map<String, Object> map = new HashMap<>();
|
||||||
|
|
||||||
|
private InstrumenterContext() {}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T computeIfAbsent(String key, Function<String, T> function) {
|
||||||
|
return (T) get().computeIfAbsent(key, function);
|
||||||
|
}
|
||||||
|
|
||||||
|
// visible for testing
|
||||||
|
static Map<String, Object> get() {
|
||||||
|
return instrumenterContext.get().map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reset() {
|
||||||
|
instrumenterContext.remove();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright The OpenTelemetry Authors
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.opentelemetry.instrumentation.api.internal;
|
||||||
|
|
||||||
|
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import io.opentelemetry.api.common.Attributes;
|
||||||
|
import io.opentelemetry.api.common.AttributesBuilder;
|
||||||
|
import io.opentelemetry.context.Context;
|
||||||
|
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
|
||||||
|
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor;
|
||||||
|
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesGetter;
|
||||||
|
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlStatementInfo;
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
|
||||||
|
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
|
||||||
|
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
|
||||||
|
import io.opentelemetry.semconv.incubating.DbIncubatingAttributes;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
|
|
||||||
|
class InstrumenterContextTest {
|
||||||
|
@RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "deprecation"}) // using deprecated DB_SQL_TABLE
|
||||||
|
@Test
|
||||||
|
void testSqlSanitizer() {
|
||||||
|
String testQuery = "SELECT name FROM test WHERE id = 1";
|
||||||
|
SqlClientAttributesGetter<Object, Void> getter =
|
||||||
|
new SqlClientAttributesGetter<Object, Void>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getRawQueryTexts(Object request) {
|
||||||
|
return Collections.singletonList(testQuery);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SpanNameExtractor<Object> spanNameExtractor = DbClientSpanNameExtractor.create(getter);
|
||||||
|
AttributesExtractor<Object, Void> attributesExtractor =
|
||||||
|
SqlClientAttributesExtractor.create(getter);
|
||||||
|
|
||||||
|
InstrumenterContext.reset();
|
||||||
|
cleanup.deferCleanup(InstrumenterContext::reset);
|
||||||
|
|
||||||
|
assertThat(InstrumenterContext.get()).isEmpty();
|
||||||
|
assertThat(spanNameExtractor.extract(null)).isEqualTo("SELECT test");
|
||||||
|
// verify that sanitized statement was cached, see SqlStatementSanitizerUtil
|
||||||
|
assertThat(InstrumenterContext.get()).containsKey("sanitized-sql-map");
|
||||||
|
Map<String, SqlStatementInfo> sanitizedMap =
|
||||||
|
(Map<String, SqlStatementInfo>) InstrumenterContext.get().get("sanitized-sql-map");
|
||||||
|
assertThat(sanitizedMap).containsKey(testQuery);
|
||||||
|
|
||||||
|
// replace cached sanitization result to verify it is used
|
||||||
|
sanitizedMap.put(
|
||||||
|
testQuery,
|
||||||
|
SqlStatementInfo.create("SELECT name2 FROM test2 WHERE id = ?", "SELECT", "test2"));
|
||||||
|
{
|
||||||
|
AttributesBuilder builder = Attributes.builder();
|
||||||
|
attributesExtractor.onStart(builder, Context.root(), null);
|
||||||
|
assertThat(builder.build().get(maybeStable(DbIncubatingAttributes.DB_SQL_TABLE)))
|
||||||
|
.isEqualTo("test2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear cached value to see whether it gets recomputed correctly
|
||||||
|
sanitizedMap.clear();
|
||||||
|
{
|
||||||
|
AttributesBuilder builder = Attributes.builder();
|
||||||
|
attributesExtractor.onStart(builder, Context.root(), null);
|
||||||
|
assertThat(builder.build().get(maybeStable(DbIncubatingAttributes.DB_SQL_TABLE)))
|
||||||
|
.isEqualTo("test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue