Skip to content

Commit 97e5260

Browse files
stillyalaurit
andauthored
Added instrumentation for transaction commit/rollback in jdbc (#13709)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 4a6921c commit 97e5260

File tree

20 files changed

+520
-102
lines changed

20 files changed

+520
-102
lines changed

instrumentation/jdbc/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Settings for the JDBC instrumentation
22

3-
| System property | Type | Default | Description |
4-
|---------------------------------------------------------|---------|---------|----------------------------------------|
5-
| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. |
3+
| System property | Type | Default | Description |
4+
|--------------------------------------------------------------|---------|---------|------------------------------------------------------------------------------------------|
5+
| `otel.instrumentation.jdbc.statement-sanitizer.enabled` | Boolean | `true` | Enables the DB statement sanitization. |
6+
| `otel.instrumentation.jdbc.experimental.transaction.enabled` | Boolean | `false` | Enables experimental instrumentation to create spans for COMMIT and ROLLBACK operations. |

instrumentation/jdbc/javaagent/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,9 @@ tasks {
9191
dependsOn(testSlickStableSemconv)
9292
}
9393
}
94+
95+
tasks {
96+
withType<Test>().configureEach {
97+
jvmArgs("-Dotel.instrumentation.jdbc.experimental.transaction.enabled=true")
98+
}
99+
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,27 @@
55

66
package io.opentelemetry.javaagent.instrumentation.jdbc;
77

8+
import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
89
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
910
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
11+
import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcSingletons.transactionInstrumenter;
12+
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
1013
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
1114
import static net.bytebuddy.matcher.ElementMatchers.named;
15+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
1216
import static net.bytebuddy.matcher.ElementMatchers.returns;
1317
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
18+
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;
1419

20+
import io.opentelemetry.context.Context;
21+
import io.opentelemetry.context.Scope;
22+
import io.opentelemetry.instrumentation.jdbc.internal.DbRequest;
1523
import io.opentelemetry.instrumentation.jdbc.internal.JdbcData;
1624
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1725
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
26+
import java.sql.Connection;
1827
import java.sql.PreparedStatement;
28+
import java.util.Locale;
1929
import net.bytebuddy.asm.Advice;
2030
import net.bytebuddy.description.type.TypeDescription;
2131
import net.bytebuddy.matcher.ElementMatcher;
@@ -40,6 +50,9 @@ public void transform(TypeTransformer transformer) {
4050
// Also include CallableStatement, which is a sub type of PreparedStatement
4151
.and(returns(implementsInterface(named("java.sql.PreparedStatement")))),
4252
ConnectionInstrumentation.class.getName() + "$PrepareAdvice");
53+
transformer.applyAdviceToMethod(
54+
namedOneOf("commit", "rollback").and(takesNoArguments()).and(isPublic()),
55+
ConnectionInstrumentation.class.getName() + "$TransactionAdvice");
4356
}
4457

4558
@SuppressWarnings("unused")
@@ -51,4 +64,39 @@ public static void addDbInfo(
5164
JdbcData.preparedStatement.set(statement, sql);
5265
}
5366
}
67+
68+
@SuppressWarnings("unused")
69+
public static class TransactionAdvice {
70+
71+
@Advice.OnMethodEnter(suppress = Throwable.class)
72+
public static void onEnter(
73+
@Advice.This Connection connection,
74+
@Advice.Origin("#m") String methodName,
75+
@Advice.Local("otelContext") Context context,
76+
@Advice.Local("otelScope") Scope scope) {
77+
Context parentContext = currentContext();
78+
DbRequest request =
79+
DbRequest.createTransaction(connection, methodName.toUpperCase(Locale.ROOT));
80+
81+
if (request == null || !transactionInstrumenter().shouldStart(parentContext, request)) {
82+
return;
83+
}
84+
85+
context = transactionInstrumenter().start(parentContext, request);
86+
scope = context.makeCurrent();
87+
}
88+
89+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
90+
public static void stopSpan(
91+
@Advice.Thrown Throwable throwable,
92+
@Advice.Local("otelRequest") DbRequest request,
93+
@Advice.Local("otelContext") Context context,
94+
@Advice.Local("otelScope") Scope scope) {
95+
if (scope == null) {
96+
return;
97+
}
98+
scope.close();
99+
transactionInstrumenter().end(context, request, null, throwable);
100+
}
101+
}
54102
}

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

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,50 @@
88
import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createDataSourceInstrumenter;
99

