From 3fe387aa5a294ed041a10e38cdfd8585178eef4c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 14 Aug 2023 12:18:04 +0200 Subject: [PATCH] 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", ":")) + } }