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 a942d51878..24d3831ed7 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 72e3d68dc0..7b9ed6ea14 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 a0774abddf..b5900f815b 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 3174bd2824..7acccda2dd 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 7b9ed6ea14..ef85e0afe6 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 ef85e0afe6..1c35b022fa 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 2d71f67570..c6ebf62fa8 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 0659ecf967..46d54e691c 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 dda29c61d8..3637e61c8d 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 8bb85aa085..9d3114d696 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 30afeb928d..73ea7f879e 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 b5900f815b..9c3bc89ed3 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 c6ebf62fa8..8ae2f49c28 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 24d3831ed7..7305719467 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 784dd3cfe7..cd64b3151a 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 a7d0b7010a..b5749c6144 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 d338fd2262..521fe15127 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 eb5129b8ad..3e4d6d021f 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 82d441c80b..43a45da7bb 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 bbfaf74523..54b77caf6c 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 3e4d6d021f..e0370d9674 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 54b77caf6c..e77e33e42d 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 e0370d9674..c0e996bdba 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) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy