Skip to content

Commit dc92478

Browse files
committed
Collect db transaction spans
1 parent 94bff8d commit dc92478

4 files changed

Lines changed: 247 additions & 13 deletions

File tree

sentry-jdbc/api/sentry-jdbc.api

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public final class io/sentry/jdbc/BuildConfig {
66
public final class io/sentry/jdbc/DatabaseUtils {
77
public fun <init> ()V
88
public static fun parse (Ljava/lang/String;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails;
9+
public static fun readFrom (Lcom/p6spy/engine/common/ConnectionInformation;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails;
910
public static fun readFrom (Lcom/p6spy/engine/common/StatementInformation;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails;
1011
}
1112

@@ -19,6 +20,12 @@ public class io/sentry/jdbc/SentryJdbcEventListener : com/p6spy/engine/event/Sim
1920
public fun <init> ()V
2021
public fun <init> (Lio/sentry/IScopes;)V
2122
public fun onAfterAnyExecute (Lcom/p6spy/engine/common/StatementInformation;JLjava/sql/SQLException;)V
23+
public fun onAfterCommit (Lcom/p6spy/engine/common/ConnectionInformation;JLjava/sql/SQLException;)V
24+
public fun onAfterRollback (Lcom/p6spy/engine/common/ConnectionInformation;JLjava/sql/SQLException;)V
25+
public fun onAfterSetAutoCommit (Lcom/p6spy/engine/common/ConnectionInformation;ZZLjava/sql/SQLException;)V
2226
public fun onBeforeAnyExecute (Lcom/p6spy/engine/common/StatementInformation;)V
27+
public fun onBeforeCommit (Lcom/p6spy/engine/common/ConnectionInformation;)V
28+
public fun onBeforeRollback (Lcom/p6spy/engine/common/ConnectionInformation;)V
29+
public fun onBeforeSetAutoCommit (Lcom/p6spy/engine/common/ConnectionInformation;ZZ)V
2330
}
2431

sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static DatabaseDetails readFrom(
2020

2121
final @Nullable ConnectionInformation connectionInformation =
2222
statementInformation.getConnectionInformation();
23+
return readFrom(connectionInformation);
24+
}
25+
26+
public static DatabaseDetails readFrom(
27+
final @Nullable ConnectionInformation connectionInformation) {
2328
if (connectionInformation == null) {
2429
return EMPTY;
2530
}

sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import static io.sentry.SpanDataConvention.DB_SYSTEM_KEY;
55

66
import com.jakewharton.nopen.annotation.Open;
7+
import com.p6spy.engine.common.ConnectionInformation;
78
import com.p6spy.engine.common.StatementInformation;
89
import com.p6spy.engine.event.SimpleJdbcEventListener;
910
import io.sentry.IScopes;
1011
import io.sentry.ISentryLifecycleToken;
1112
import io.sentry.ISpan;
1213
import io.sentry.ScopesAdapter;
1314
import io.sentry.SentryIntegrationPackageStorage;
14-
import io.sentry.Span;
1515
import io.sentry.SpanOptions;
1616
import io.sentry.SpanStatus;
1717
import io.sentry.util.AutoClosableReentrantLock;
@@ -20,12 +20,12 @@
2020
import org.jetbrains.annotations.NotNull;
2121
import org.jetbrains.annotations.Nullable;
2222

23-
/** P6Spy JDBC event listener that creates {@link Span}s around database queries. */
2423
@Open
2524
public class SentryJdbcEventListener extends SimpleJdbcEventListener {
2625
private static final String TRACE_ORIGIN = "auto.db.jdbc";
2726
private final @NotNull IScopes scopes;
28-
private static final @NotNull ThreadLocal<ISpan> CURRENT_SPAN = new ThreadLocal<>();
27+
private static final @NotNull ThreadLocal<ISpan> CURRENT_QUERY_SPAN = new ThreadLocal<>();
28+
private static final @NotNull ThreadLocal<ISpan> CURRENT_TRANSACTION_SPAN = new ThreadLocal<>();
2929

3030
private volatile @Nullable DatabaseUtils.DatabaseDetails cachedDatabaseDetails = null;
3131
protected final @NotNull AutoClosableReentrantLock databaseDetailsLock =
@@ -52,7 +52,7 @@ public void onBeforeAnyExecute(final @NotNull StatementInformation statementInfo
5252
final @NotNull SpanOptions spanOptions = new SpanOptions();
5353
spanOptions.setOrigin(TRACE_ORIGIN);
5454
final ISpan span = parent.startChild("db.query", statementInformation.getSql(), spanOptions);
55-
CURRENT_SPAN.set(span);
55+
CURRENT_QUERY_SPAN.set(span);
5656
}
5757
}
5858

@@ -61,10 +61,79 @@ public void onAfterAnyExecute(
6161
final @NotNull StatementInformation statementInformation,
6262
long timeElapsedNanos,
6363
final @Nullable SQLException e) {
64-
final ISpan span = CURRENT_SPAN.get();
64+
finishSpan(CURRENT_QUERY_SPAN, statementInformation.getConnectionInformation(), e);
65+
}
66+
67+
@Override
68+
public void onBeforeSetAutoCommit(
69+
final @NotNull ConnectionInformation connectionInformation,
70+
boolean newAutoCommit,
71+
boolean currentAutoCommit) {
72+
final boolean isSwitchingToManualCommit = !newAutoCommit && currentAutoCommit;
73+
if (isSwitchingToManualCommit) {
74+
startSpan(CURRENT_TRANSACTION_SPAN, "db.sql.transaction.begin", "BEGIN");
75+
}
76+
}
77+
78+
@Override
79+
public void onAfterSetAutoCommit(
80+
final @NotNull ConnectionInformation connectionInformation,
81+
final boolean newAutoCommit,
82+
final boolean oldAutoCommit,
83+
final @Nullable SQLException e) {
84+
final boolean isSwitchingToManualCommit = !newAutoCommit && oldAutoCommit;
85+
if (isSwitchingToManualCommit) {
86+
finishSpan(CURRENT_TRANSACTION_SPAN, connectionInformation, e);
87+
}
88+
}
89+
90+
@Override
91+
public void onBeforeCommit(final @NotNull ConnectionInformation connectionInformation) {
92+
startSpan(CURRENT_TRANSACTION_SPAN, "db.sql.transaction.commit", "COMMIT");
93+
}
94+
95+
@Override
96+
public void onAfterCommit(
97+
final @NotNull ConnectionInformation connectionInformation,
98+
final long timeElapsedNanos,
99+
final @Nullable SQLException e) {
100+
finishSpan(CURRENT_TRANSACTION_SPAN, connectionInformation, e);
101+
}
102+
103+
@Override
104+
public void onBeforeRollback(final @NotNull ConnectionInformation connectionInformation) {
105+
startSpan(CURRENT_TRANSACTION_SPAN, "db.sql.transaction.rollback", "ROLLBACK");
106+
}
107+
108+
@Override
109+
public void onAfterRollback(
110+
final @NotNull ConnectionInformation connectionInformation,
111+
final long timeElapsedNanos,
112+
final @Nullable SQLException e) {
113+
finishSpan(CURRENT_TRANSACTION_SPAN, connectionInformation, e);
114+
}
115+
116+
private void startSpan(
117+
final @NotNull ThreadLocal<ISpan> spanHolder,
118+
final @NotNull String operation,
119+
final @Nullable String description) {
120+
final @Nullable ISpan parent = scopes.getSpan();
121+
if (parent != null && !parent.isNoOp()) {
122+
final @NotNull SpanOptions spanOptions = new SpanOptions();
123+
spanOptions.setOrigin(TRACE_ORIGIN);
124+
final @NotNull ISpan span = parent.startChild(operation, description, spanOptions);
125+
spanHolder.set(span);
126+
}
127+
}
128+
129+
private void finishSpan(
130+
final @NotNull ThreadLocal<ISpan> spanHolder,
131+
final @Nullable ConnectionInformation connectionInformation,
132+
final @Nullable SQLException e) {
133+
final @Nullable ISpan span = spanHolder.get();
65134

66135
if (span != null) {
67-
applyDatabaseDetailsToSpan(statementInformation, span);
136+
applyDatabaseDetailsToSpan(connectionInformation, span);
68137

69138
if (e != null) {
70139
span.setThrowable(e);
@@ -73,7 +142,7 @@ public void onAfterAnyExecute(
73142
span.setStatus(SpanStatus.OK);
74143
}
75144
span.finish();
76-
CURRENT_SPAN.set(null);
145+
spanHolder.remove();
77146
}
78147
}
79148

@@ -82,9 +151,9 @@ private void addPackageAndIntegrationInfo() {
82151
}
83152

84153
private void applyDatabaseDetailsToSpan(
85-
final @NotNull StatementInformation statementInformation, final @NotNull ISpan span) {
154+
final @Nullable ConnectionInformation connectionInformation, final @NotNull ISpan span) {
86155
final @NotNull DatabaseUtils.DatabaseDetails databaseDetails =
87-
getOrComputeDatabaseDetails(statementInformation);
156+
getOrComputeDatabaseDetails(connectionInformation);
88157

89158
if (databaseDetails.getDbSystem() != null) {
90159
span.setData(DB_SYSTEM_KEY, databaseDetails.getDbSystem());
@@ -96,11 +165,11 @@ private void applyDatabaseDetailsToSpan(
96165
}
97166

98167
private @NotNull DatabaseUtils.DatabaseDetails getOrComputeDatabaseDetails(
99-
final @NotNull StatementInformation statementInformation) {
168+
final @Nullable ConnectionInformation connectionInformation) {
100169
if (cachedDatabaseDetails == null) {
101170
try (final @NotNull ISentryLifecycleToken ignored = databaseDetailsLock.acquire()) {
102171
if (cachedDatabaseDetails == null) {
103-
cachedDatabaseDetails = DatabaseUtils.readFrom(statementInformation);
172+
cachedDatabaseDetails = DatabaseUtils.readFrom(connectionInformation);
104173
}
105174
}
106175
}

sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.sentry.jdbc
22

3-
import com.p6spy.engine.common.StatementInformation
3+
import com.p6spy.engine.common.ConnectionInformation
44
import com.p6spy.engine.spy.P6DataSource
55
import io.sentry.IScopes
66
import io.sentry.SentryOptions
@@ -146,7 +146,7 @@ class SentryJdbcEventListenerTest {
146146
Mockito.mockStatic(DatabaseUtils::class.java).use { utils ->
147147
var invocationCount = 0
148148
utils
149-
.`when`<Any> { DatabaseUtils.readFrom(any<StatementInformation>()) }
149+
.`when`<Any> { DatabaseUtils.readFrom(any<ConnectionInformation>()) }
150150
.thenAnswer {
151151
invocationCount++
152152
DatabaseDetails("a", "b")
@@ -169,4 +169,157 @@ class SentryJdbcEventListenerTest {
169169
assertEquals(1, invocationCount)
170170
}
171171
}
172+
173+
@Test
174+
fun `creates span for commit`() {
175+
val sut = fixture.getSut()
176+
177+
sut.connection.use {
178+
it.autoCommit = false
179+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
180+
it.commit()
181+
}
182+
183+
val commitSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.commit" }
184+
assertEquals(1, commitSpans.size)
185+
assertEquals(SpanStatus.OK, commitSpans[0].status)
186+
assertEquals("auto.db.jdbc", commitSpans[0].spanContext.origin)
187+
}
188+
189+
@Test
190+
fun `creates span for rollback`() {
191+
val sut = fixture.getSut()
192+
193+
sut.connection.use {
194+
it.autoCommit = false
195+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
196+
it.rollback()
197+
}
198+
199+
val rollbackSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.rollback" }
200+
assertEquals(1, rollbackSpans.size)
201+
assertEquals(SpanStatus.OK, rollbackSpans[0].status)
202+
assertEquals("auto.db.jdbc", rollbackSpans[0].spanContext.origin)
203+
}
204+
205+
@Test
206+
fun `commit span has database details`() {
207+
val sut = fixture.getSut()
208+
209+
sut.connection.use {
210+
it.autoCommit = false
211+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
212+
it.commit()
213+
}
214+
215+
val commitSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.commit" }
216+
assertEquals(1, commitSpans.size)
217+
assertEquals("hsqldb", commitSpans[0].data[DB_SYSTEM_KEY])
218+
assertEquals("testdb", commitSpans[0].data[DB_NAME_KEY])
219+
}
220+
221+
@Test
222+
fun `rollback span has database details`() {
223+
val sut = fixture.getSut()
224+
225+
sut.connection.use {
226+
it.autoCommit = false
227+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
228+
it.rollback()
229+
}
230+
231+
val rollbackSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.rollback" }
232+
assertEquals(1, rollbackSpans.size)
233+
assertEquals("hsqldb", rollbackSpans[0].data[DB_SYSTEM_KEY])
234+
assertEquals("testdb", rollbackSpans[0].data[DB_NAME_KEY])
235+
}
236+
237+
@Test
238+
fun `does not create commit span when there is no running transaction`() {
239+
val sut = fixture.getSut(withRunningTransaction = false)
240+
241+
sut.connection.use {
242+
it.autoCommit = false
243+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
244+
it.commit()
245+
}
246+
247+
val commitSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.commit" }
248+
assertTrue(commitSpans.isEmpty())
249+
}
250+
251+
@Test
252+
fun `does not create rollback span when there is no running transaction`() {
253+
val sut = fixture.getSut(withRunningTransaction = false)
254+
255+
sut.connection.use {
256+
it.autoCommit = false
257+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
258+
it.rollback()
259+
}
260+
261+
val rollbackSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.rollback" }
262+
assertTrue(rollbackSpans.isEmpty())
263+
}
264+
265+
@Test
266+
fun `creates span for transaction begin when setAutoCommit false`() {
267+
val sut = fixture.getSut()
268+
269+
sut.connection.use {
270+
it.autoCommit = false
271+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
272+
it.commit()
273+
}
274+
275+
val beginSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.begin" }
276+
assertEquals(1, beginSpans.size)
277+
assertEquals(SpanStatus.OK, beginSpans[0].status)
278+
assertEquals("auto.db.jdbc", beginSpans[0].spanContext.origin)
279+
}
280+
281+
@Test
282+
fun `transaction begin span has database details`() {
283+
val sut = fixture.getSut()
284+
285+
sut.connection.use {
286+
it.autoCommit = false
287+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
288+
it.commit()
289+
}
290+
291+
val beginSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.begin" }
292+
assertEquals(1, beginSpans.size)
293+
assertEquals("hsqldb", beginSpans[0].data[DB_SYSTEM_KEY])
294+
assertEquals("testdb", beginSpans[0].data[DB_NAME_KEY])
295+
}
296+
297+
@Test
298+
fun `does not create begin span when already in manual commit mode`() {
299+
val sut = fixture.getSut()
300+
301+
sut.connection.use {
302+
it.autoCommit = false
303+
it.autoCommit = false // setting again should not create another span
304+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
305+
it.commit()
306+
}
307+
308+
val beginSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.begin" }
309+
assertEquals(1, beginSpans.size)
310+
}
311+
312+
@Test
313+
fun `does not create begin span when there is no running transaction`() {
314+
val sut = fixture.getSut(withRunningTransaction = false)
315+
316+
sut.connection.use {
317+
it.autoCommit = false
318+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
319+
it.commit()
320+
}
321+
322+
val beginSpans = fixture.tx.children.filter { it.operation == "db.sql.transaction.begin" }
323+
assertTrue(beginSpans.isEmpty())
324+
}
172325
}

0 commit comments

Comments
 (0)