1010
import io.opentelemetry.api.GlobalOpenTelemetry;
11-
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
12-
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
13-
import io.opentelemetry.instrumentation.api.incubator.semconv.db.SqlClientAttributesExtractor;
1411
import io.opentelemetry.instrumentation.api.incubator.semconv.net.PeerServiceAttributesExtractor;
12+
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
1513
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
16-
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
17-
import io.opentelemetry.instrumentation.api.semconv.network.ServerAttributesExtractor;
1814
import io.opentelemetry.instrumentation.jdbc.internal.DbRequest;
19-
import io.opentelemetry.instrumentation.jdbc.internal.JdbcAttributesGetter;
15+
import io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory;
2016
import io.opentelemetry.instrumentation.jdbc.internal.JdbcNetworkAttributesGetter;
2117
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
2218
import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig;
2319
import io.opentelemetry.javaagent.bootstrap.jdbc.DbInfo;
20+
import java.util.Collections;
2421
import javax.sql.DataSource;
2522

2623
public final class JdbcSingletons {
27-
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jdbc";
28-
2924
private static final Instrumenter<DbRequest, Void> STATEMENT_INSTRUMENTER;
25+
private static final Instrumenter<DbRequest, Void> TRANSACTION_INSTRUMENTER;
3026
public static final Instrumenter<DataSource, DbInfo> DATASOURCE_INSTRUMENTER =
3127
createDataSourceInstrumenter(GlobalOpenTelemetry.get(), true);
3228

3329
static {
34-
JdbcAttributesGetter dbAttributesGetter = new JdbcAttributesGetter();
3530
JdbcNetworkAttributesGetter netAttributesGetter = new JdbcNetworkAttributesGetter();
31+
AttributesExtractor<DbRequest, Void> peerServiceExtractor =
32+
PeerServiceAttributesExtractor.create(
33+
netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver());
3634

3735
STATEMENT_INSTRUMENTER =
38-
Instrumenter.<DbRequest, Void>builder(
39-
GlobalOpenTelemetry.get(),
40-
INSTRUMENTATION_NAME,
41-
DbClientSpanNameExtractor.create(dbAttributesGetter))
42-
.addAttributesExtractor(
43-
SqlClientAttributesExtractor.builder(dbAttributesGetter)
44-
.setStatementSanitizationEnabled(
45-
AgentInstrumentationConfig.get()
46-
.getBoolean(
47-
"otel.instrumentation.jdbc.statement-sanitizer.enabled",
48-
AgentCommonConfig.get().isStatementSanitizationEnabled()))
49-
.build())
50-
.addAttributesExtractor(ServerAttributesExtractor.create(netAttributesGetter))
51-
.addAttributesExtractor(
52-
PeerServiceAttributesExtractor.create(
53-
netAttributesGetter, AgentCommonConfig.get().getPeerServiceResolver()))
54-
.addOperationMetrics(DbClientMetrics.get())
55-
.buildInstrumenter(SpanKindExtractor.alwaysClient());
36+
JdbcInstrumenterFactory.createStatementInstrumenter(
37+
GlobalOpenTelemetry.get(),
38+
Collections.singletonList(peerServiceExtractor),
39+
true,
40+
AgentInstrumentationConfig.get()
41+
.getBoolean(
42+
"otel.instrumentation.jdbc.statement-sanitizer.enabled",
43+
AgentCommonConfig.get().isStatementSanitizationEnabled()));
44+
45+
TRANSACTION_INSTRUMENTER =
46+
JdbcInstrumenterFactory.createTransactionInstrumenter(
47+
GlobalOpenTelemetry.get(),
48+
Collections.singletonList(peerServiceExtractor),
49+
AgentInstrumentationConfig.get()
50+
.getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false));
51+
}
52+
53+
public static Instrumenter<DbRequest, Void> transactionInstrumenter() {
54+
return TRANSACTION_INSTRUMENTER;
5655
}
5756

