Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add datasource tracing with P6Spy. #1784

Merged
merged 8 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Add datasource tracing with P6Spy (#1784)

## 5.2.4

* Fix: Window.FEATURE_NO_TITLE does not work when using activity traces (#1769)
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ object Config {
val springBootStarterWebflux = "org.springframework.boot:spring-boot-starter-webflux:$springBootVersion"
val springBootStarterAop = "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
val springBootStarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
val springBootStarterJdbc = "org.springframework.boot:spring-boot-starter-jdbc:$springBootVersion"

val springWeb = "org.springframework:spring-webmvc"
val springWebflux = "org.springframework:spring-webflux"
Expand Down Expand Up @@ -98,6 +99,8 @@ object Config {
private val apolloVersion = "2.5.9"
val apolloAndroid = "com.apollographql.apollo:apollo-runtime:$apolloVersion"
val apolloCoroutines = "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"

val p6spy = "p6spy:p6spy:3.9.1"
}

object AnnotationProcessors {
Expand All @@ -120,6 +123,7 @@ object Config {
val mockWebserver = "com.squareup.okhttp3:mockwebserver:${Libs.okHttpVersion}"
val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9"
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.28.0"
val hsqldb = "org.hsqldb:hsqldb:2.6.1"
}

object QualityPlugins {
Expand Down
7 changes: 7 additions & 0 deletions sentry-jdbc/api/sentry-jdbc.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public class io/sentry/jdbc/SentryJdbcEventListener : com/p6spy/engine/event/SimpleJdbcEventListener {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public fun onAfterAnyExecute (Lcom/p6spy/engine/common/StatementInformation;JLjava/sql/SQLException;)V
public fun onBeforeAnyExecute (Lcom/p6spy/engine/common/StatementInformation;)V
}

74 changes: 74 additions & 0 deletions sentry-jdbc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
jacoco
id(Config.QualityPlugins.errorProne)
id(Config.QualityPlugins.gradleVersions)
}

configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}

dependencies {
api(projects.sentry)
api(Config.Libs.p6spy)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
errorprone(Config.CompileOnly.errorProneNullAway)

// tests
testImplementation(projects.sentryTestSupport)
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.awaitility)
testImplementation(Config.TestLibs.hsqldb)
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

jacoco {
toolVersion = Config.QualityPlugins.Jacoco.version
}

tasks.jacocoTestReport {
reports {
xml.isEnabled = true
html.isEnabled = false
}
}

tasks {
jacocoTestCoverageVerification {
violationRules {
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
}
}
check {
dependsOn(jacocoTestCoverageVerification)
dependsOn(jacocoTestReport)
}
}

tasks.withType<JavaCompile>().configureEach {
options.errorprone {
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "io.sentry")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.sentry.jdbc;

import com.jakewharton.nopen.annotation.Open;
import com.p6spy.engine.common.StatementInformation;
import com.p6spy.engine.event.SimpleJdbcEventListener;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ISpan;
import io.sentry.Span;
import io.sentry.SpanStatus;
import io.sentry.util.Objects;
import java.sql.SQLException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** P6Spy JDBC event listener that creates {@link Span}s around database queries. */
@Open
public class SentryJdbcEventListener extends SimpleJdbcEventListener {
private final @NotNull IHub hub;
private static final @NotNull ThreadLocal<ISpan> CURRENT_SPAN = new ThreadLocal<>();

public SentryJdbcEventListener(final @NotNull IHub hub) {
this.hub = Objects.requireNonNull(hub, "hub is required");
}

public SentryJdbcEventListener() {
this(HubAdapter.getInstance());
}

@Override
public void onBeforeAnyExecute(final @NotNull StatementInformation statementInformation) {
final ISpan parent = hub.getSpan();
if (parent != null) {
final ISpan span = parent.startChild("db.query", statementInformation.getSql());
CURRENT_SPAN.set(span);
}
}

@Override
public void onAfterAnyExecute(
final @NotNull StatementInformation statementInformation,
long timeElapsedNanos,
final @Nullable SQLException e) {
final ISpan span = CURRENT_SPAN.get();
if (span != null) {
if (e != null) {
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
} else {
span.setStatus(SpanStatus.OK);
}
span.finish();
CURRENT_SPAN.set(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.sentry.jdbc.SentryJdbcEventListener
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package io.sentry.jdbc

import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import com.p6spy.engine.spy.P6DataSource
import io.sentry.IHub
import io.sentry.SentryOptions
import io.sentry.SentryTracer
import io.sentry.SpanStatus
import io.sentry.TransactionContext
import org.hsqldb.jdbc.JDBCDataSource
import javax.sql.DataSource
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SentryJdbcEventListenerTest {

class Fixture {
private val hub = mock<IHub>()
val tx = SentryTracer(TransactionContext("name", "op"), hub)
val actualDataSource = JDBCDataSource()

fun getSut(withRunningTransaction: Boolean = true, existingRow: Int? = null): DataSource {
whenever(hub.options).thenReturn(SentryOptions())
if (withRunningTransaction) {
whenever(hub.span).thenReturn(tx)
}
actualDataSource.setURL("jdbc:hsqldb:mem:testdb")

actualDataSource.connection.use {
it.prepareStatement("CREATE TABLE foo (id int unique)").execute()
}
existingRow?.let { row ->
actualDataSource.connection.use {
val statement = it.prepareStatement("INSERT INTO foo VALUES (?)")
statement.setInt(1, existingRow)
statement.executeUpdate()
}
}

val sentryQueryExecutionListener = SentryJdbcEventListener(hub)
val p6spyDataSource = P6DataSource(actualDataSource)
p6spyDataSource.setJdbcEventListenerFactory { sentryQueryExecutionListener }
return p6spyDataSource
}
}

private val fixture = Fixture()

@AfterTest
fun clean() {
fixture.actualDataSource.connection.use {
it.prepareStatement("drop table foo").execute()
}
}

@Test
fun `creates spans for successful calls`() {
val sut = fixture.getSut()

sut.connection.use {
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate()
}

assertEquals(2, fixture.tx.children.size)
fixture.tx.children.forEach {
assertEquals(SpanStatus.OK, it.status)
assertEquals("db.query", it.operation)
}
assertEquals("INSERT INTO foo VALUES (1)", fixture.tx.children[0].description)
assertEquals("INSERT INTO foo VALUES (2)", fixture.tx.children[1].description)
}

@Test
fun `creates spans for calls resulting in error`() {
val sut = fixture.getSut(existingRow = 1)

try {
sut.connection.use {
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
}
} catch (e: Exception) {
}

assertEquals(1, fixture.tx.children.size)
assertEquals(SpanStatus.INTERNAL_ERROR, fixture.tx.children[0].status)
assertEquals("INSERT INTO foo VALUES (1)", fixture.tx.children[0].description)
assertEquals("db.query", fixture.tx.children[0].operation)
}

@Test
fun `does not create spans when there is no running transactions`() {
val sut = fixture.getSut(withRunningTransaction = false)

sut.connection.use {
it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate()
it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate()
}

assertTrue(fixture.tx.children.isEmpty())
}
}
5 changes: 5 additions & 0 deletions sentry-samples/sentry-samples-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ dependencies {
implementation(Config.Libs.springBootStarterAop)
implementation(Config.Libs.aspectj)
implementation(Config.Libs.springBootStarter)
implementation(Config.Libs.springBootStarterJdbc)
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
implementation(projects.sentrySpringBootStarter)
implementation(projects.sentryLogback)

// database query tracing
implementation(projects.sentryJdbc)
runtimeOnly(Config.TestLibs.hsqldb)
testImplementation(Config.Libs.springBootStarterTest) {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.sentry.spring.tracing.SentrySpan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

/**
Expand All @@ -14,12 +15,17 @@
public class PersonService {
private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class);

private final JdbcTemplate jdbcTemplate;

public PersonService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

Person create(Person person) {
LOGGER.warn("Creating person: {}", person);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
jdbcTemplate.update(
"insert into person (firstName, lastName) values (?, ?)",
person.getFirstName(),
person.getLastName());
return person;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ sentry.logging.minimum-breadcrumb-level=debug
sentry.traces-sample-rate=1.0
sentry.debug=true
in-app-includes="io.sentry.samples"

# Database configuration
spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=sa
spring.datasource.password=
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE person (
id INTEGER IDENTITY PRIMARY KEY,
firstName VARCHAR(50) NOT NULL,
lastName VARCHAR(50) NOT NULL
);
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ include(
"sentry-spring-boot-starter",
"sentry-bom",
"sentry-openfeign",
"sentry-jdbc",
"sentry-samples:sentry-samples-android",
"sentry-samples:sentry-samples-console",
"sentry-samples:sentry-samples-jul",
Expand Down