diff --git a/CHANGELOG.md b/CHANGELOG.md index bf354f1c460..baf158193ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Fixes + +- 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). + ## 8.16.1-alpha.2 ### Fixes 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..ebf99e2b970 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,10 @@ 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 org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,73 +46,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(); + // Check if logs are enabled before doing expensive operations + if (!scopes.getOptions().getLogs().isEnabled()) { + return; + } + final @Nullable String trMessage = tr != null ? tr.getMessage() : null; + if (tr == null || trMessage == null) { + scopes.logger().log(level, msg); + } else { + scopes.logger().log(level, msg != null ? (msg + "\n" + trMessage) : trMessage); + } + } + 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/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 7aebabfc434..5917d44d11d 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 @@ -269,6 +269,9 @@ class InternalSentrySdkTest { // then modifications should not be reflected 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 127f0fb3ad2..1e58bbf6e7a 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,185 +5,215 @@ 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 java.lang.RuntimeException -import kotlin.test.BeforeTest +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue import org.junit.runner.RunWith @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) { val metadata = Bundle().apply { 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) } } } private val fixture = Fixture() - @BeforeTest - fun `set up`() { - Sentry.close() + @AfterTest + fun `clean up`() { AppStartMetrics.getInstance().clear() ContextUtils.resetInstance() - breadcrumbs.clear() - - fixture.initSut { - it.beforeBreadcrumb = - SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> - breadcrumbs.add(breadcrumb) - breadcrumb - } - } - - 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") + Sentry.close() + fixture.breadcrumbs.clear() + fixture.logs.clear() } @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.message}", 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.message}", 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.message}", 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.message}", 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.message}", 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.message}", SentryLogLevel.FATAL) + } + + @Test + fun `do not send logs if logs is disabled`() { + fixture.initSut { it.logs.isEnabled = false } - 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")) + 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) + + 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 { it.message?.contains("SentryLogcatAdapter") ?: false }.size, + 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) + } }