From 3572c9fa1836c49bccbbe6fd736bd4ef5e367480 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 16:47:13 +0200 Subject: [PATCH 01/13] SentryLogcatAdapter now forwards output to Sentry Logs, if enabled --- .../android/core/SentryLogcatAdapter.java | 35 ++++ .../android/core/SentryLogcatAdapterTest.kt | 192 +++++++++++------- 2 files changed, 148 insertions(+), 79 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index a942d51878c..24d3831ed7f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -2,8 +2,12 @@ import android.util.Log; import io.sentry.Breadcrumb; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryLevel; +import io.sentry.SentryLogLevel; +import java.io.PrintWriter; +import java.io.StringWriter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,73 +48,104 @@ private static void addAsBreadcrumb( Sentry.addBreadcrumb(breadcrumb); } + private static void addAsLog( + @NotNull final SentryLogLevel level, + @Nullable final String msg, + @Nullable final Throwable tr) { + final @NotNull ScopesAdapter scopes = ScopesAdapter.getInstance(); + if (tr == null) { + scopes.logger().log(level, msg); + } else { + StringWriter sw = new StringWriter(256); + PrintWriter pw = new PrintWriter(sw, false); + tr.printStackTrace(pw); + pw.flush(); + scopes.logger().log(level, msg != null ? (msg + "\n" + sw.toString()) : sw.toString()); + pw.close(); + } + } + public static int v(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg); + addAsLog(SentryLogLevel.TRACE, msg, null); return Log.v(tag, msg); } public static int v(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg, tr); + addAsLog(SentryLogLevel.TRACE, msg, tr); return Log.v(tag, msg, tr); } public static int d(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg); + addAsLog(SentryLogLevel.DEBUG, msg, null); return Log.d(tag, msg); } public static int d(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg, tr); + addAsLog(SentryLogLevel.DEBUG, msg, tr); return Log.d(tag, msg, tr); } public static int i(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.INFO, msg); + addAsLog(SentryLogLevel.INFO, msg, null); return Log.i(tag, msg); } public static int i(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.INFO, msg, tr); + addAsLog(SentryLogLevel.INFO, msg, tr); return Log.i(tag, msg, tr); } public static int w(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.WARNING, msg); + addAsLog(SentryLogLevel.WARN, msg, null); return Log.w(tag, msg); } public static int w(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.WARNING, msg, tr); + addAsLog(SentryLogLevel.WARN, msg, tr); return Log.w(tag, msg, tr); } public static int w(@Nullable String tag, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.WARNING, tr); + addAsLog(SentryLogLevel.WARN, null, tr); return Log.w(tag, tr); } public static int e(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg); + addAsLog(SentryLogLevel.ERROR, msg, null); return Log.e(tag, msg); } public static int e(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg, tr); + addAsLog(SentryLogLevel.ERROR, msg, tr); return Log.e(tag, msg, tr); } public static int wtf(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg); + addAsLog(SentryLogLevel.FATAL, msg, null); return Log.wtf(tag, msg); } public static int wtf(@Nullable String tag, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, tr); + addAsLog(SentryLogLevel.FATAL, null, tr); return Log.wtf(tag, tr); } public static int wtf(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg, tr); + addAsLog(SentryLogLevel.FATAL, msg, tr); return Log.wtf(tag, msg, tr); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 72e3d68dc09..7b9ed6ea146 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -5,22 +5,26 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.SentryLogEvent +import io.sentry.SentryLogLevel import io.sentry.SentryOptions import io.sentry.android.core.performance.AppStartMetrics import org.junit.runner.RunWith -import java.lang.RuntimeException +import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class SentryLogcatAdapterTest { - private val breadcrumbs = mutableListOf() private val tag = "my-tag" private val commonMsg = "SentryLogcatAdapter" private val throwable = RuntimeException("Test Exception") class Fixture { + val breadcrumbs = mutableListOf() + val logs = mutableListOf() fun initSut( options: Sentry.OptionsConfiguration? = null @@ -29,9 +33,17 @@ class SentryLogcatAdapterTest { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) - when { - options != null -> initForTest(mockContext, options) - else -> initForTest(mockContext) + initForTest(mockContext) { + it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> + breadcrumbs.add(breadcrumb) + breadcrumb + } + it.logs.isEnabled = true + it.logs.beforeSend = SentryOptions.Logs.BeforeSendLogCallback { logEvent -> + logs.add(logEvent) + logEvent + } + options?.configure(it) } } } @@ -43,144 +55,166 @@ class SentryLogcatAdapterTest { Sentry.close() AppStartMetrics.getInstance().clear() ContextUtils.resetInstance() - breadcrumbs.clear() - - fixture.initSut { - it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> - breadcrumbs.add(breadcrumb) - breadcrumb - } - } + fixture.breadcrumbs.clear() + fixture.logs.clear() + } - SentryLogcatAdapter.v(tag, "$commonMsg verbose") - SentryLogcatAdapter.i(tag, "$commonMsg info") - SentryLogcatAdapter.d(tag, "$commonMsg debug") - SentryLogcatAdapter.w(tag, "$commonMsg warning") - SentryLogcatAdapter.e(tag, "$commonMsg error") - SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + @AfterTest + fun `clean up`() { + AppStartMetrics.getInstance().clear() + ContextUtils.resetInstance() + Sentry.close() } @Test fun `verbose log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.DEBUG && it.message?.contains("verbose") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("verbose") == true) + fixture.initSut() + SentryLogcatAdapter.v(tag, "$commonMsg verbose") + fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg verbose", SentryLogLevel.TRACE) } @Test fun `info log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.INFO && it.message?.contains("info") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("info") == true) + fixture.initSut() + SentryLogcatAdapter.i(tag, "$commonMsg info") + fixture.breadcrumbs.first().assert(tag, "$commonMsg info", SentryLevel.INFO) + fixture.logs.first().assert("$commonMsg info", SentryLogLevel.INFO) } @Test fun `debug log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.DEBUG && it.message?.contains("debug") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("debug") == true) + fixture.initSut() + SentryLogcatAdapter.d(tag, "$commonMsg debug") + fixture.breadcrumbs.first().assert(tag, "$commonMsg debug", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg debug", SentryLogLevel.DEBUG) } @Test fun `warning log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.WARNING && it.message?.contains("warning") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("warning") == true) + fixture.initSut() + SentryLogcatAdapter.w(tag, "$commonMsg warning") + fixture.breadcrumbs.first().assert(tag, "$commonMsg warning", SentryLevel.WARNING) + fixture.logs.first().assert("$commonMsg warning", SentryLogLevel.WARN) } @Test fun `error log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.ERROR && it.message?.contains("error") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("error") == true) + fixture.initSut() + SentryLogcatAdapter.e(tag, "$commonMsg error") + fixture.breadcrumbs.first().assert(tag, "$commonMsg error", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg error", SentryLogLevel.ERROR) } @Test fun `wtf log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.ERROR && it.message?.contains("wtf") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("wtf") == true) + fixture.initSut() + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg wtf", SentryLogLevel.FATAL) } @Test fun `e log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.ERROR, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg error exception", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) } @Test fun `v log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.DEBUG, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg verbose exception\n${throwable.stackTraceToString()}", SentryLogLevel.TRACE) } @Test fun `i log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.INFO, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg info exception", SentryLevel.INFO) + fixture.logs.first().assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) } @Test fun `d log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.DEBUG, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg debug exception", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) } @Test fun `w log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.WARNING, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) + fixture.logs.first().assert("$commonMsg warning exception\n${throwable.stackTraceToString()}", SentryLogLevel.WARN) } @Test fun `wtf log throwable has expected content`() { + fixture.initSut() + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) + fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf exception", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) + } + + @Test + fun `do not send logs if logs is disabled`() { + fixture.initSut { it.logs.isEnabled = false } + + SentryLogcatAdapter.v(tag, "$commonMsg verbose") + SentryLogcatAdapter.i(tag, "$commonMsg info") + SentryLogcatAdapter.d(tag, "$commonMsg debug") + SentryLogcatAdapter.w(tag, "$commonMsg warning") + SentryLogcatAdapter.e(tag, "$commonMsg error") + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) + SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) + SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) + SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) + SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.ERROR, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + assertTrue(fixture.logs.isEmpty()) } @Test fun `logs add correct number of breadcrumb`() { + fixture.initSut() + SentryLogcatAdapter.v(tag, commonMsg) + SentryLogcatAdapter.d(tag, commonMsg) + SentryLogcatAdapter.i(tag, commonMsg) + SentryLogcatAdapter.w(tag, commonMsg) + SentryLogcatAdapter.e(tag, commonMsg) + SentryLogcatAdapter.wtf(tag, commonMsg) assertEquals( 6, - breadcrumbs.filter { + fixture.breadcrumbs.filter { it.message?.contains("SentryLogcatAdapter") ?: false }.size ) } + + private fun Breadcrumb.assert( + expectedTag: String, + expectedMessage: String, + expectedLevel: SentryLevel + ) { + assertEquals(expectedMessage, message) + assertEquals(expectedTag, data["tag"]) + assertEquals(expectedLevel, level) + assertEquals("Logcat", category) + } + + private fun SentryLogEvent.assert( + expectedMessage: String, + expectedLevel: SentryLogLevel + ) { + assertEquals(expectedMessage, body) + assertEquals(expectedLevel, level) + } } From ead3f38c938e706ffbabe1031974ec7a8c80b9bc Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 16:51:35 +0200 Subject: [PATCH 02/13] updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0774abddf7..b5900f815bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- 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. + ## 8.13.3 ### Fixes From c8e6a6ebb5ad1889479bf125f6351681a7037fa3 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 19:09:13 +0200 Subject: [PATCH 03/13] fix tests --- .../io/sentry/android/core/InternalSentrySdkTest.kt | 3 +++ .../io/sentry/android/core/SentryLogcatAdapterTest.kt | 11 ++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 3174bd2824e..7acccda2dda 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -241,6 +241,9 @@ class InternalSentrySdkTest { Sentry.configureScope { scope -> assertEquals(3, scope.breadcrumbs.size) } + + // Ensure we don't interfere with other tests + Sentry.configureScope(ScopeType.GLOBAL) { scope -> scope.clear() } } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 7b9ed6ea146..ef85e0afe68 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -50,20 +50,13 @@ class SentryLogcatAdapterTest { private val fixture = Fixture() - @BeforeTest - fun `set up`() { - Sentry.close() - AppStartMetrics.getInstance().clear() - ContextUtils.resetInstance() - fixture.breadcrumbs.clear() - fixture.logs.clear() - } - @AfterTest fun `clean up`() { AppStartMetrics.getInstance().clear() ContextUtils.resetInstance() Sentry.close() + fixture.breadcrumbs.clear() + fixture.logs.clear() } @Test From eacca576dc27c84c415a8a82fc69fdb3c51087e0 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 12 Jun 2025 17:10:50 +0000 Subject: [PATCH 04/13] Format code --- .../test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index ef85e0afe68..1c35b022faa 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -11,7 +11,6 @@ import io.sentry.SentryOptions import io.sentry.android.core.performance.AppStartMetrics import org.junit.runner.RunWith import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue From d19a5532f689eaa33c80fd514143dc80921a0594 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 13 Jun 2025 16:47:38 +0200 Subject: [PATCH 05/13] logs captured by Timber are now sent as Sentry Logs --- .../api/sentry-android-timber.api | 8 +- .../android/timber/SentryTimberIntegration.kt | 6 +- .../sentry/android/timber/SentryTimberTree.kt | 47 +++++++++- .../timber/SentryTimberIntegrationTest.kt | 11 ++- .../android/timber/SentryTimberTreeTest.kt | 93 ++++++++++++++++++- 5 files changed, 152 insertions(+), 13 deletions(-) diff --git a/sentry-android-timber/api/sentry-android-timber.api b/sentry-android-timber/api/sentry-android-timber.api index 2d71f67570e..c6ebf62fa8e 100644 --- a/sentry-android-timber/api/sentry-android-timber.api +++ b/sentry-android-timber/api/sentry-android-timber.api @@ -9,22 +9,24 @@ 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 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 public fun e (Ljava/lang/String;[Ljava/lang/Object;)V public fun e (Ljava/lang/Throwable;)V public fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V + public final fun getMinLogsLevel ()Lio/sentry/SentryLogLevel; public fun i (Ljava/lang/String;[Ljava/lang/Object;)V public fun i (Ljava/lang/Throwable;)V public fun i (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 0659ecf9670..46d54e691ca 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 @@ -16,7 +17,8 @@ import java.io.Closeable */ public class SentryTimberIntegration( public val minEventLevel: SentryLevel = SentryLevel.ERROR, - public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO + public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, + public val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO ) : Integration, Closeable { private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger @@ -31,7 +33,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 dda29c61d88..3637e61c8dd 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 @@ -15,7 +16,8 @@ import timber.log.Timber public class SentryTimberTree( private val scopes: IScopes, private val minEventLevel: SentryLevel, - private val minBreadcrumbLevel: SentryLevel + private val minBreadcrumbLevel: SentryLevel, + private val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO ) : Timber.Tree() { private val pendingTag = ThreadLocal() @@ -229,6 +231,7 @@ public class SentryTimberTree( } val level = getSentryLevel(priority) + val logLevel = getSentryLogLevel(priority) val sentryMessage = Message().apply { this.message = message if (!message.isNullOrEmpty() && args.isNotEmpty()) { @@ -239,6 +242,7 @@ public class SentryTimberTree( captureEvent(level, tag, sentryMessage, throwable) addBreadcrumb(level, sentryMessage, throwable) + addLog(logLevel, message, throwable, *args) } /** @@ -249,6 +253,14 @@ public class SentryTimberTree( 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 */ @@ -300,6 +312,23 @@ 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, minLogsLevel)) { + val throwableMsg = throwable?.message + when { + 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. @@ -315,4 +344,20 @@ public class SentryTimberTree( 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 8bb85aa085c..9d3114d6969 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 org.mockito.kotlin.any @@ -23,11 +24,13 @@ class SentryTimberIntegrationTest { fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, - minBreadcrumbLevel: SentryLevel = SentryLevel.INFO + minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO ): SentryTimberIntegration { return SentryTimberIntegration( minEventLevel = minEventLevel, - minBreadcrumbLevel = minBreadcrumbLevel + minBreadcrumbLevel = minBreadcrumbLevel, + minLogsLevel = minLogsLevel ) } } @@ -82,12 +85,14 @@ class SentryTimberIntegrationTest { fun `Integrations pass the right min levels`() { val sut = fixture.getSut( minEventLevel = SentryLevel.INFO, - minBreadcrumbLevel = SentryLevel.DEBUG + 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 30afeb928d7..73ea7f879ec 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,13 +1,18 @@ 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 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 import kotlin.test.BeforeTest import kotlin.test.Test @@ -18,13 +23,19 @@ import kotlin.test.assertNull 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 + minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO ): SentryTimberTree { - return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel) + return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) } } @@ -281,4 +292,78 @@ 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") + ) + } } From c8f381440b78981e994033c92ce50e60cfeed2a0 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 13 Jun 2025 16:52:49 +0200 Subject: [PATCH 06/13] updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5900f815bf..9c3bc89ed37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features +- 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. From d82553fdb155403f56323d2e0296b2c868f3c791 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 13 Jun 2025 17:41:09 +0200 Subject: [PATCH 07/13] fixed api --- sentry-android-timber/api/sentry-android-timber.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-timber/api/sentry-android-timber.api b/sentry-android-timber/api/sentry-android-timber.api index c6ebf62fa8e..8ae2f49c28d 100644 --- a/sentry-android-timber/api/sentry-android-timber.api +++ b/sentry-android-timber/api/sentry-android-timber.api @@ -20,13 +20,13 @@ 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 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 public fun e (Ljava/lang/String;[Ljava/lang/Object;)V public fun e (Ljava/lang/Throwable;)V public fun e (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V - public final fun getMinLogsLevel ()Lio/sentry/SentryLogLevel; public fun i (Ljava/lang/String;[Ljava/lang/Object;)V public fun i (Ljava/lang/Throwable;)V public fun i (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V From e566455f8ffdadcec75fe53c50c3f79a9763b208 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 18 Jun 2025 16:59:30 +0200 Subject: [PATCH 08/13] add log enabled check in logcat adapter --- .../main/java/io/sentry/android/core/SentryLogcatAdapter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index 24d3831ed7f..73057194678 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -53,6 +53,10 @@ private static void addAsLog( @Nullable final String msg, @Nullable final Throwable tr) { final @NotNull ScopesAdapter scopes = ScopesAdapter.getInstance(); + // Check if logs are enabled before doing expensive operations + if (!scopes.getOptions().getLogs().isEnabled()) { + return; + } if (tr == null) { scopes.logger().log(level, msg); } else { From 77c5c538e95cc25788215e7d175b9706e224c0c7 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 20 Jun 2025 11:52:46 +0200 Subject: [PATCH 09/13] updated changelog with doc reference --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784dd3cfe71..cd64b3151a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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). - No longer send out empty log envelopes ([#4497](https://github.com/getsentry/sentry-java/pull/4497)) ### Dependencies From a42eb855652a3baf35fa68c3361dbeefa5402d6e Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 30 Jun 2025 18:40:44 +0200 Subject: [PATCH 10/13] merged main --- .../android/core/SentryLogcatAdapterTest.kt | 187 ++++++++++-------- 1 file changed, 100 insertions(+), 87 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index a7d0b7010a7..b5749c61447 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -18,144 +18,162 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SentryLogcatAdapterTest { - private val tag = "my-tag" - private val commonMsg = "SentryLogcatAdapter" - private val throwable = RuntimeException("Test Exception") + private val tag = "my-tag" + private val commonMsg = "SentryLogcatAdapter" + private val throwable = RuntimeException("Test Exception") class Fixture { - val breadcrumbs = mutableListOf() - val logs = mutableListOf() + val breadcrumbs = mutableListOf() + val logs = mutableListOf() - fun initSut(options: Sentry.OptionsConfiguration? = null) { + fun initSut(options: Sentry.OptionsConfiguration? = null) { val metadata = Bundle().apply { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) - initForTest(mockContext) { - it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> - breadcrumbs.add(breadcrumb) - breadcrumb - } - it.logs.isEnabled = true - it.logs.beforeSend = SentryOptions.Logs.BeforeSendLogCallback { logEvent -> - logs.add(logEvent) - logEvent - } - options?.configure(it) + initForTest(mockContext) { + it.beforeBreadcrumb = + SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> + breadcrumbs.add(breadcrumb) + breadcrumb } + it.logs.isEnabled = true + it.logs.beforeSend = + SentryOptions.Logs.BeforeSendLogCallback { logEvent -> + logs.add(logEvent) + logEvent + } + options?.configure(it) } } } private val fixture = Fixture() - @AfterTest - fun `clean up`() { - AppStartMetrics.getInstance().clear() - ContextUtils.resetInstance() - Sentry.close() - fixture.breadcrumbs.clear() - fixture.logs.clear() - } - + @AfterTest + fun `clean up`() { + AppStartMetrics.getInstance().clear() + ContextUtils.resetInstance() + Sentry.close() + fixture.breadcrumbs.clear() + fixture.logs.clear() + } -@Test -fun `verbose log message has expected content`() { + @Test + fun `verbose log message has expected content`() { fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose") fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose", SentryLevel.DEBUG) fixture.logs.first().assert("$commonMsg verbose", SentryLogLevel.TRACE) -} + } -@Test -fun `info log message has expected content`() { + @Test + fun `info log message has expected content`() { fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info") fixture.breadcrumbs.first().assert(tag, "$commonMsg info", SentryLevel.INFO) fixture.logs.first().assert("$commonMsg info", SentryLogLevel.INFO) -} + } -@Test -fun `debug log message has expected content`() { + @Test + fun `debug log message has expected content`() { fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug") fixture.breadcrumbs.first().assert(tag, "$commonMsg debug", SentryLevel.DEBUG) fixture.logs.first().assert("$commonMsg debug", SentryLogLevel.DEBUG) -} + } -@Test -fun `warning log message has expected content`() { + @Test + fun `warning log message has expected content`() { fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning") fixture.breadcrumbs.first().assert(tag, "$commonMsg warning", SentryLevel.WARNING) fixture.logs.first().assert("$commonMsg warning", SentryLogLevel.WARN) -} + } -@Test -fun `error log message has expected content`() { + @Test + fun `error log message has expected content`() { fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error") fixture.breadcrumbs.first().assert(tag, "$commonMsg error", SentryLevel.ERROR) fixture.logs.first().assert("$commonMsg error", SentryLogLevel.ERROR) -} + } -@Test -fun `wtf log message has expected content`() { + @Test + fun `wtf log message has expected content`() { fixture.initSut() SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf", SentryLevel.ERROR) fixture.logs.first().assert("$commonMsg wtf", SentryLogLevel.FATAL) -} + } -@Test -fun `e log throwable has expected content`() { + @Test + fun `e log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg error exception", SentryLevel.ERROR) - fixture.logs.first().assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) -} + fixture.logs + .first() + .assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) + } -@Test -fun `v log throwable has expected content`() { + @Test + fun `v log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) - fixture.logs.first().assert("$commonMsg verbose exception\n${throwable.stackTraceToString()}", SentryLogLevel.TRACE) -} + fixture.logs + .first() + .assert( + "$commonMsg verbose exception\n${throwable.stackTraceToString()}", + SentryLogLevel.TRACE, + ) + } -@Test -fun `i log throwable has expected content`() { + @Test + fun `i log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg info exception", SentryLevel.INFO) - fixture.logs.first().assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) -} + fixture.logs + .first() + .assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) + } -@Test -fun `d log throwable has expected content`() { + @Test + fun `d log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg debug exception", SentryLevel.DEBUG) - fixture.logs.first().assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) -} + fixture.logs + .first() + .assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) + } -@Test -fun `w log throwable has expected content`() { + @Test + fun `w log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) - fixture.logs.first().assert("$commonMsg warning exception\n${throwable.stackTraceToString()}", SentryLogLevel.WARN) -} + fixture.logs + .first() + .assert( + "$commonMsg warning exception\n${throwable.stackTraceToString()}", + SentryLogLevel.WARN, + ) + } -@Test -fun `wtf log throwable has expected content`() { + @Test + fun `wtf log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf exception", SentryLevel.ERROR) - fixture.logs.first().assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) -} + fixture.logs + .first() + .assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) + } -@Test -fun `do not send logs if logs is disabled`() { + @Test + fun `do not send logs if logs is disabled`() { fixture.initSut { it.logs.isEnabled = false } SentryLogcatAdapter.v(tag, "$commonMsg verbose") @@ -172,10 +190,10 @@ fun `do not send logs if logs is disabled`() { SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) assertTrue(fixture.logs.isEmpty()) -} + } -@Test -fun `logs add correct number of breadcrumb`() { + @Test + fun `logs add correct number of breadcrumb`() { fixture.initSut() SentryLogcatAdapter.v(tag, commonMsg) SentryLogcatAdapter.d(tag, commonMsg) @@ -184,29 +202,24 @@ fun `logs add correct number of breadcrumb`() { SentryLogcatAdapter.e(tag, commonMsg) SentryLogcatAdapter.wtf(tag, commonMsg) assertEquals( - 6, - fixture.breadcrumbs.filter { - it.message?.contains("SentryLogcatAdapter") ?: false - }.size + 6, + fixture.breadcrumbs.filter { it.message?.contains("SentryLogcatAdapter") ?: false }.size, ) -} + } -private fun Breadcrumb.assert( + private fun Breadcrumb.assert( expectedTag: String, expectedMessage: String, - expectedLevel: SentryLevel -) { + expectedLevel: SentryLevel, + ) { assertEquals(expectedMessage, message) assertEquals(expectedTag, data["tag"]) assertEquals(expectedLevel, level) assertEquals("Logcat", category) -} + } -private fun SentryLogEvent.assert( - expectedMessage: String, - expectedLevel: SentryLogLevel -) { + private fun SentryLogEvent.assert(expectedMessage: String, expectedLevel: SentryLogLevel) { assertEquals(expectedMessage, body) assertEquals(expectedLevel, level) -} + } } From 86007fb0ad97644bd8fb4ab5d5d930ca323d393b Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 30 Jun 2025 18:47:30 +0200 Subject: [PATCH 11/13] merged main --- .../android/timber/SentryTimberIntegration.kt | 2 +- .../sentry/android/timber/SentryTimberTree.kt | 69 +++++----- .../timber/SentryTimberIntegrationTest.kt | 10 +- .../android/timber/SentryTimberTreeTest.kt | 120 ++++++++---------- 4 files changed, 91 insertions(+), 110 deletions(-) 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 d338fd22629..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 @@ -16,7 +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 + public val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Integration, Closeable { private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger 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 eb5129b8ad3..3e4d6d021f0 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 @@ -15,7 +15,7 @@ public class SentryTimberTree( private val scopes: IScopes, private val minEventLevel: SentryLevel, private val minBreadcrumbLevel: SentryLevel, - private val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO + private val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Timber.Tree() { private val pendingTag = ThreadLocal() @@ -170,7 +170,7 @@ public class SentryTimberTree( } val level = getSentryLevel(priority) - val logLevel = getSentryLogLevel(priority) + val logLevel = getSentryLogLevel(priority) val sentryMessage = Message().apply { this.message = message @@ -182,18 +182,16 @@ public class SentryTimberTree( captureEvent(level, tag, sentryMessage, throwable) addBreadcrumb(level, sentryMessage, throwable) - addLog(logLevel, message, throwable, *args) + 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 + /** 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( @@ -237,22 +235,22 @@ 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, minLogsLevel)) { - val throwableMsg = throwable?.message - when { - msg != null -> scopes.logger().log(sentryLogLevel, msg, *args) - throwableMsg != null -> scopes.logger().log(sentryLogLevel, throwableMsg, *args) - } - } + /** Send a Sentry Logs */ + private fun addLog( + sentryLogLevel: SentryLogLevel, + msg: String?, + throwable: Throwable?, + vararg args: Any?, + ) { + // checks the log level + if (isLoggable(sentryLogLevel, minLogsLevel)) { + val throwableMsg = throwable?.message + when { + 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 = @@ -266,19 +264,16 @@ public class SentryTimberTree( 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 - } + /** 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 82d441c80bf..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 @@ -22,12 +22,12 @@ class SentryTimberIntegrationTest { fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, - minLogsLevel: SentryLogLevel = SentryLogLevel.INFO + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ): SentryTimberIntegration = SentryTimberIntegration( minEventLevel = minEventLevel, minBreadcrumbLevel = minBreadcrumbLevel, - minLogsLevel = minLogsLevel + minLogsLevel = minLogsLevel, ) } @@ -82,9 +82,9 @@ class SentryTimberIntegrationTest { fun `Integrations pass the right min levels`() { val sut = fixture.getSut( - minEventLevel = SentryLevel.INFO, - minBreadcrumbLevel = SentryLevel.DEBUG, - minLogsLevel = SentryLogLevel.TRACE + minEventLevel = SentryLevel.INFO, + minBreadcrumbLevel = SentryLevel.DEBUG, + minLogsLevel = SentryLogLevel.TRACE, ) sut.register(fixture.scopes, fixture.options) 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 bbfaf74523e..54b77caf6c0 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 @@ -23,16 +23,16 @@ import timber.log.Timber class SentryTimberTreeTest { private class Fixture { val scopes = mock() - val logs = mock() + val logs = mock() - init { - whenever(scopes.logger()).thenReturn(logs) - } + init { + whenever(scopes.logger()).thenReturn(logs) + } fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO, - minLogsLevel: SentryLogLevel = SentryLogLevel.INFO + minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, ): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel) } @@ -243,77 +243,63 @@ class SentryTimberTreeTest { 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) + @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) - ) - } + 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 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 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 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") + @Test + fun `Tree adds an info log`() { + val sut = fixture.getSut() + sut.i("message") - verify(fixture.logs).log( - eq(SentryLogLevel.INFO), - eq("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")) + @Test + fun `Tree adds an error log`() { + val sut = fixture.getSut() + sut.e(Throwable("test")) - verify(fixture.logs).log( - eq(SentryLogLevel.ERROR), - eq("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 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")) + @Test + fun `Tree logs throwable`() { + val sut = fixture.getSut() + sut.e(Throwable("throwable message")) - verify(fixture.logs).log( - eq(SentryLogLevel.ERROR), - eq("throwable message") - ) - } + verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("throwable message")) + } } From 4563eaead1396b8e9bdf465064403ef1e648e870 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 7 Jul 2025 12:26:35 +0200 Subject: [PATCH 12/13] Added case for timber logging both a message and a throwable --- .../java/io/sentry/android/timber/SentryTimberTree.kt | 5 +++-- .../java/io/sentry/android/timber/SentryTimberTreeTest.kt | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) 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 3e4d6d021f0..e0370d9674b 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 @@ -15,7 +15,7 @@ public class SentryTimberTree( private val scopes: IScopes, private val minEventLevel: SentryLevel, private val minBreadcrumbLevel: SentryLevel, - private val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO, + private val minLogLevel: SentryLogLevel = SentryLogLevel.INFO, ) : Timber.Tree() { private val pendingTag = ThreadLocal() @@ -243,9 +243,10 @@ public class SentryTimberTree( vararg args: Any?, ) { // checks the log level - if (isLoggable(sentryLogLevel, minLogsLevel)) { + 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) } 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 54b77caf6c0..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 @@ -302,4 +302,12 @@ class SentryTimberTreeTest { 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")) + } } From d4c9a7c2947e755ad599e7ec674f7b137a8587a9 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 7 Jul 2025 12:26:52 +0200 Subject: [PATCH 13/13] Added case for timber logging both a message and a throwable --- .../src/main/java/io/sentry/android/timber/SentryTimberTree.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 e0370d9674b..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 @@ -246,7 +246,8 @@ public class SentryTimberTree( if (isLoggable(sentryLogLevel, minLogLevel)) { val throwableMsg = throwable?.message when { - msg != null && throwableMsg != null -> scopes.logger().log(sentryLogLevel, "$msg\n$throwableMsg", *args) + 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) }