Skip to content

Send Timber logs through Sentry Logs #4490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: feat/logcat-sentry-logs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 5 additions & 3 deletions sentry-android-timber/api/sentry-android-timber.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ public final class io/sentry/android/timber/BuildConfig {

public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> ()V
public fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V
public synthetic fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V
public synthetic fun <init> (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 <init> (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V
public synthetic fun <init> (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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this default value so we don't introduce breaking changes

) : Timber.Tree() {
private val pendingTag = ThreadLocal<String?>()

Expand Down Expand Up @@ -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()) {
Expand All @@ -239,6 +242,7 @@ public class SentryTimberTree(

captureEvent(level, tag, sentryMessage, throwable)
addBreadcrumb(level, sentryMessage, throwable)
addLog(logLevel, message, throwable, *args)
}

/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -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.
Expand All @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,13 +23,19 @@ import kotlin.test.assertNull
class SentryTimberTreeTest {

private class Fixture {
val scopes = mock<IScopes>()
val scopes = mock<Scopes>()
val logs = mock<ILoggerApi>()

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)
}
}

Expand Down Expand Up @@ -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<String>(), 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")
)
}
}
Loading