diff --git a/CHANGELOG.md b/CHANGELOG.md index baf158193ae..ef77be1743a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Fixes +- Send Timber logs through Sentry Logs ([#4490](https://github.com/getsentry/sentry-java/pull/4490)) + - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send Timber logs to Sentry, if the TimberIntegration is enabled. + - The SDK will automatically detect Timber and use it to send logs to Sentry. - Send logcat through Sentry Logs ([#4487](https://github.com/getsentry/sentry-java/pull/4487)) - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send logcat logs to Sentry, if the Sentry Android Gradle plugin is applied. - To set the logcat level check the [Logcat integration documentation](https://docs.sentry.io/platforms/android/integrations/logcat/#configure). diff --git a/sentry-android-timber/api/sentry-android-timber.api b/sentry-android-timber/api/sentry-android-timber.api index 2d71f67570e..8ae2f49c28d 100644 --- a/sentry-android-timber/api/sentry-android-timber.api +++ b/sentry-android-timber/api/sentry-android-timber.api @@ -9,16 +9,18 @@ public final class io/sentry/android/timber/BuildConfig { public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/Integration, java/io/Closeable { public fun ()V - public fun (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V - public synthetic fun (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V + public synthetic fun (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun close ()V public final fun getMinBreadcrumbLevel ()Lio/sentry/SentryLevel; public final fun getMinEventLevel ()Lio/sentry/SentryLevel; + public final fun getMinLogsLevel ()Lio/sentry/SentryLogLevel; public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/timber/SentryTimberTree : timber/log/Timber$Tree { - public fun (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V + public fun (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V + public synthetic fun (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun d (Ljava/lang/String;[Ljava/lang/Object;)V public fun d (Ljava/lang/Throwable;)V public fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt index 59edeac8690..521fe15127a 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt @@ -5,6 +5,7 @@ import io.sentry.IScopes import io.sentry.Integration import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel +import io.sentry.SentryLogLevel import io.sentry.SentryOptions import io.sentry.android.timber.BuildConfig.VERSION_NAME import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion @@ -15,6 +16,7 @@ import timber.log.Timber public class SentryTimberIntegration( public val minEventLevel: SentryLevel = SentryLevel.ERROR, public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, + public val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Integration, Closeable { private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger @@ -29,7 +31,7 @@ public class SentryTimberIntegration( override fun register(scopes: IScopes, options: SentryOptions) { logger = options.logger - tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel) + tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) Timber.plant(tree) logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.") diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt index 7ff65242e96..c0e996bdba9 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryLevel +import io.sentry.SentryLogLevel import io.sentry.protocol.Message import timber.log.Timber @@ -14,6 +15,7 @@ public class SentryTimberTree( private val scopes: IScopes, private val minEventLevel: SentryLevel, private val minBreadcrumbLevel: SentryLevel, + private val minLogLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Timber.Tree() { private val pendingTag = ThreadLocal() @@ -168,6 +170,7 @@ public class SentryTimberTree( } val level = getSentryLevel(priority) + val logLevel = getSentryLogLevel(priority) val sentryMessage = Message().apply { this.message = message @@ -179,12 +182,17 @@ public class SentryTimberTree( captureEvent(level, tag, sentryMessage, throwable) addBreadcrumb(level, sentryMessage, throwable) + addLog(logLevel, message, throwable, *args) } /** do not log if it's lower than min. required level. */ private fun isLoggable(level: SentryLevel, minLevel: SentryLevel): Boolean = level.ordinal >= minLevel.ordinal + /** do not log if it's lower than min. required level. */ + private fun isLoggable(level: SentryLogLevel, minLevel: SentryLogLevel): Boolean = + level.ordinal >= minLevel.ordinal + /** Captures an event with the given attributes */ private fun captureEvent( sentryLevel: SentryLevel, @@ -227,6 +235,25 @@ public class SentryTimberTree( } } + /** Send a Sentry Logs */ + private fun addLog( + sentryLogLevel: SentryLogLevel, + msg: String?, + throwable: Throwable?, + vararg args: Any?, + ) { + // checks the log level + if (isLoggable(sentryLogLevel, minLogLevel)) { + val throwableMsg = throwable?.message + when { + msg != null && throwableMsg != null -> + scopes.logger().log(sentryLogLevel, "$msg\n$throwableMsg", *args) + msg != null -> scopes.logger().log(sentryLogLevel, msg, *args) + throwableMsg != null -> scopes.logger().log(sentryLogLevel, throwableMsg, *args) + } + } + } + /** Converts from Timber priority to SentryLevel. Fallback to SentryLevel.DEBUG. */ private fun getSentryLevel(priority: Int): SentryLevel = when (priority) { @@ -238,4 +265,17 @@ public class SentryTimberTree( Log.VERBOSE -> SentryLevel.DEBUG else -> SentryLevel.DEBUG } + + /** Converts from Timber priority to SentryLogLevel. Fallback to SentryLogLevel.DEBUG. */ + private fun getSentryLogLevel(priority: Int): SentryLogLevel { + return when (priority) { + Log.ASSERT -> SentryLogLevel.FATAL + Log.ERROR -> SentryLogLevel.ERROR + Log.WARN -> SentryLogLevel.WARN + Log.INFO -> SentryLogLevel.INFO + Log.DEBUG -> SentryLogLevel.DEBUG + Log.VERBOSE -> SentryLogLevel.TRACE + else -> SentryLogLevel.DEBUG + } + } } diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt index 924512951d2..43a45da7bb3 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.timber import io.sentry.IScopes import io.sentry.SentryLevel +import io.sentry.SentryLogLevel import io.sentry.SentryOptions import io.sentry.protocol.SdkVersion import kotlin.test.BeforeTest @@ -21,10 +22,12 @@ class SentryTimberIntegrationTest { fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ): SentryTimberIntegration = SentryTimberIntegration( minEventLevel = minEventLevel, minBreadcrumbLevel = minBreadcrumbLevel, + minLogsLevel = minLogsLevel, ) } @@ -78,11 +81,16 @@ class SentryTimberIntegrationTest { @Test fun `Integrations pass the right min levels`() { val sut = - fixture.getSut(minEventLevel = SentryLevel.INFO, minBreadcrumbLevel = SentryLevel.DEBUG) + fixture.getSut( + minEventLevel = SentryLevel.INFO, + minBreadcrumbLevel = SentryLevel.DEBUG, + minLogsLevel = SentryLogLevel.TRACE, + ) sut.register(fixture.scopes, fixture.options) assertEquals(sut.minEventLevel, SentryLevel.INFO) assertEquals(sut.minBreadcrumbLevel, SentryLevel.DEBUG) + assertEquals(sut.minLogsLevel, SentryLogLevel.TRACE) } @Test diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt index 40a2d419f79..e77e33e42d6 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt @@ -1,8 +1,10 @@ package io.sentry.android.timber import io.sentry.Breadcrumb -import io.sentry.IScopes +import io.sentry.Scopes import io.sentry.SentryLevel +import io.sentry.SentryLogLevel +import io.sentry.logger.ILoggerApi import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -10,19 +12,28 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import org.mockito.kotlin.any import org.mockito.kotlin.check +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever import timber.log.Timber class SentryTimberTreeTest { private class Fixture { - val scopes = mock() + val scopes = mock() + val logs = mock() + + init { + whenever(scopes.logger()).thenReturn(logs) + } fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, - ): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel) + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, + ): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) } private val fixture = Fixture() @@ -231,4 +242,72 @@ class SentryTimberTreeTest { val sut = fixture.getSut() sut.d("test %s, %s", 1, 1) } + + @Test + fun `Tree adds a log with message and arguments, when provided`() { + val sut = fixture.getSut() + sut.e("test count: %d %d", 32, 5) + + verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("test count: %d %d"), eq(32), eq(5)) + } + + @Test + fun `Tree adds a log if min level is equal`() { + val sut = fixture.getSut() + sut.i(Throwable("test")) + verify(fixture.logs).log(any(), any()) + } + + @Test + fun `Tree adds a log if min level is higher`() { + val sut = fixture.getSut() + sut.e(Throwable("test")) + verify(fixture.logs).log(any(), any(), any()) + } + + @Test + fun `Tree won't add a log if min level is lower`() { + val sut = fixture.getSut(minLogsLevel = SentryLogLevel.ERROR) + sut.i(Throwable("test")) + verifyNoInteractions(fixture.logs) + } + + @Test + fun `Tree adds an info log`() { + val sut = fixture.getSut() + sut.i("message") + + verify(fixture.logs).log(eq(SentryLogLevel.INFO), eq("message")) + } + + @Test + fun `Tree adds an error log`() { + val sut = fixture.getSut() + sut.e(Throwable("test")) + + verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("test")) + } + + @Test + fun `Tree does not add a log, if no message or throwable is provided`() { + val sut = fixture.getSut() + sut.e(null as String?) + verifyNoInteractions(fixture.logs) + } + + @Test + fun `Tree logs throwable`() { + val sut = fixture.getSut() + sut.e(Throwable("throwable message")) + + verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("throwable message")) + } + + @Test + fun `Tree logs throwable and message`() { + val sut = fixture.getSut() + sut.e(Throwable("throwable message"), "My message") + + verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("My message\nthrowable message")) + } }