5857
public static Instrumenter<DbRequest, Void> statementInstrumenter() {

instrumentation/jdbc/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/jdbc/test/JdbcInstrumentationTest.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,4 +1632,128 @@ void testPreparedBatch(String system, Connection connection, String username, St
16321632
DB_OPERATION_BATCH_SIZE,
16331633
emitStableDatabaseSemconv() ? 2L : null))));
16341634
}
1635+
1636+
@ParameterizedTest
1637+
@MethodSource("transactionOperationsStream")
1638+
void testCommitTransaction(String system, Connection connection, String username, String url)
1639+
throws SQLException {
1640+
1641+
String tableName = "TXN_COMMIT_TEST_" + system.toUpperCase(Locale.ROOT);
1642+
Statement createTable = connection.createStatement();
1643+
createTable.execute("CREATE TABLE " + tableName + " (id INTEGER not NULL, PRIMARY KEY ( id ))");
1644+
cleanup.deferCleanup(createTable);
1645+
1646+
connection.setAutoCommit(false);
1647+
1648+
testing.waitForTraces(1);
1649+
testing.clearData();
1650+
1651+
Statement insertStatement = connection.createStatement();
1652+
cleanup.deferCleanup(insertStatement);
1653+
1654+
testing.runWithSpan(
1655+
"parent",
1656+
() -> {
1657+
insertStatement.executeUpdate("INSERT INTO " + tableName + " VALUES(1)");
1658+
connection.commit();
1659+
});
1660+
1661+
testing.waitAndAssertTraces(
1662+
trace ->
1663+
trace.hasSpansSatisfyingExactly(
1664+
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
1665+
span ->
1666+
span.hasName("INSERT jdbcunittest." + tableName)
1667+
.hasKind(SpanKind.CLIENT)
1668+
.hasParent(trace.getSpan(0))
1669+
.hasAttributesSatisfyingExactly(
1670+
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)),
1671+
equalTo(maybeStable(DB_NAME), dbNameLower),
1672+
equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username),
1673+
equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url),
1674+
equalTo(
1675+
maybeStable(DB_STATEMENT),
1676+
"INSERT INTO " + tableName + " VALUES(?)"),
1677+
equalTo(maybeStable(DB_OPERATION), "INSERT"),
1678+
equalTo(maybeStable(DB_SQL_TABLE), tableName)),
1679+
span ->
1680+
span.hasName("COMMIT")
1681+
.hasKind(SpanKind.CLIENT)
1682+
.hasParent(trace.getSpan(0))
1683+
.hasAttributesSatisfyingExactly(
1684+
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)),
1685+
equalTo(maybeStable(DB_NAME), dbNameLower),
1686+
equalTo(maybeStable(DB_OPERATION), "COMMIT"),
1687+
equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username),
1688+
equalTo(
1689+
DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url))));
1690+
}
1691+
1692+
@ParameterizedTest
1693+
@MethodSource("transactionOperationsStream")
1694+
void testRollbackTransaction(String system, Connection connection, String username, String url)
1695+
throws SQLException {
1696+
1697+
String tableName = "TXN_ROLLBACK_TEST_" + system.toUpperCase(Locale.ROOT);
1698+
Statement createTable = connection.createStatement();
1699+
createTable.execute("CREATE TABLE " + tableName + " (id INTEGER not NULL, PRIMARY KEY ( id ))");
1700+
cleanup.deferCleanup(createTable);
1701+
1702+
connection.setAutoCommit(false);
1703+
1704+
testing.waitForTraces(1);
1705+
testing.clearData();
1706+
1707+
Statement insertStatement = connection.createStatement();
1708+
cleanup.deferCleanup(insertStatement);
1709+
1710+
testing.runWithSpan(
1711+
"parent",
1712+
() -> {
1713+
insertStatement.executeUpdate("INSERT INTO " + tableName + " VALUES(1)");
1714+
connection.rollback();
1715+
});
1716+
1717+
testing.waitAndAssertTraces(
1718+
trace ->
1719+
trace.hasSpansSatisfyingExactly(
1720+
span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
1721+
span ->
1722+
span.hasName("INSERT jdbcunittest." + tableName)
1723+
.hasKind(SpanKind.CLIENT)
1724+
.hasParent(trace.getSpan(0))
1725+
.hasAttributesSatisfyingExactly(
1726+
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)),
1727+
equalTo(maybeStable(DB_NAME), dbNameLower),
1728+
equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username),
1729+
equalTo(DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url),
1730+
equalTo(
1731+
maybeStable(DB_STATEMENT),
1732+
"INSERT INTO " + tableName + " VALUES(?)"),
1733+
equalTo(maybeStable(DB_OPERATION), "INSERT"),
1734+
equalTo(maybeStable(DB_SQL_TABLE), tableName)),
1735+
span ->
1736+
span.hasName("ROLLBACK")
1737+
.hasKind(SpanKind.CLIENT)
1738+
.hasParent(trace.getSpan(0))
1739+
.hasAttributesSatisfyingExactly(
1740+
equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(system)),
1741+
equalTo(maybeStable(DB_NAME), dbNameLower),
1742+
equalTo(maybeStable(DB_OPERATION), "ROLLBACK"),
1743+
equalTo(DB_USER, emitStableDatabaseSemconv() ? null : username),
1744+
equalTo(
1745+
DB_CONNECTION_STRING, emitStableDatabaseSemconv() ? null : url))));
1746+
}
1747+
1748+
static Stream<Arguments> transactionOperationsStream() throws SQLException {
1749+
return Stream.of(
1750+
Arguments.of("h2", new org.h2.Driver().connect(jdbcUrls.get("h2"), null), null, "h2:mem:"),
1751+
Arguments.of(
1752+
"derby",
1753+
new EmbeddedDriver().connect(jdbcUrls.get("derby"), null),
1754+
"APP",
1755+
"derby:memory:"),
1756+
Arguments.of(
1757+
"hsqldb", new JDBCDriver().connect(jdbcUrls.get("hsqldb"), null), "SA", "hsqldb:mem:"));
1758+
}
16351759
}

