From 9f752eb0290233a536ae4c18565ae142d061a43f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 9 Aug 2023 10:27:36 +0200 Subject: [PATCH 1/3] Add spring autoconfigure modules to registry task in `.craft.yml` (#2888) --- .craft.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.craft.yml b/.craft.yml index 76f9fae0f2..57e92b7995 100644 --- a/.craft.yml +++ b/.craft.yml @@ -23,8 +23,8 @@ targets: maven:io.sentry:sentry: maven:io.sentry:sentry-spring: maven:io.sentry:sentry-spring-jakarta: - #maven:io.sentry:sentry-spring-boot: - #maven:io.sentry:sentry-spring-boot-jakarta: + maven:io.sentry:sentry-spring-boot: + maven:io.sentry:sentry-spring-boot-jakarta: maven:io.sentry:sentry-spring-boot-starter: maven:io.sentry:sentry-spring-boot-starter-jakarta: maven:io.sentry:sentry-servlet: From d55d97a1620f0ac60412e85e36508c54b96025bf Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 10 Aug 2023 14:04:49 +0200 Subject: [PATCH 2/3] Codecov reports for Android modules (#2868) * Add coverage report for unit test on sentry-android-core * Try running coverage report for android (FOR TESTING PURPOSES ONLY) * Format code * Retrigger checks * Try clean firsT * Use makefile * Try running testDebugUnitTest first * Run test only * list tasks * disable cache * Try with release unit test * Try with release build * Specify files to upload for codecov * fix typo * Add archiving * Without upload * Try matching all android build dirs during codecov upload * Enable unit test coverage for android modules * Create task * Exclude sentry-android-integration-tests * Format code * Try whole process * Try without specifying files * No archive * Rename reports file * Add coverage task in makefile * Reset workflow file * Remove coverage for debug * Test * Test * Fix build error * Test without archiving * Test with array of android module strings * Try with different classes folders * Fix classes dir name * Format code * Include sentry-compose * Adjust sentry-compose source dir * Fix subproject name reference * Revert some changes * Revert some changes * Cleanup * Use gradle plugin * Apply binary plugin * Improve copy and reduce code duplication * (don't merge): debugging Kover * (don't merge): debugging Kover * (don't merge): remove archiving for now * (don't merge): specify file path for codecov * (don't merge): specify file path for codecov * Revert build.yml * Apply jacoco manually for sentry-compose * Remove renaming of report * Fix 'this' receiver * (don't merge): test kover * (don't merge): use release variant) * (don't merge): run test before kover * (don't merge): change report file name * Format code * (don't merge): try specifying the file * (don't merge): try renaming the kover report * Format code * Add Kover for KMP modules * Formatting * Remove Kover configuration in sentry-compose build.gradle.kts --------- Co-authored-by: Sentry Github Bot --- Makefile | 11 ++++-- build.gradle.kts | 41 ++++++++++++++++++++++ buildSrc/src/main/java/Config.kt | 5 +++ sentry-android-core/build.gradle.kts | 7 +--- sentry-android-fragment/build.gradle.kts | 7 +--- sentry-android-navigation/build.gradle.kts | 7 +--- sentry-android-ndk/build.gradle.kts | 7 +--- sentry-android-okhttp/build.gradle.kts | 7 +--- sentry-android-sqlite/build.gradle.kts | 7 +--- sentry-android-timber/build.gradle.kts | 7 +--- sentry-compose/build.gradle.kts | 8 +---- 11 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Makefile b/Makefile index f66cfce775..4ed3732751 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease +.PHONY: all clean compile javadocs dryRelease update stop checkFormat format api assembleBenchmarkTestRelease assembleUiTestRelease createCoverageReports -all: stop clean javadocs compile +all: stop clean javadocs compile createCoverageReports assembleBenchmarks: stop clean assembleBenchmarkTestRelease assembleUiTests: stop clean assembleUiTestRelease @@ -50,3 +50,10 @@ assembleBenchmarkTestRelease: assembleUiTestRelease: ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest -DtestBuildType=release + +# Create coverage reports +# - Jacoco for Java & Android modules +# - Kover for KMP modules e.g sentry-compose +createCoverageReports: + ./gradlew jacocoTestReport + ./gradlew koverXmlReportRelease diff --git a/build.gradle.kts b/build.gradle.kts index 8c4cd4fd38..c3e6eb98eb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ import com.vanniktech.maven.publish.MavenPublishPlugin import com.vanniktech.maven.publish.MavenPublishPluginExtension import groovy.util.Node import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent @@ -14,6 +15,8 @@ plugins { id(Config.QualityPlugins.detekt) version Config.QualityPlugins.detektVersion `maven-publish` id(Config.QualityPlugins.binaryCompatibilityValidator) version Config.QualityPlugins.binaryCompatibilityValidatorVersion + id(Config.QualityPlugins.jacocoAndroid) version Config.QualityPlugins.jacocoAndroidVersion apply false + id(Config.QualityPlugins.kover) version Config.QualityPlugins.koverVersion apply false } buildscript { @@ -97,6 +100,44 @@ allprojects { } subprojects { + val jacocoAndroidModules = listOf( + "sentry-android-core", + "sentry-android-fragment", + "sentry-android-navigation", + "sentry-android-ndk", + "sentry-android-okhttp", + "sentry-android-sqlite", + "sentry-android-timber" + ) + if (jacocoAndroidModules.contains(name)) { + afterEvaluate { + jacoco { + toolVersion = "0.8.10" + } + + tasks.withType { + configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*") + } + } + } + } + + val koverKmpModules = listOf("sentry-compose") + if (koverKmpModules.contains(name)) { + afterEvaluate { + configure { + androidReports("release") { + xml { + // Change the report file name so the Codecov Github action can find it + setReportFile(file("$buildDir/reports/kover/report.xml")) + } + } + } + } + } + plugins.withId(Config.QualityPlugins.detektPlugin) { configure { buildUponDefaultConfig = true diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index be82ce3c3a..eda3873d81 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -1,3 +1,4 @@ + import java.math.BigDecimal object Config { @@ -205,6 +206,10 @@ object Config { val binaryCompatibilityValidatorVersion = "0.13.0" val binaryCompatibilityValidatorPlugin = "org.jetbrains.kotlinx:binary-compatibility-validator:$binaryCompatibilityValidatorVersion" val binaryCompatibilityValidator = "org.jetbrains.kotlinx.binary-compatibility-validator" + val jacocoAndroid = "com.mxalbert.gradle.jacoco-android" + val jacocoAndroidVersion = "0.2.0" + val kover = "org.jetbrains.kotlinx.kover" + val koverVersion = "0.7.3" } object Sentry { diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index d7478786fe..4ab0aec423 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.errorProne) id(Config.QualityPlugins.gradleVersions) } @@ -64,12 +65,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - tasks.withType().configureEach { options.errorprone { check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts index c0a212a98f..14fb3ff9c1 100644 --- a/sentry-android-fragment/build.gradle.kts +++ b/sentry-android-fragment/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) } @@ -54,12 +55,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - kotlin { explicitApi() } diff --git a/sentry-android-navigation/build.gradle.kts b/sentry-android-navigation/build.gradle.kts index 6ae30766e1..d46c7e2b84 100644 --- a/sentry-android-navigation/build.gradle.kts +++ b/sentry-android-navigation/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) } @@ -55,12 +56,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - kotlin { explicitApi() } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 16315c80e4..f6564cd97f 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.NativePlugins.nativeBundleExport) id(Config.QualityPlugins.gradleVersions) } @@ -96,12 +97,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - dependencies { api(projects.sentry) api(projects.sentryAndroidCore) diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts index ae80ad068e..0f98011fac 100644 --- a/sentry-android-okhttp/build.gradle.kts +++ b/sentry-android-okhttp/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) } @@ -56,12 +57,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - kotlin { explicitApi() } diff --git a/sentry-android-sqlite/build.gradle.kts b/sentry-android-sqlite/build.gradle.kts index 11b895f385..6de1b1ab30 100644 --- a/sentry-android-sqlite/build.gradle.kts +++ b/sentry-android-sqlite/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) } @@ -56,12 +57,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - kotlin { explicitApi() } diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index dfebefd93c..a2a999da4a 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -5,6 +5,7 @@ plugins { id("com.android.library") kotlin("android") jacoco + id(Config.QualityPlugins.jacocoAndroid) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) } @@ -59,12 +60,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - kotlin { explicitApi() } diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 7da32ec1f6..1291ea1696 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -8,7 +8,7 @@ plugins { kotlin("multiplatform") id("com.android.library") id("org.jetbrains.compose") - jacoco + id(Config.QualityPlugins.kover) id(Config.QualityPlugins.gradleVersions) id(Config.QualityPlugins.detektPlugin) id(Config.BuildPlugins.dokkaPluginAlias) @@ -113,12 +113,6 @@ android { } } -tasks.withType { - configure { - isIncludeNoLocationClasses = false - } -} - tasks.withType { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() From 3fe387aa5a294ed041a10e38cdfd8585178eef4c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 14 Aug 2023 12:18:04 +0200 Subject: [PATCH 3/3] Send `db.system` and `db.name` in span data (#2894) --- CHANGELOG.md | 6 + sentry-jdbc/api/sentry-jdbc.api | 11 + sentry-jdbc/build.gradle.kts | 1 + .../java/io/sentry/jdbc/DatabaseUtils.java | 156 ++++++++++++ .../sentry/jdbc/SentryJdbcEventListener.java | 36 +++ .../io/sentry/jdbc/DatabaseUtilsTest.kt | 234 ++++++++++++++++++ .../jdbc/SentryJdbcEventListenerTest.kt | 46 ++++ sentry/api/sentry.api | 4 + .../java/io/sentry/SpanDataConvention.java | 2 + .../main/java/io/sentry/util/StringUtils.java | 26 ++ .../java/io/sentry/util/StringUtilsTest.kt | 40 +++ 11 files changed, 562 insertions(+) create mode 100644 sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java create mode 100644 sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f927dd571..d4e0dd0503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Send `db.system` and `db.name` in span data ([#2894](https://github.com/getsentry/sentry-java/pull/2894)) + ## 6.28.0 ### Features diff --git a/sentry-jdbc/api/sentry-jdbc.api b/sentry-jdbc/api/sentry-jdbc.api index a1df685bc3..cff0f37fd2 100644 --- a/sentry-jdbc/api/sentry-jdbc.api +++ b/sentry-jdbc/api/sentry-jdbc.api @@ -3,6 +3,17 @@ public final class io/sentry/jdbc/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } +public final class io/sentry/jdbc/DatabaseUtils { + public fun ()V + public static fun parse (Ljava/lang/String;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails; + public static fun readFrom (Lcom/p6spy/engine/common/StatementInformation;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails; +} + +public final class io/sentry/jdbc/DatabaseUtils$DatabaseDetails { + public fun getDbName ()Ljava/lang/String; + public fun getDbSystem ()Ljava/lang/String; +} + public class io/sentry/jdbc/SentryJdbcEventListener : com/p6spy/engine/event/SimpleJdbcEventListener { public fun ()V public fun (Lio/sentry/IHub;)V diff --git a/sentry-jdbc/build.gradle.kts b/sentry-jdbc/build.gradle.kts index 0395470b8d..239bd46cab 100644 --- a/sentry-jdbc/build.gradle.kts +++ b/sentry-jdbc/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.awaitility) testImplementation(Config.TestLibs.hsqldb) } diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java new file mode 100644 index 0000000000..a7585a8866 --- /dev/null +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java @@ -0,0 +1,156 @@ +package io.sentry.jdbc; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.StatementInformation; +import io.sentry.util.StringUtils; +import java.net.URI; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class DatabaseUtils { + + private static final @NotNull DatabaseDetails EMPTY = new DatabaseDetails(null, null); + + public static DatabaseDetails readFrom( + final @Nullable StatementInformation statementInformation) { + if (statementInformation == null) { + return EMPTY; + } + + final @Nullable ConnectionInformation connectionInformation = + statementInformation.getConnectionInformation(); + if (connectionInformation == null) { + return EMPTY; + } + + return parse(connectionInformation.getUrl()); + } + + public static DatabaseDetails parse(final @Nullable String databaseConnectionUrl) { + if (databaseConnectionUrl == null) { + return EMPTY; + } + try { + final @NotNull String rawUrl = + removeP6SpyPrefix(databaseConnectionUrl.toLowerCase(Locale.ROOT)); + final @NotNull String[] urlParts = rawUrl.split(":", -1); + if (urlParts.length > 1) { + final @NotNull String dbSystem = urlParts[0]; + return parseDbSystemSpecific(dbSystem, urlParts, rawUrl); + } + } catch (Throwable t) { + // ignore + } + + return EMPTY; + } + + private static @NotNull DatabaseDetails parseDbSystemSpecific( + final @NotNull String dbSystem, + final @NotNull String[] urlParts, + final @NotNull String urlString) { + if ("hsqldb".equalsIgnoreCase(dbSystem) + || "h2".equalsIgnoreCase(dbSystem) + || "derby".equalsIgnoreCase(dbSystem) + || "sqlite".equalsIgnoreCase(dbSystem)) { + if (urlString.contains("//")) { + return parseAsUri(dbSystem, StringUtils.removePrefix(urlString, dbSystem + ":")); + } + if (urlParts.length > 2) { + String dbNameAndMaybeMore = urlParts[2]; + return new DatabaseDetails(dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + if (urlParts.length > 1) { + String dbNameAndMaybeMore = urlParts[1]; + return new DatabaseDetails(dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + } + if ("mariadb".equalsIgnoreCase(dbSystem) + || "mysql".equalsIgnoreCase(dbSystem) + || "postgresql".equalsIgnoreCase(dbSystem) + || "mongo".equalsIgnoreCase(dbSystem)) { + return parseAsUri(dbSystem, urlString); + } + if ("sqlserver".equalsIgnoreCase(dbSystem)) { + try { + String dbProperty = ";databasename="; + final int index = urlString.indexOf(dbProperty); + if (index >= 0) { + final @NotNull String dbNameAndMaybeMore = + urlString.substring(index + dbProperty.length()); + return new DatabaseDetails( + dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + } catch (Throwable t) { + // ignore + } + } + if ("oracle".equalsIgnoreCase(dbSystem)) { + String uriPrefix = "oracle:thin:@//"; + final int indexOfUri = urlString.indexOf(uriPrefix); + if (indexOfUri >= 0) { + final @NotNull String uri = + "makethisaprotocol://" + urlString.substring(indexOfUri + uriPrefix.length()); + return parseAsUri(dbSystem, uri); + } + + final int indexOfTnsNames = urlString.indexOf("oracle:thin:@("); + if (indexOfTnsNames >= 0) { + String serviceNamePrefix = "(service_name="; + final int indexOfServiceName = urlString.indexOf(serviceNamePrefix); + if (indexOfServiceName >= 0) { + final int indexOfClosingBrace = urlString.indexOf(")", indexOfServiceName); + final @NotNull String serviceName = + urlString.substring( + indexOfServiceName + serviceNamePrefix.length(), indexOfClosingBrace); + return new DatabaseDetails(dbSystem, serviceName); + } + } + } + if ("datadirect".equalsIgnoreCase(dbSystem) + || "tibcosoftware".equalsIgnoreCase(dbSystem) + || "jtds".equalsIgnoreCase(dbSystem) + || "microsoft".equalsIgnoreCase(dbSystem)) { + return parse(StringUtils.removePrefix(urlString, dbSystem + ":")); + } + + return new DatabaseDetails(dbSystem, null); + } + + private static @NotNull DatabaseDetails parseAsUri( + final @NotNull String dbSystem, final @NotNull String urlString) { + try { + final @NotNull URI url = new URI(urlString); + String path = StringUtils.removePrefix(url.getPath(), "/"); + String pathWithoutProperties = StringUtils.substringBefore(path, ";"); + return new DatabaseDetails(dbSystem, pathWithoutProperties); + } catch (Throwable t) { + System.out.println(t.getMessage()); + // ignore + } + return new DatabaseDetails(dbSystem, null); + } + + private static @NotNull String removeP6SpyPrefix(final @NotNull String url) { + return StringUtils.removePrefix(StringUtils.removePrefix(url, "jdbc:"), "p6spy:"); + } + + public static final class DatabaseDetails { + private final @Nullable String dbSystem; + private final @Nullable String dbName; + + DatabaseDetails(final @Nullable String dbSystem, final @Nullable String dbName) { + this.dbSystem = dbSystem; + this.dbName = dbName; + } + + public @Nullable String getDbSystem() { + return dbSystem; + } + + public @Nullable String getDbName() { + return dbName; + } + } +} diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java index 0e301cd1c6..0346d2d0b9 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java @@ -1,5 +1,8 @@ package io.sentry.jdbc; +import static io.sentry.SpanDataConvention.DB_NAME_KEY; +import static io.sentry.SpanDataConvention.DB_SYSTEM_KEY; + import com.jakewharton.nopen.annotation.Open; import com.p6spy.engine.common.StatementInformation; import com.p6spy.engine.event.SimpleJdbcEventListener; @@ -21,6 +24,9 @@ public class SentryJdbcEventListener extends SimpleJdbcEventListener { private final @NotNull IHub hub; private static final @NotNull ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); + private volatile @Nullable DatabaseUtils.DatabaseDetails cachedDatabaseDetails = null; + private final @NotNull Object databaseDetailsLock = new Object(); + public SentryJdbcEventListener(final @NotNull IHub hub) { this.hub = Objects.requireNonNull(hub, "hub is required"); addPackageAndIntegrationInfo(); @@ -46,7 +52,10 @@ public void onAfterAnyExecute( long timeElapsedNanos, final @Nullable SQLException e) { final ISpan span = CURRENT_SPAN.get(); + if (span != null) { + applyDatabaseDetailsToSpan(statementInformation, span); + if (e != null) { span.setThrowable(e); span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -63,4 +72,31 @@ private void addPackageAndIntegrationInfo() { SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-jdbc", BuildConfig.VERSION_NAME); } + + private void applyDatabaseDetailsToSpan( + final @NotNull StatementInformation statementInformation, final @NotNull ISpan span) { + final @NotNull DatabaseUtils.DatabaseDetails databaseDetails = + getOrComputeDatabaseDetails(statementInformation); + + if (databaseDetails.getDbSystem() != null) { + span.setData(DB_SYSTEM_KEY, databaseDetails.getDbSystem()); + } + + if (databaseDetails.getDbName() != null) { + span.setData(DB_NAME_KEY, databaseDetails.getDbName()); + } + } + + private @NotNull DatabaseUtils.DatabaseDetails getOrComputeDatabaseDetails( + final @NotNull StatementInformation statementInformation) { + if (cachedDatabaseDetails == null) { + synchronized (databaseDetailsLock) { + if (cachedDatabaseDetails == null) { + cachedDatabaseDetails = DatabaseUtils.readFrom(statementInformation); + } + } + } + + return cachedDatabaseDetails; + } } diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt new file mode 100644 index 0000000000..7ff70e49bd --- /dev/null +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt @@ -0,0 +1,234 @@ +package io.sentry.jdbc + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class DatabaseUtilsTest { + + @Test + fun `parses to empty details for null`() { + val details = DatabaseUtils.parse(null) + assertNotNull(details) + assertNull(details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for hsql in-memory`() { + val details = DatabaseUtils.parse("jdbc:p6spy:hsqldb:mem:testdb;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals("testdb", details.dbName) + } + + @Test + fun `detects db system for hsql in-memory legacy`() { + val details = DatabaseUtils.parse("jdbc:p6spy:hsqldb:.;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals(".", details.dbName) + } + + @Test + fun `detects db system for hsql remote`() { + val details = DatabaseUtils.parse("jdbc:hsqldb:hsql://some-host.com:1234/testdb;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals("testdb", details.dbName) + } + + @Test + fun `detects db system for h2 in-memory`() { + val details = DatabaseUtils.parse("jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") + assertEquals("h2", details.dbSystem) + assertEquals("az", details.dbName) + } + + @Test + fun `detects db system for h2 tcp`() { + val details = DatabaseUtils.parse("jdbc:h2:tcp://localhost/~/test") + assertEquals("h2", details.dbSystem) + assertEquals("~/test", details.dbName) + } + + @Test + fun `detects db system for derby`() { + val details = DatabaseUtils.parse("jdbc:derby:sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for derby remote`() { + val details = DatabaseUtils.parse("jdbc:derby://some-host.com:1234/sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for derby remote no port`() { + val details = DatabaseUtils.parse("jdbc:derby://some-host.com/sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for sqlite`() { + val details = DatabaseUtils.parse("jdbc:sqlite:sample.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("sample.db", details.dbName) + } + + @Test + fun `detects db system for sqlite memory`() { + val details = DatabaseUtils.parse("jdbc:sqlite::memory:") + assertEquals("sqlite", details.dbSystem) + assertEquals("memory", details.dbName) + } + + @Test + fun `detects db system for sqlite windows`() { + val details = DatabaseUtils.parse("jdbc:sqlite:C:/sqlite/db/some.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("/sqlite/db/some.db", details.dbName) + } + + @Test + fun `detects db system for sqlite linux`() { + val details = DatabaseUtils.parse("jdbc:sqlite:/home/sqlite/db/some.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("/home/sqlite/db/some.db", details.dbName) + } + + @Test + fun `detects db system for mongo`() { + val details = DatabaseUtils.parse("jdbc:mongo://some-server.com:1234/mydb") + assertEquals("mongo", details.dbSystem) + assertEquals("mydb", details.dbName) + } + + @Test + fun `detects db system for mongo no db`() { + val details = DatabaseUtils.parse("jdbc:mongo://some-server.com:1234") + assertEquals("mongo", details.dbSystem) + assertEquals("", details.dbName) + } + + @Test + fun `detects db system for redis`() { + val details = DatabaseUtils.parse("jdbc:redis:Server=127.0.0.1;Port=6379;Password=myPassword;") + assertEquals("redis", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for dynamodb`() { + val details = DatabaseUtils.parse("jdbc:amazondynamodb:Access Key=xxx;Secret Key=xxx;Domain=amazonaws.com;Region=OREGON;") + assertEquals("amazondynamodb", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for oracle`() { + val details = DatabaseUtils.parse("jdbc:oracle:thin:@//myoracle.db.server:1521/my_servicename") + assertEquals("oracle", details.dbSystem) + assertEquals("my_servicename", details.dbName) + } + + @Test + fun `detects db system for oracle2`() { + val details = DatabaseUtils.parse("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=myoracle.db.server)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=my_servicename)))") + assertEquals("oracle", details.dbSystem) + assertEquals("my_servicename", details.dbName) + } + + @Test + fun `detects db system for mariadb`() { + val details = DatabaseUtils.parse("jdbc:mariadb://example.skysql.net:5001/jdbc_demo?useSsl=true&serverSslCert=/path/to/skysql_chain.pem") + assertEquals("mariadb", details.dbSystem) + assertEquals("jdbc_demo", details.dbName) + } + + @Test + fun `detects db system for mariadb no host and port`() { + val details = DatabaseUtils.parse("jdbc:mariadb://") + assertEquals("mariadb", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for mysql`() { + val details = DatabaseUtils.parse("jdbc:mysql://mysql.db.server:3306/my_database?useSSL=false&serverTimezone=UTC") + assertEquals("mysql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mysql no host and port and database`() { + val details = DatabaseUtils.parse("jdbc:mysql://") + assertEquals("mysql", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for mssql`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mssql2`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database;otherProperty=value") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mssql2 no host and port`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://;databaseName=my_database;otherProperty=value") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for postgres`() { + val details = DatabaseUtils.parse("jdbc:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for postgres no host and port`() { + val details = DatabaseUtils.parse("jdbc:postgresql:///my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for datadirect postgres`() { + val details = DatabaseUtils.parse("jdbc:datadirect:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for tibcosoftware postgres`() { + val details = DatabaseUtils.parse("jdbc:tibcosoftware:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for jtds postgres`() { + val details = DatabaseUtils.parse("jdbc:jtds:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for microsoft sqlserver`() { + val details = DatabaseUtils.parse("jdbc:microsoft:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } +} diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt index 962410647e..78c5d4cf12 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt @@ -1,13 +1,19 @@ package io.sentry.jdbc +import com.p6spy.engine.common.StatementInformation import com.p6spy.engine.spy.P6DataSource import io.sentry.IHub import io.sentry.SentryOptions import io.sentry.SentryTracer +import io.sentry.SpanDataConvention.DB_NAME_KEY +import io.sentry.SpanDataConvention.DB_SYSTEM_KEY import io.sentry.SpanStatus import io.sentry.TransactionContext +import io.sentry.jdbc.DatabaseUtils.DatabaseDetails import io.sentry.protocol.SdkVersion import org.hsqldb.jdbc.JDBCDataSource +import org.mockito.Mockito +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import javax.sql.DataSource @@ -131,4 +137,44 @@ class SentryJdbcEventListenerTest { assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } + + @Test + fun `sets database details`() { + val sut = fixture.getSut() + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() + } + + assertEquals("hsqldb", fixture.tx.children.first().data[DB_SYSTEM_KEY]) + assertEquals("testdb", fixture.tx.children.first().data[DB_NAME_KEY]) + } + + @Test + fun `only parses database details once`() { + Mockito.mockStatic(DatabaseUtils::class.java).use { utils -> + var invocationCount = 0 + utils.`when` { DatabaseUtils.readFrom(any()) } + .thenAnswer { + invocationCount++ + DatabaseDetails("a", "b") + } + val sut = fixture.getSut() + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() + it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate() + } + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (3)").executeUpdate() + it.prepareStatement("INSERT INTO foo VALUES (4)").executeUpdate() + } + + assertEquals("a", fixture.tx.children.first().data[DB_SYSTEM_KEY]) + assertEquals("b", fixture.tx.children.first().data[DB_NAME_KEY]) + + assertEquals(1, invocationCount) + } + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 978f582bed..43ca4d5fad 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2225,6 +2225,8 @@ public final class io/sentry/SpanContext$JsonKeys { public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; + public static final field DB_NAME_KEY Ljava/lang/String; + public static final field DB_SYSTEM_KEY Ljava/lang/String; public static final field HTTP_FRAGMENT_KEY Ljava/lang/String; public static final field HTTP_METHOD_KEY Ljava/lang/String; public static final field HTTP_QUERY_KEY Ljava/lang/String; @@ -4317,7 +4319,9 @@ public final class io/sentry/util/StringUtils { public static fun getStringAfterDot (Ljava/lang/String;)Ljava/lang/String; public static fun join (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String; public static fun normalizeUUID (Ljava/lang/String;)Ljava/lang/String; + public static fun removePrefix (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static fun removeSurrounding (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public static fun substringBefore (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static fun toString (Ljava/lang/Object;)Ljava/lang/String; } diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index 7e8d81ac44..bffae1206a 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -6,6 +6,8 @@ public interface SpanDataConvention { // Keys that should respect the span data conventions, as described in // https://develop.sentry.dev/sdk/performance/span-data-conventions/ + String DB_SYSTEM_KEY = "db.system"; + String DB_NAME_KEY = "db.name"; String HTTP_QUERY_KEY = "http.query"; String HTTP_FRAGMENT_KEY = "http.fragment"; String HTTP_METHOD_KEY = "http.method"; diff --git a/sentry/src/main/java/io/sentry/util/StringUtils.java b/sentry/src/main/java/io/sentry/util/StringUtils.java index 2158576619..ac95f9ad70 100644 --- a/sentry/src/main/java/io/sentry/util/StringUtils.java +++ b/sentry/src/main/java/io/sentry/util/StringUtils.java @@ -189,4 +189,30 @@ public static String join( return object.toString(); } + + public static @NotNull String removePrefix( + final @Nullable String string, final @NotNull String prefix) { + if (string == null) { + return ""; + } + final int index = string.indexOf(prefix); + if (index == 0) { + return string.substring(prefix.length()); + } else { + return string; + } + } + + public static @NotNull String substringBefore( + final @Nullable String string, final @NotNull String separator) { + if (string == null) { + return ""; + } + final int index = string.indexOf(separator); + if (index >= 0) { + return string.substring(0, index); + } else { + return string; + } + } } diff --git a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt index a8c9058c29..d06f3859ae 100644 --- a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt @@ -147,4 +147,44 @@ class StringUtilsTest { val result = StringUtils.join(",", emptyList()) assertEquals("", result) } + + @Test + fun `remove prefix on null returns emtpy string`() { + assertEquals("", StringUtils.removePrefix(null, ":")) + } + + @Test + fun `remove prefix on string equal to prefix returns empty string`() { + assertEquals("", StringUtils.removePrefix(":", ":")) + } + + @Test + fun `remove prefix on string returns string without prefix`() { + assertEquals("abc", StringUtils.removePrefix(":abc", ":")) + } + + @Test + fun `remove prefix on string returns string untouched if prefix is not at start`() { + assertEquals("abc:", StringUtils.removePrefix("abc:", ":")) + } + + @Test + fun `returns only prefix before separator`() { + assertEquals("abc", StringUtils.substringBefore("abc:", ":")) + } + + @Test + fun `returns empty string if string is null substringBefore`() { + assertEquals("", StringUtils.substringBefore(null, ":")) + } + + @Test + fun `returns full string if separator is not in string`() { + assertEquals("abc", StringUtils.substringBefore("abc", ":")) + } + + @Test + fun `returns empty string if separator is at start of string`() { + assertEquals("", StringUtils.substringBefore(":abc", ":")) + } }