Skip to content

Commit 76d4c6f

Browse files
authored
Make sqlcommenter more configurable (#15169)
1 parent 20387ee commit 76d4c6f

File tree

28 files changed

+542
-216
lines changed

28 files changed

+542
-216
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db.internal;
7+
8+
import io.opentelemetry.context.propagation.TextMapPropagator;
9+
import java.util.function.BiFunction;
10+
import java.util.function.Predicate;
11+
12+
/**
13+
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
14+
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
15+
* guarantees are made.
16+
*/
17+
public final class SqlCommenter {
18+
private final boolean enabled;
19+
private final BiFunction<Object, Boolean, TextMapPropagator> propagator;
20+
private final Predicate<Object> prepend;
21+
22+
SqlCommenter(
23+
boolean enabled,
24+
BiFunction<Object, Boolean, TextMapPropagator> propagator,
25+
Predicate<Object> prepend) {
26+
this.enabled = enabled;
27+
this.propagator = propagator;
28+
this.prepend = prepend;
29+
}
30+
31+
public static SqlCommenterBuilder builder() {
32+
return new SqlCommenterBuilder();
33+
}
34+
35+
public static SqlCommenter noop() {
36+
return builder().build();
37+
}
38+
39+
/**
40+
* Augments the given SQL query with comment containing tracing context.
41+
*
42+
* @param connection connection object, e.g. JDBC connection or R2DBC connection, that is used to
43+
* execute the query
44+
* @param sql original query
45+
* @param executed whether the query is immediately executed after being processed, e.g. {@link
46+
* java.sql.Statement#execute(String)}, or may be executed later, e.g. {@link
47+
* java.sql.Connection#prepareStatement(String)}
48+
* @return modified query
49+
*/
50+
public String processQuery(Object connection, String sql, boolean executed) {
51+
if (!enabled) {
52+
return sql;
53+
}
54+
55+
return SqlCommenterUtil.processQuery(
56+
sql, propagator.apply(connection, executed), prepend.test(connection));
57+
}
58+
59+
public boolean isEnabled() {
60+
return enabled;
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db.internal;
7+
8+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
9+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
10+
import io.opentelemetry.context.propagation.TextMapPropagator;
11+
import java.sql.Connection;
12+
import java.sql.Statement;
13+
import java.util.function.BiFunction;
14+
import java.util.function.Predicate;
15+
16+
/**
17+
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
18+
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
19+
* guarantees are made.
20+
*/
21+
public final class SqlCommenterBuilder {
22+
private boolean enabled;
23+
private BiFunction<Object, Boolean, TextMapPropagator> propagator =
24+
(unused1, unused2) -> W3CTraceContextPropagator.getInstance();
25+
private Predicate<Object> prepend = unused -> false;
26+
27+
SqlCommenterBuilder() {}
28+
29+
/** Enable adding sqlcommenter comments to sql queries. Default is disabled. */
30+
@CanIgnoreReturnValue
31+
public SqlCommenterBuilder setEnabled(boolean enabled) {
32+
this.enabled = enabled;
33+
return this;
34+
}
35+
36+
/**
37+
* Prepend the sqlcommenter comment to the query instead of appending it. Default is to append.
38+
*/
39+
@CanIgnoreReturnValue
40+
public SqlCommenterBuilder setPrepend(boolean prepend) {
41+
this.prepend = unused -> prepend;
42+
return this;
43+
}
44+
45+
/**
46+
* Prepend the sqlcommenter comment to the query instead of appending it. Default is to append.
47+
*
48+
* @param prepend a predicate that receives the database connection. Connection may be a jdbc
49+
* Connection, R2DBC Connection, or any other connection type used by the data access
50+
* framework performing the operation.
51+
*/
52+
@CanIgnoreReturnValue
53+
public SqlCommenterBuilder setPrepend(Predicate<Object> prepend) {
54+
this.prepend = prepend;
55+
return this;
56+
}
57+
58+
/**
59+
* Set the propagator used to inject tracing context into sql comments. Default is W3C Trace
60+
* Context propagator.
61+
*/
62+
@CanIgnoreReturnValue
63+
public SqlCommenterBuilder setPropagator(TextMapPropagator propagator) {
64+
this.propagator = (unused1, unused2) -> propagator;
65+
return this;
66+
}
67+
68+
/**
69+
* Set the propagator used to inject tracing context into sql comments. Default is W3C Trace
70+
* Context propagator.
71+
*
72+
* @param propagator a function that receives the database connection and whether the query is
73+
* executed only once or could be reused. Connection may be a JDBC connection, R2DBC
74+
* connection, or any other connection type used by the data access framework performing the
75+
* operation. If the second argument to the function is true, the query is executed only once
76+
* (e.g. JDBC {@link Statement#execute(String)}) immediately after processing. If false, the
77+
* query could be reused (e.g. JDBC {@link Connection#prepareStatement(String)}).
78+
*/
79+
@CanIgnoreReturnValue
80+
public SqlCommenterBuilder setPropagator(
81+
BiFunction<Object, Boolean, TextMapPropagator> propagator) {
82+
this.propagator = propagator;
83+
return this;
84+
}
85+
86+
public SqlCommenter build() {
87+
return new SqlCommenter(enabled, propagator, prepend);
88+
}
89+
}

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

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,22 @@
66
package io.opentelemetry.instrumentation.api.incubator.semconv.db.internal;
77

88
import io.opentelemetry.api.trace.Span;
9-
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
109
import io.opentelemetry.context.Context;
10+
import io.opentelemetry.context.propagation.TextMapPropagator;
1111
import java.io.UnsupportedEncodingException;
1212
import java.net.URLEncoder;
13-
import javax.annotation.Nullable;
13+
import java.util.Iterator;
14+
import java.util.LinkedHashMap;
15+
import java.util.Map;
1416

15-
/**
16-
* This class is internal and experimental. Its APIs are unstable and can change at any time. Its
17-
* APIs (or a version of them) may be promoted to the public stable API in the future, but no
18-
* guarantees are made.
19-
*/
20-
public final class SqlCommenterUtil {
17+
final class SqlCommenterUtil {
2118

2219
/**
23-
* Append comment containing tracing information at the end of the query. See <a
20+
* Append comment containing tracing information to the query. See <a
2421
* href="https://google.github.io/sqlcommenter/spec/">sqlcommenter</a> for the description of the
2522
* algorithm.
2623
*/
27-
public static String processQuery(String query) {
24+
public static String processQuery(String query, TextMapPropagator propagator, boolean prepend) {
2825
if (!Span.current().getSpanContext().isValid()) {
2926
return query;
3027
}
@@ -33,51 +30,53 @@ public static String processQuery(String query) {
3330
return query;
3431
}
3532

36-
class State {
37-
@Nullable String traceparent;
38-
@Nullable String tracestate;
39-
}
33+
Map<String, String> state = new LinkedHashMap<>();
34+
propagator.inject(
35+
Context.current(),
36+
state,
37+
(carrier, key, value) -> {
38+
if (carrier == null) {
39+
return;
40+
}
41+
carrier.put(key, value);
42+
});
4043

41-
State state = new State();
44+
if (state.isEmpty()) {
45+
return query;
46+
}
4247

43-
W3CTraceContextPropagator.getInstance()
44-
.inject(
45-
Context.current(),
46-
state,
47-
(carrier, key, value) -> {
48-
if (carrier == null) {
49-
return;
50-
}
51-
if ("traceparent".equals(key)) {
52-
carrier.traceparent = value;
53-
} else if ("tracestate".equals(key)) {
54-
carrier.tracestate = value;
55-
}
56-
});
48+
StringBuilder stringBuilder = new StringBuilder("/*");
5749
try {
58-
// we know that the traceparent doesn't contain anything that needs to be encoded
59-
query += " /*traceparent='" + state.traceparent + "'";
60-
if (state.tracestate != null) {
61-
query += ", tracestate=" + serialize(state.tracestate);
50+
for (Iterator<Map.Entry<String, String>> iterator = state.entrySet().iterator();
51+
iterator.hasNext(); ) {
52+
Map.Entry<String, String> entry = iterator.next();
53+
stringBuilder
54+
.append(serialize(entry.getKey()))
55+
.append("='")
56+
.append(serialize(entry.getValue()))
57+
.append("'");
58+
if (iterator.hasNext()) {
59+
stringBuilder.append(", ");
60+
}
6261
}
63-
query += "*/";
6462
} catch (UnsupportedEncodingException exception) {
6563
// this exception should never happen as UTF-8 encoding is always available
6664
}
67-
return query;
65+
stringBuilder.append("*/");
66+
67+
return prepend ? stringBuilder + " " + query : query + " " + stringBuilder;
6868
}
6969

7070
private static boolean containsSqlComment(String query) {
7171
return query.contains("--") || query.contains("/*");
7272
}
7373

7474
private static String serialize(String value) throws UnsupportedEncodingException {
75-
// specification requires percent encoding, here we use the java build in url encoder that
75+
// specification requires percent encoding, here we use the java built in url encoder that
7676
// encodes space as '+' instead of '%20' as required
77-
String result = URLEncoder.encode(value, "UTF-8").replace("+", "%20");
7877
// specification requires escaping ' with a backslash, we skip this because URLEncoder already
7978
// encodes the '
80-
return "'" + result + "'";
79+
return URLEncoder.encode(value, "UTF-8").replace("+", "%20");
8180
}
8281

8382
private SqlCommenterUtil() {}

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
import io.opentelemetry.api.trace.SpanContext;
1212
import io.opentelemetry.api.trace.TraceFlags;
1313
import io.opentelemetry.api.trace.TraceState;
14+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
1415
import io.opentelemetry.context.Context;
1516
import io.opentelemetry.context.Scope;
17+
import io.opentelemetry.context.propagation.TextMapPropagator;
1618
import org.junit.jupiter.params.ParameterizedTest;
1719
import org.junit.jupiter.params.provider.ValueSource;
20+
import org.junitpioneer.jupiter.cartesian.CartesianTest;
1821

1922
class SqlCommenterUtilTest {
23+
private static final TextMapPropagator propagator = W3CTraceContextPropagator.getInstance();
2024

2125
@ParameterizedTest
2226
@ValueSource(strings = {"SELECT /**/ 1", "SELECT 1 --", "SELECT '/*'"})
@@ -32,13 +36,14 @@ void skipQueriesWithComments(String query) {
3236
TraceState.getDefault())));
3337

3438
try (Scope ignore = parent.makeCurrent()) {
35-
assertThat(SqlCommenterUtil.processQuery(query)).isEqualTo(query);
39+
assertThat(SqlCommenterUtil.processQuery(query, propagator, false)).isEqualTo(query);
3640
}
3741
}
3842

39-
@ParameterizedTest
40-
@ValueSource(booleans = {true, false})
41-
void sqlCommenter(boolean hasTraceState) {
43+
@CartesianTest
44+
void sqlCommenter(
45+
@CartesianTest.Values(booleans = {true, false}) boolean hasTraceState,
46+
@CartesianTest.Values(booleans = {true, false}) boolean prepend) {
4247
TraceState state =
4348
hasTraceState ? TraceState.builder().put("test", "test'").build() : TraceState.getDefault();
4449
Context parent =
@@ -52,11 +57,12 @@ void sqlCommenter(boolean hasTraceState) {
5257
state)));
5358

5459
try (Scope ignore = parent.makeCurrent()) {
55-
assertThat(SqlCommenterUtil.processQuery("SELECT 1"))
56-
.isEqualTo(
57-
hasTraceState
58-
? "SELECT 1 /*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01', tracestate='test%3Dtest%27'*/"
59-
: "SELECT 1 /*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01'*/");
60+
String fragment =
61+
hasTraceState
62+
? "/*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01', tracestate='test%3Dtest%27'*/"
63+
: "/*traceparent='00-ff01020304050600ff0a0b0c0d0e0f00-090a0b0c0d0e0f00-01'*/";
64+
assertThat(SqlCommenterUtil.processQuery("SELECT 1", propagator, prepend))
65+
.isEqualTo(prepend ? fragment + " SELECT 1" : "SELECT 1 " + fragment);
6066
}
6167
}
6268
}

instrumentation/jdbc/javaagent/build.gradle.kts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,17 @@ tasks {
6363
}
6464

6565
val testSqlCommenter by registering(Test::class) {
66+
testClassesDirs = sourceSets.test.get().output.classesDirs
67+
classpath = sourceSets.test.get().runtimeClasspath
68+
6669
filter {
6770
includeTestsMatching("SqlCommenterTest")
6871
}
6972
include("**/SqlCommenterTest.*")
70-
jvmArgs("-Dotel.instrumentation.jdbc.experimental.sqlcommenter.enabled=true")
73+
// This property is read in TestAgentSqlCommenterCustomizer, we use it instead of the
74+
// otel.instrumentation.jdbc.experimental.sqlcommenter.enabled to test that the
75+
// SqlCommenterCustomizer is run.
76+
jvmArgs("-Dotel.testing.sqlcommenter.enabled=true")
7177
}
7278

7379
val testStableSemconv by registering(Test::class) {

instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/ConnectionInstrumentation.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ public Context storeInContext(Context context) {
9999

100100
@AssignReturned.ToArguments(@ToArgument(value = 0, index = 0))
101101
@Advice.OnMethodEnter(suppress = Throwable.class)
102-
public static Object[] processSql(@Advice.Argument(0) String sql) {
102+
public static Object[] processSql(
103+
@Advice.This Connection connection, @Advice.Argument(0) String sql) {
103104
Context context = Java8BytecodeBridge.currentContext();
104105
if (PrepareContext.get(context) == null) {
105106
// process sql only in the outermost prepare call and save the original sql in context
106-
String processSql = JdbcSingletons.processSql(sql);
107+
String processSql = JdbcSingletons.processSql(connection, sql, false);
107108
Scope scope = PrepareContext.init(context, sql).makeCurrent();
108109
return new Object[] {processSql, scope};
109110
} else {

0 commit comments

Comments
 (0)