instrumentation/jdbc/library/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ tasks {
5959
dependsOn(testStableSemconv)
6060
}
6161
}
62+
63+
tasks {
64+
withType<Test>().configureEach {
65+
jvmArgs("-Dotel.instrumentation.jdbc.experimental.transaction.enabled=true")
66+
}
67+
}

instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/OpenTelemetryDriver.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ public Connection connect(String url, Properties info) throws SQLException {
244244

245245
Instrumenter<DbRequest, Void> statementInstrumenter =
246246
JdbcInstrumenterFactory.createStatementInstrumenter(openTelemetry);
247-
return OpenTelemetryConnection.create(connection, dbInfo, statementInstrumenter);
247+
Instrumenter<DbRequest, Void> transactionInstrumenter =
248+
JdbcInstrumenterFactory.createTransactionInstrumenter(openTelemetry);
249+
return OpenTelemetryConnection.create(
250+
connection, dbInfo, statementInstrumenter, transactionInstrumenter);
248251
}
249252

250253
@Override

instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetry.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,22 @@ public static JdbcTelemetryBuilder builder(OpenTelemetry openTelemetry) {
2626

2727
private final Instrumenter<DataSource, DbInfo> dataSourceInstrumenter;
2828
private final Instrumenter<DbRequest, Void> statementInstrumenter;
29+
private final Instrumenter<DbRequest, Void> transactionInstrumenter;
2930

3031
JdbcTelemetry(
3132
Instrumenter<DataSource, DbInfo> dataSourceInstrumenter,
32-
Instrumenter<DbRequest, Void> statementInstrumenter) {
33+
Instrumenter<DbRequest, Void> statementInstrumenter,
34+
Instrumenter<DbRequest, Void> transactionInstrumenter) {
3335
this.dataSourceInstrumenter = dataSourceInstrumenter;
3436
this.statementInstrumenter = statementInstrumenter;
37+
this.transactionInstrumenter = transactionInstrumenter;
3538
}
3639

3740
public DataSource wrap(DataSource dataSource) {
3841
return new OpenTelemetryDataSource(
39-
dataSource, this.dataSourceInstrumenter, this.statementInstrumenter);
42+
dataSource,
43+
this.dataSourceInstrumenter,
44+
this.statementInstrumenter,
45+
this.transactionInstrumenter);
4046
}
4147
}

instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/datasource/JdbcTelemetryBuilder.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public final class JdbcTelemetryBuilder {
1616
private boolean dataSourceInstrumenterEnabled = true;
1717
private boolean statementInstrumenterEnabled = true;
1818
private boolean statementSanitizationEnabled = true;
19+
private boolean transactionInstrumenterEnabled = false;
1920

2021
JdbcTelemetryBuilder(OpenTelemetry openTelemetry) {
2122
this.openTelemetry = openTelemetry;
@@ -42,12 +43,21 @@ public JdbcTelemetryBuilder setStatementSanitizationEnabled(boolean enabled) {
4243
return this;
4344
}
4445

46+
/** Configures whether spans are created for JDBC Transactions. Disabled by default. */
47+
@CanIgnoreReturnValue
48+
public JdbcTelemetryBuilder setTransactionInstrumenterEnabled(boolean enabled) {
49+
this.transactionInstrumenterEnabled = enabled;
50+
return this;
51+
}
52+
4553
/** Returns a new {@link JdbcTelemetry} with the settings of this {@link JdbcTelemetryBuilder}. */
4654
public JdbcTelemetry build() {
4755
return new JdbcTelemetry(
4856
JdbcInstrumenterFactory.createDataSourceInstrumenter(
4957
openTelemetry, dataSourceInstrumenterEnabled),
5058
JdbcInstrumenterFactory.createStatementInstrumenter(
51-
openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled));
59+
openTelemetry, statementInstrumenterEnabled, statementSanitizationEnabled),
60+
JdbcInstrumenterFactory.createTransactionInstrumenter(
61+
openTelemetry, transactionInstrumenterEnabled));
5262
}
5363
}

0 commit comments

Comments
 (0)