diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af676f676a..8ff42f9fe41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add an explicit Logs opt-in to the Android Timber integration ([#5943](https://github.com/getsentry/sentry-java/pull/5943)) - Add an explicit Logs opt-in to the JUL handler ([#5942](https://github.com/getsentry/sentry-java/pull/5942)) - Add an explicit Logs opt-in to the Log4j2 appender ([#5941](https://github.com/getsentry/sentry-java/pull/5941)) - Add an explicit Logs opt-in to the Logback appender ([#5940](https://github.com/getsentry/sentry-java/pull/5940)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index da80a74e32c..9e3b0fcf174 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -439,6 +439,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableStandaloneAppStartTracing ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun isEnableSystemEventBreadcrumbsExtras ()Z + public fun isEnableTimberLogs ()Z public fun isReportHistoricalAnrs ()Z public fun isReportHistoricalTombstones ()Z public fun isTombstoneEnabled ()Z @@ -472,6 +473,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableStandaloneAppStartTracing (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setEnableSystemEventBreadcrumbsExtras (Z)V + public fun setEnableTimberLogs (Z)V public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V public fun setNativeSdkName (Ljava/lang/String;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index a0547a78b34..d31d925810f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -473,7 +473,7 @@ static void installDefaultIntegrations( } if (isTimberAvailable) { - options.addIntegration(new SentryTimberIntegration()); + options.addIntegration(new SentryTimberIntegration(options.isEnableTimberLogs())); } options.addIntegration(new AppComponentsBreadcrumbsIntegration(context)); options.addIntegration(new SystemEventsBreadcrumbsIntegration(context)); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index f21d4c801a3..ddebd1b5eef 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -163,6 +163,8 @@ final class ManifestMetadataReader { static final String ENABLE_LOGS = "io.sentry.logs.enabled"; + static final String ENABLE_TIMBER_LOGS = "io.sentry.timber.logs.enabled"; + static final String ENABLE_METRICS = "io.sentry.metrics.enabled"; static final String ENABLE_AUTO_TRACE_ID_GENERATION = @@ -706,6 +708,9 @@ static void applyMetadata( .getLogs() .setEnabled(readBool(metadata, logger, ENABLE_LOGS, options.getLogs().isEnabled())); + options.setEnableTimberLogs( + readBool(metadata, logger, ENABLE_TIMBER_LOGS, options.isEnableTimberLogs())); + options .getMetrics() .setEnabled( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 615db97a28d..a0ca7a36c24 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -71,6 +71,9 @@ public final class SentryAndroidOptions extends SentryOptions { /** Enable or disable automatic breadcrumbs for Network Events Using NetworkCallback */ private boolean enableNetworkEventBreadcrumbs = true; + /** Enable or disable automatic Sentry Logs capture from Timber. Default is disabled. */ + private boolean enableTimberLogs = false; + /** * Enables the Auto instrumentation for Activity lifecycle tracing. * @@ -457,6 +460,14 @@ public void setEnableNetworkEventBreadcrumbs(boolean enableNetworkEventBreadcrum this.enableNetworkEventBreadcrumbs = enableNetworkEventBreadcrumbs; } + public boolean isEnableTimberLogs() { + return enableTimberLogs; + } + + public void setEnableTimberLogs(boolean enableTimberLogs) { + this.enableTimberLogs = enableTimberLogs; + } + /** * Enable or disable all the automatic breadcrumbs * diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 6df1ed7167e..224b66bbfb1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -679,8 +679,21 @@ class AndroidOptionsInitializerTest { fun `SentryTimberIntegration added to the integration list if available on classpath`() { fixture.initSutWithClassLoader(isTimberAvailable = true) - val actual = fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } - assertNotNull(actual) + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + as SentryTimberIntegration + assertFalse(actual.enableLogs) + } + + @Test + fun `SentryTimberIntegration receives Timber logs option`() { + fixture.sentryOptions.isEnableTimberLogs = true + fixture.initSutWithClassLoader(isTimberAvailable = true) + + val actual = + fixture.sentryOptions.integrations.firstOrNull { it is SentryTimberIntegration } + as SentryTimberIntegration + assertTrue(actual.enableLogs) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index d67a869eff0..f0d6e6f562d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1967,6 +1967,25 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.logs.isEnabled) } + @Test + fun `applyMetadata keeps Timber logs disabled if not found`() { + val context = fixture.getContext() + + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + assertFalse(fixture.options.isEnableTimberLogs) + } + + @Test + fun `applyMetadata reads Timber logs enabled to options`() { + val bundle = bundleOf(ManifestMetadataReader.ENABLE_TIMBER_LOGS to true) + val context = fixture.getContext(metaData = bundle) + + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + assertTrue(fixture.options.isEnableTimberLogs) + } + @Test fun `applyMetadata reads metrics enabled and keep default value if not found`() { // Arrange diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 94857b91058..2e5fa0ea45d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -93,6 +93,21 @@ class SentryAndroidOptionsTest { assertTrue(sentryOptions.isEnableScopeSync) } + @Test + fun `Timber logs are disabled by default`() { + val sentryOptions = SentryAndroidOptions() + + assertFalse(sentryOptions.isEnableTimberLogs) + } + + @Test + fun `Timber logs can be enabled`() { + val sentryOptions = SentryAndroidOptions() + sentryOptions.isEnableTimberLogs = true + + assertTrue(sentryOptions.isEnableTimberLogs) + } + @Test fun `attach screenshots disabled by default for Android`() { val sentryOptions = SentryAndroidOptions() diff --git a/sentry-android-timber/api/sentry-android-timber.api b/sentry-android-timber/api/sentry-android-timber.api index 8ae2f49c28d..ad5c909836e 100644 --- a/sentry-android-timber/api/sentry-android-timber.api +++ b/sentry-android-timber/api/sentry-android-timber.api @@ -11,7 +11,10 @@ public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/ public fun ()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 (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;Z)V + public fun (Z)V public fun close ()V + public final fun getEnableLogs ()Z public final fun getMinBreadcrumbLevel ()Lio/sentry/SentryLevel; public final fun getMinEventLevel ()Lio/sentry/SentryLevel; public final fun getMinLogsLevel ()Lio/sentry/SentryLogLevel; @@ -21,6 +24,7 @@ public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/ public final class io/sentry/android/timber/SentryTimberTree : timber/log/Timber$Tree { 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 (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;Z)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 521fe15127a..aa71fa74710 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 @@ -18,6 +18,22 @@ public class SentryTimberIntegration( public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, public val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Integration, Closeable { + public var enableLogs: Boolean = false + private set + + public constructor(enableLogs: Boolean) : this() { + this.enableLogs = enableLogs + } + + public constructor( + minEventLevel: SentryLevel, + minBreadcrumbLevel: SentryLevel, + minLogsLevel: SentryLogLevel, + enableLogs: Boolean, + ) : this(minEventLevel, minBreadcrumbLevel, minLogsLevel) { + this.enableLogs = enableLogs + } + private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger @@ -31,7 +47,7 @@ public class SentryTimberIntegration( override fun register(scopes: IScopes, options: SentryOptions) { logger = options.logger - tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) + tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel, enableLogs) 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 61b1f99fb16..f63dd7b66d0 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 @@ -20,6 +20,18 @@ public class SentryTimberTree( private val minBreadcrumbLevel: SentryLevel, private val minLogLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Timber.Tree() { + private var enableLogs: Boolean = false + + public constructor( + scopes: IScopes, + minEventLevel: SentryLevel, + minBreadcrumbLevel: SentryLevel, + minLogLevel: SentryLogLevel, + enableLogs: Boolean, + ) : this(scopes, minEventLevel, minBreadcrumbLevel, minLogLevel) { + this.enableLogs = enableLogs + } + private val pendingTag = ThreadLocal() private fun retrieveTag(): String? { @@ -185,7 +197,9 @@ public class SentryTimberTree( captureEvent(level, tag, sentryMessage, throwable) addBreadcrumb(level, sentryMessage, throwable) - addLog(logLevel, message, tag, throwable, *args) + if (enableLogs) { + addLog(logLevel, message, tag, throwable, *args) + } } /** do not log if it's lower than min. required level. */ 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 7c21eca8ef0..5aae1be93fe 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 @@ -1,5 +1,6 @@ package io.sentry.android.timber +import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.ScopesAdapter @@ -7,33 +8,52 @@ import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryLogLevel import io.sentry.SentryOptions +import io.sentry.logger.ILoggerApi +import io.sentry.logger.SentryLogParameters import io.sentry.protocol.SdkVersion import io.sentry.transport.ITransport import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import timber.log.Timber class SentryTimberIntegrationTest { private class Fixture { val scopes = mock() + val logs = mock() val options = SentryOptions().apply { sdkVersion = SdkVersion("test", "1.2.3") } + init { + whenever(scopes.logger()).thenReturn(logs) + } + fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, + enableLogs: Boolean? = null, ): SentryTimberIntegration = - SentryTimberIntegration( - minEventLevel = minEventLevel, - minBreadcrumbLevel = minBreadcrumbLevel, - minLogsLevel = minLogsLevel, - ) + if (enableLogs == null) { + SentryTimberIntegration( + minEventLevel = minEventLevel, + minBreadcrumbLevel = minBreadcrumbLevel, + minLogsLevel = minLogsLevel, + ) + } else { + SentryTimberIntegration( + minEventLevel = minEventLevel, + minBreadcrumbLevel = minBreadcrumbLevel, + minLogsLevel = minLogsLevel, + enableLogs = enableLogs, + ) + } } private val fixture = Fixture() @@ -64,6 +84,30 @@ class SentryTimberIntegrationTest { verify(fixture.scopes).captureEvent(any()) } + @Test + fun `Manual integration defaults logs to disabled while capturing events and breadcrumbs`() { + val sut = fixture.getSut() + sut.register(fixture.scopes, fixture.options) + + assertFalse(sut.enableLogs) + Timber.e("message") + + verify(fixture.scopes).captureEvent(any()) + verify(fixture.scopes).addBreadcrumb(any()) + verifyNoInteractions(fixture.logs) + } + + @Test + fun `Manual integration captures logs when enabled`() { + val sut = fixture.getSut(enableLogs = true) + sut.register(fixture.scopes, fixture.options) + + assertTrue(sut.enableLogs) + Timber.i("message") + + verify(fixture.logs).log(any(), any(), any()) + } + @Test fun `Integrations removes a tree from Timber on close integration`() { val sut = fixture.getSut() 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 f1d6d5a51bd..457ab31d33c 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 @@ -31,11 +31,16 @@ class SentryTimberTreeTest { minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, + enableLogs: Boolean? = true, ): SentryTimberTree { logs = mock() scopes = mock() whenever(scopes.logger()).thenReturn(logs) - return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) + return if (enableLogs == null) { + SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) + } else { + SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel, enableLogs) + } } } @@ -296,6 +301,17 @@ class SentryTimberTreeTest { sut.d("test %s, %s", 1, 1) } + @Test + fun `Tree defaults logs to disabled while capturing events and breadcrumbs`() { + val sut = fixture.getSut(enableLogs = null) + + sut.e("message") + + verify(fixture.scopes).captureEvent(any()) + verify(fixture.scopes).addBreadcrumb(any()) + verifyNoInteractions(fixture.logs) + } + @Test fun `Tree adds a log with message and arguments, when provided`() { val sut = fixture.getSut()