Skip to content

Commit 972a01b

Browse files
Database queries tracing with datasource-proxy. (#1095)
1 parent a4949a2 commit 972a01b

File tree

28 files changed

+692
-28
lines changed

28 files changed

+692
-28
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ This release brings the Sentry Performance feature to Java SDK, Spring, Spring B
166166
* Enhancement: Add the ability to register multiple OptionsConfiguration beans (#1093)
167167
* Fix: Append DebugImage list if event already has it
168168
* Fix: Sort breadcrumbs by Date if there are breadcrumbs already in the event
169+
* Feat: Database query tracing with datasource-proxy (#1095)
169170

170171
# 4.0.0-alpha.1
171172

buildSrc/src/main/java/Config.kt

+9
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ object Config {
6464
val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
6565
val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
6666
val springBootStarterAop = "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
67+
val springBootStarterJdbc = "org.springframework.boot:spring-boot-starter-jdbc:$springBootVersion"
6768
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
6869

6970
val springWeb = "org.springframework:spring-webmvc"
@@ -73,6 +74,9 @@ object Config {
7374
val aspectj = "org.aspectj:aspectjweaver"
7475
val servletApi = "javax.servlet:javax.servlet-api"
7576

77+
val datasourceProxy = "net.ttddyy:datasource-proxy:1.7"
78+
val p6spy = "p6spy:p6spy:3.9.1"
79+
7680
val apacheHttpClient = "org.apache.httpcomponents.client5:httpclient5:5.0.3"
7781
val apacheHttpCore = "org.apache.httpcomponents.core5:httpcore5:5.0.3"
7882
val apacheHttpCoreH2 = "org.apache.httpcomponents.core5:httpcore5-h2:5.0.3"
@@ -101,6 +105,7 @@ object Config {
101105
val mockitoInline = "org.mockito:mockito-inline:3.8.0"
102106
val awaitility = "org.awaitility:awaitility-kotlin:4.0.3"
103107
val mockWebserver = "com.squareup.okhttp3:mockwebserver:4.9.0"
108+
val hsqldb = "org.hsqldb:hsqldb:2.5.1"
104109
}
105110

106111
object QualityPlugins {
@@ -149,4 +154,8 @@ object Config {
149154
val nativeBundlePlugin = "com.ydq.android.gradle.build.tool:nativeBundle:1.0.7"
150155
val nativeBundleExport = "com.ydq.android.gradle.native-aar.export"
151156
}
157+
158+
object SamplesLibs {
159+
val datasourceProxySpringBootStarter = "com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.7.0"
160+
}
152161
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public class io/sentry/dsproxy/SentryQueryExecutionListener : net/ttddyy/dsproxy/listener/QueryExecutionListener {
2+
public fun <init> ()V
3+
public fun <init> (Lio/sentry/IHub;)V
4+
public fun afterQuery (Lnet/ttddyy/dsproxy/ExecutionInfo;Ljava/util/List;)V
5+
public fun beforeQuery (Lnet/ttddyy/dsproxy/ExecutionInfo;Ljava/util/List;)V
6+
}
7+
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
3+
plugins {
4+
`java-library`
5+
kotlin("jvm")
6+
jacoco
7+
id(Config.QualityPlugins.errorProne)
8+
id(Config.QualityPlugins.gradleVersions)
9+
}
10+
11+
configure<JavaPluginConvention> {
12+
sourceCompatibility = JavaVersion.VERSION_1_8
13+
targetCompatibility = JavaVersion.VERSION_1_8
14+
}
15+
16+
tasks.withType<KotlinCompile>().configureEach {
17+
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
18+
}
19+
20+
dependencies {
21+
api(project(":sentry"))
22+
api(Config.Libs.datasourceProxy)
23+
24+
compileOnly(Config.CompileOnly.nopen)
25+
errorprone(Config.CompileOnly.nopenChecker)
26+
errorprone(Config.CompileOnly.errorprone)
27+
errorproneJavac(Config.CompileOnly.errorProneJavac8)
28+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
29+
30+
// tests
31+
testImplementation(project(":sentry-test-support"))
32+
testImplementation(kotlin(Config.kotlinStdLib))
33+
testImplementation(Config.TestLibs.kotlinTestJunit)
34+
testImplementation(Config.TestLibs.mockitoKotlin)
35+
testImplementation(Config.TestLibs.awaitility)
36+
testImplementation(Config.TestLibs.hsqldb)
37+
}
38+
39+
configure<SourceSetContainer> {
40+
test {
41+
java.srcDir("src/test/java")
42+
}
43+
}
44+
45+
jacoco {
46+
toolVersion = Config.QualityPlugins.Jacoco.version
47+
}
48+
49+
tasks.jacocoTestReport {
50+
reports {
51+
xml.isEnabled = true
52+
html.isEnabled = false
53+
}
54+
}
55+
56+
tasks {
57+
jacocoTestCoverageVerification {
58+
violationRules {
59+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
60+
}
61+
}
62+
check {
63+
dependsOn(jacocoTestCoverageVerification)
64+
dependsOn(jacocoTestReport)
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.sentry.dsproxy;
2+
3+
import com.jakewharton.nopen.annotation.Open;
4+
import io.sentry.HubAdapter;
5+
import io.sentry.IHub;
6+
import io.sentry.ISpan;
7+
import io.sentry.Span;
8+
import io.sentry.SpanStatus;
9+
import io.sentry.util.Objects;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.WeakHashMap;
14+
import java.util.stream.Collectors;
15+
import net.ttddyy.dsproxy.ExecutionInfo;
16+
import net.ttddyy.dsproxy.QueryInfo;
17+
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
18+
import org.jetbrains.annotations.NotNull;
19+
20+
/** datasource-proxy query execution listener that creates {@link Span}s around database queries. */
21+
@Open
22+
public class SentryQueryExecutionListener implements QueryExecutionListener {
23+
private final @NotNull IHub hub;
24+
private final @NotNull Map<String, ISpan> spans =
25+
Collections.synchronizedMap(new WeakHashMap<>());
26+
27+
public SentryQueryExecutionListener(final @NotNull IHub hub) {
28+
this.hub = Objects.requireNonNull(hub, "hub is required");
29+
}
30+
31+
public SentryQueryExecutionListener() {
32+
this(HubAdapter.getInstance());
33+
}
34+
35+
@Override
36+
public void beforeQuery(
37+
final @NotNull ExecutionInfo execInfo, final @NotNull List<QueryInfo> queryInfoList) {
38+
final ISpan parent = hub.getSpan();
39+
if (parent != null) {
40+
final ISpan span = parent.startChild("db", resolveSpanDescription(queryInfoList));
41+
spans.put(execInfo.getConnectionId(), span);
42+
}
43+
}
44+
45+
private @NotNull String resolveSpanDescription(final @NotNull List<QueryInfo> queryInfoList) {
46+
return queryInfoList.stream().map(QueryInfo::getQuery).collect(Collectors.joining(","));
47+
}
48+
49+
@Override
50+
public void afterQuery(
51+
final @NotNull ExecutionInfo execInfo, final @NotNull List<QueryInfo> queryInfoList) {
52+
final ISpan span = spans.get(execInfo.getConnectionId());
53+
if (span != null) {
54+
if (execInfo.getThrowable() != null) {
55+
span.setThrowable(execInfo.getThrowable());
56+
span.setStatus(SpanStatus.INTERNAL_ERROR);
57+
} else {
58+
span.setStatus(SpanStatus.OK);
59+
}
60+
span.finish();
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.sentry.dsproxy
2+
3+
import com.nhaarman.mockitokotlin2.mock
4+
import com.nhaarman.mockitokotlin2.whenever
5+
import io.sentry.IHub
6+
import io.sentry.SentryTracer
7+
import io.sentry.SpanStatus
8+
import io.sentry.TransactionContext
9+
import javax.sql.DataSource
10+
import kotlin.test.AfterTest
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
import kotlin.test.assertTrue
14+
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder
15+
import org.hsqldb.jdbc.JDBCDataSource
16+
17+
class SentryQueryExecutionListenerTest {
18+
19+
class Fixture {
20+
private val hub = mock<IHub>()
21+
val tx = SentryTracer(TransactionContext("name", "op"), hub)
22+
val actualDataSource = JDBCDataSource()
23+
24+
fun getSut(withRunningTransaction: Boolean = true): DataSource {
25+
if (withRunningTransaction) {
26+
whenever(hub.span).thenReturn(tx)
27+
}
28+
actualDataSource.setURL("jdbc:hsqldb:mem:testdb")
29+
30+
actualDataSource.connection.use {
31+
it.prepareStatement("CREATE TABLE foo (id int)").execute()
32+
}
33+
34+
val sentryQueryExecutionListener = SentryQueryExecutionListener(hub)
35+
return ProxyDataSourceBuilder.create(actualDataSource)
36+
.listener(sentryQueryExecutionListener)
37+
.build()
38+
}
39+
}
40+
41+
private val fixture = Fixture()
42+
43+
@AfterTest
44+
fun clean() {
45+
fixture.actualDataSource.connection.use {
46+
it.prepareStatement("drop table foo").execute()
47+
}
48+
}
49+
50+
@Test
51+
fun `creates spans for successful calls`() {
52+
val sut = fixture.getSut()
53+
54+
sut.connection.use {
55+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
56+
it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate()
57+
}
58+
59+
assertEquals(2, fixture.tx.children.size)
60+
fixture.tx.children.forEach {
61+
assertEquals(SpanStatus.OK, it.status)
62+
assertEquals("db", it.operation)
63+
}
64+
assertEquals("INSERT INTO foo VALUES (1)", fixture.tx.children[0].description)
65+
assertEquals("INSERT INTO foo VALUES (2)", fixture.tx.children[1].description)
66+
}
67+
68+
@Test
69+
fun `creates spans for calls resulting in error`() {
70+
val sut = fixture.getSut()
71+
72+
try {
73+
sut.connection.use {
74+
it.prepareStatement("INSERT INTO foo VALUES ('x')").executeUpdate()
75+
}
76+
} catch (e: Exception) {
77+
}
78+
79+
assertEquals(1, fixture.tx.children.size)
80+
assertEquals("INSERT INTO foo VALUES ('x')", fixture.tx.children[0].description)
81+
assertEquals("db", fixture.tx.children[0].operation)
82+
assertEquals(SpanStatus.INTERNAL_ERROR, fixture.tx.children[0].status)
83+
}
84+
85+
@Test
86+
fun `does not create spans when there is no running transactions`() {
87+
val sut = fixture.getSut(withRunningTransaction = false)
88+
89+
sut.connection.use {
90+
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
91+
it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate()
92+
}
93+
94+
assertTrue(fixture.tx.children.isEmpty())
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline

sentry-p6spy/api/sentry-p6spy.api

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public class io/sentry/p6spy/SentryJdbcEventListener : com/p6spy/engine/event/SimpleJdbcEventListener {
2+
public fun <init> ()V
3+
public fun <init> (Lio/sentry/IHub;)V
4+
public fun onAfterAnyExecute (Lcom/p6spy/engine/common/StatementInformation;JLjava/sql/SQLException;)V
5+
public fun onBeforeAnyExecute (Lcom/p6spy/engine/common/StatementInformation;)V
6+
}
7+

sentry-p6spy/build.gradle.kts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
3+
plugins {
4+
`java-library`
5+
kotlin("jvm")
6+
jacoco
7+
id(Config.QualityPlugins.errorProne)
8+
id(Config.QualityPlugins.gradleVersions)
9+
}
10+
11+
configure<JavaPluginConvention> {
12+
sourceCompatibility = JavaVersion.VERSION_1_8
13+
targetCompatibility = JavaVersion.VERSION_1_8
14+
}
15+
16+
tasks.withType<KotlinCompile>().configureEach {
17+
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
18+
}
19+
20+
dependencies {
21+
api(project(":sentry"))
22+
api(Config.Libs.p6spy)
23+
24+
compileOnly(Config.CompileOnly.nopen)
25+
errorprone(Config.CompileOnly.nopenChecker)
26+
errorprone(Config.CompileOnly.errorprone)
27+
errorproneJavac(Config.CompileOnly.errorProneJavac8)
28+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
29+
30+
// tests
31+
testImplementation(project(":sentry-test-support"))
32+
testImplementation(kotlin(Config.kotlinStdLib))
33+
testImplementation(Config.TestLibs.kotlinTestJunit)
34+
testImplementation(Config.TestLibs.mockitoKotlin)
35+
testImplementation(Config.TestLibs.awaitility)
36+
testImplementation(Config.TestLibs.hsqldb)
37+
}
38+
39+
configure<SourceSetContainer> {
40+
test {
41+
java.srcDir("src/test/java")
42+
}
43+
}
44+
45+
jacoco {
46+
toolVersion = Config.QualityPlugins.Jacoco.version
47+
}
48+
49+
tasks.jacocoTestReport {
50+
reports {
51+
xml.isEnabled = true
52+
html.isEnabled = false
53+
}
54+
}
55+
56+
tasks {
57+
jacocoTestCoverageVerification {
58+
violationRules {
59+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
60+
}
61+
}
62+
check {
63+
dependsOn(jacocoTestCoverageVerification)
64+
dependsOn(jacocoTestReport)
65+
}
66+
}

0 commit comments

Comments
 (0)