Skip to content

Commit d1e46e5

Browse files
authored
Send Timber logs through Sentry Logs (#4490)
* logs captured by Timber are now sent as Sentry Logs
1 parent bc15877 commit d1e46e5

File tree

6 files changed

+142
-8
lines changed

6 files changed

+142
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Fixes
66

7+
- Send Timber logs through Sentry Logs ([#4490](https://github.com/getsentry/sentry-java/pull/4490))
8+
- 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.
9+
- The SDK will automatically detect Timber and use it to send logs to Sentry.
710
- Send logcat through Sentry Logs ([#4487](https://github.com/getsentry/sentry-java/pull/4487))
811
- 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.
912
- To set the logcat level check the [Logcat integration documentation](https://docs.sentry.io/platforms/android/integrations/logcat/#configure).

sentry-android-timber/api/sentry-android-timber.api

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ public final class io/sentry/android/timber/BuildConfig {
99

1010
public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/Integration, java/io/Closeable {
1111
public fun <init> ()V
12-
public fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V
13-
public synthetic fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
12+
public fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V
13+
public synthetic fun <init> (Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
1414
public fun close ()V
1515
public final fun getMinBreadcrumbLevel ()Lio/sentry/SentryLevel;
1616
public final fun getMinEventLevel ()Lio/sentry/SentryLevel;
17+
public final fun getMinLogsLevel ()Lio/sentry/SentryLogLevel;
1718
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
1819
}
1920

2021
public final class io/sentry/android/timber/SentryTimberTree : timber/log/Timber$Tree {
21-
public fun <init> (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V
22+
public fun <init> (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;)V
23+
public synthetic fun <init> (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;Lio/sentry/SentryLogLevel;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
2224
public fun d (Ljava/lang/String;[Ljava/lang/Object;)V
2325
public fun d (Ljava/lang/Throwable;)V
2426
public fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V

sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.sentry.IScopes
55
import io.sentry.Integration
66
import io.sentry.SentryIntegrationPackageStorage
77
import io.sentry.SentryLevel
8+
import io.sentry.SentryLogLevel
89
import io.sentry.SentryOptions
910
import io.sentry.android.timber.BuildConfig.VERSION_NAME
1011
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
@@ -15,6 +16,7 @@ import timber.log.Timber
1516
public class SentryTimberIntegration(
1617
public val minEventLevel: SentryLevel = SentryLevel.ERROR,
1718
public val minBreadcrumbLevel: SentryLevel = SentryLevel.INFO,
19+
public val minLogsLevel: SentryLogLevel = SentryLogLevel.INFO,
1820
) : Integration, Closeable {
1921
private lateinit var tree: SentryTimberTree
2022
private lateinit var logger: ILogger
@@ -29,7 +31,7 @@ public class SentryTimberIntegration(
2931
override fun register(scopes: IScopes, options: SentryOptions) {
3032
logger = options.logger
3133

32-
tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel)
34+
tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel)
3335
Timber.plant(tree)
3436

3537
logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.")

sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberTree.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.sentry.Breadcrumb
55
import io.sentry.IScopes
66
import io.sentry.SentryEvent
77
import io.sentry.SentryLevel
8+
import io.sentry.SentryLogLevel
89
import io.sentry.protocol.Message
910
import timber.log.Timber
1011

@@ -14,6 +15,7 @@ public class SentryTimberTree(
1415
private val scopes: IScopes,
1516
private val minEventLevel: SentryLevel,
1617
private val minBreadcrumbLevel: SentryLevel,
18+
private val minLogLevel: SentryLogLevel = SentryLogLevel.INFO,
1719
) : Timber.Tree() {
1820
private val pendingTag = ThreadLocal<String?>()
1921

@@ -168,6 +170,7 @@ public class SentryTimberTree(
168170
}
169171

170172
val level = getSentryLevel(priority)
173+
val logLevel = getSentryLogLevel(priority)
171174
val sentryMessage =
172175
Message().apply {
173176
this.message = message
@@ -179,12 +182,17 @@ public class SentryTimberTree(
179182

180183
captureEvent(level, tag, sentryMessage, throwable)
181184
addBreadcrumb(level, sentryMessage, throwable)
185+
addLog(logLevel, message, throwable, *args)
182186
}
183187

184188
/** do not log if it's lower than min. required level. */
185189
private fun isLoggable(level: SentryLevel, minLevel: SentryLevel): Boolean =
186190
level.ordinal >= minLevel.ordinal
187191

192+
/** do not log if it's lower than min. required level. */
193+
private fun isLoggable(level: SentryLogLevel, minLevel: SentryLogLevel): Boolean =
194+
level.ordinal >= minLevel.ordinal
195+
188196
/** Captures an event with the given attributes */
189197
private fun captureEvent(
190198
sentryLevel: SentryLevel,
@@ -227,6 +235,25 @@ public class SentryTimberTree(
227235
}
228236
}
229237

238+
/** Send a Sentry Logs */
239+
private fun addLog(
240+
sentryLogLevel: SentryLogLevel,
241+
msg: String?,
242+
throwable: Throwable?,
243+
vararg args: Any?,
244+
) {
245+
// checks the log level
246+
if (isLoggable(sentryLogLevel, minLogLevel)) {
247+
val throwableMsg = throwable?.message
248+
when {
249+
msg != null && throwableMsg != null ->
250+
scopes.logger().log(sentryLogLevel, "$msg\n$throwableMsg", *args)
251+
msg != null -> scopes.logger().log(sentryLogLevel, msg, *args)
252+
throwableMsg != null -> scopes.logger().log(sentryLogLevel, throwableMsg, *args)
253+
}
254+
}
255+
}
256+
230257
/** Converts from Timber priority to SentryLevel. Fallback to SentryLevel.DEBUG. */
231258
private fun getSentryLevel(priority: Int): SentryLevel =
232259
when (priority) {
@@ -238,4 +265,17 @@ public class SentryTimberTree(
238265
Log.VERBOSE -> SentryLevel.DEBUG
239266
else -> SentryLevel.DEBUG
240267
}
268+
269+
/** Converts from Timber priority to SentryLogLevel. Fallback to SentryLogLevel.DEBUG. */
270+
private fun getSentryLogLevel(priority: Int): SentryLogLevel {
271+
return when (priority) {
272+
Log.ASSERT -> SentryLogLevel.FATAL
273+
Log.ERROR -> SentryLogLevel.ERROR
274+
Log.WARN -> SentryLogLevel.WARN
275+
Log.INFO -> SentryLogLevel.INFO
276+
Log.DEBUG -> SentryLogLevel.DEBUG
277+
Log.VERBOSE -> SentryLogLevel.TRACE
278+
else -> SentryLogLevel.DEBUG
279+
}
280+
}
241281
}

sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.sentry.android.timber
22

33
import io.sentry.IScopes
44
import io.sentry.SentryLevel
5+
import io.sentry.SentryLogLevel
56
import io.sentry.SentryOptions
67
import io.sentry.protocol.SdkVersion
78
import kotlin.test.BeforeTest
@@ -21,10 +22,12 @@ class SentryTimberIntegrationTest {
2122
fun getSut(
2223
minEventLevel: SentryLevel = SentryLevel.ERROR,
2324
minBreadcrumbLevel: SentryLevel = SentryLevel.INFO,
25+
minLogsLevel: SentryLogLevel = SentryLogLevel.INFO,
2426
): SentryTimberIntegration =
2527
SentryTimberIntegration(
2628
minEventLevel = minEventLevel,
2729
minBreadcrumbLevel = minBreadcrumbLevel,
30+
minLogsLevel = minLogsLevel,
2831
)
2932
}
3033

@@ -78,11 +81,16 @@ class SentryTimberIntegrationTest {
7881
@Test
7982
fun `Integrations pass the right min levels`() {
8083
val sut =
81-
fixture.getSut(minEventLevel = SentryLevel.INFO, minBreadcrumbLevel = SentryLevel.DEBUG)
84+
fixture.getSut(
85+
minEventLevel = SentryLevel.INFO,
86+
minBreadcrumbLevel = SentryLevel.DEBUG,
87+
minLogsLevel = SentryLogLevel.TRACE,
88+
)
8289
sut.register(fixture.scopes, fixture.options)
8390

8491
assertEquals(sut.minEventLevel, SentryLevel.INFO)
8592
assertEquals(sut.minBreadcrumbLevel, SentryLevel.DEBUG)
93+
assertEquals(sut.minLogsLevel, SentryLogLevel.TRACE)
8694
}
8795

8896
@Test

sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
package io.sentry.android.timber
22

33
import io.sentry.Breadcrumb
4-
import io.sentry.IScopes
4+
import io.sentry.Scopes
55
import io.sentry.SentryLevel
6+
import io.sentry.SentryLogLevel
7+
import io.sentry.logger.ILoggerApi
68
import kotlin.test.BeforeTest
79
import kotlin.test.Test
810
import kotlin.test.assertEquals
911
import kotlin.test.assertNotNull
1012
import kotlin.test.assertNull
1113
import org.mockito.kotlin.any
1214
import org.mockito.kotlin.check
15+
import org.mockito.kotlin.eq
1316
import org.mockito.kotlin.mock
1417
import org.mockito.kotlin.never
1518
import org.mockito.kotlin.verify
19+
import org.mockito.kotlin.verifyNoInteractions
20+
import org.mockito.kotlin.whenever
1621
import timber.log.Timber
1722

1823
class SentryTimberTreeTest {
1924
private class Fixture {
20-
val scopes = mock<IScopes>()
25+
val scopes = mock<Scopes>()
26+
val logs = mock<ILoggerApi>()
27+
28+
init {
29+
whenever(scopes.logger()).thenReturn(logs)
30+
}
2131

2232
fun getSut(
2333
minEventLevel: SentryLevel = SentryLevel.ERROR,
2434
minBreadcrumbLevel: SentryLevel = SentryLevel.INFO,
25-
): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel)
35+
minLogsLevel: SentryLogLevel = SentryLogLevel.INFO,
36+
): SentryTimberTree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel, minLogsLevel)
2637
}
2738

2839
private val fixture = Fixture()
@@ -231,4 +242,72 @@ class SentryTimberTreeTest {
231242
val sut = fixture.getSut()
232243
sut.d("test %s, %s", 1, 1)
233244
}
245+
246+
@Test
247+
fun `Tree adds a log with message and arguments, when provided`() {
248+
val sut = fixture.getSut()
249+
sut.e("test count: %d %d", 32, 5)
250+
251+
verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("test count: %d %d"), eq(32), eq(5))
252+
}
253+
254+
@Test
255+
fun `Tree adds a log if min level is equal`() {
256+
val sut = fixture.getSut()
257+
sut.i(Throwable("test"))
258+
verify(fixture.logs).log(any(), any())
259+
}
260+
261+
@Test
262+
fun `Tree adds a log if min level is higher`() {
263+
val sut = fixture.getSut()
264+
sut.e(Throwable("test"))
265+
verify(fixture.logs).log(any(), any<String>(), any())
266+
}
267+
268+
@Test
269+
fun `Tree won't add a log if min level is lower`() {
270+
val sut = fixture.getSut(minLogsLevel = SentryLogLevel.ERROR)
271+
sut.i(Throwable("test"))
272+
verifyNoInteractions(fixture.logs)
273+
}
274+
275+
@Test
276+
fun `Tree adds an info log`() {
277+
val sut = fixture.getSut()
278+
sut.i("message")
279+
280+
verify(fixture.logs).log(eq(SentryLogLevel.INFO), eq("message"))
281+
}
282+
283+
@Test
284+
fun `Tree adds an error log`() {
285+
val sut = fixture.getSut()
286+
sut.e(Throwable("test"))
287+
288+
verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("test"))
289+
}
290+
291+
@Test
292+
fun `Tree does not add a log, if no message or throwable is provided`() {
293+
val sut = fixture.getSut()
294+
sut.e(null as String?)
295+
verifyNoInteractions(fixture.logs)
296+
}
297+
298+
@Test
299+
fun `Tree logs throwable`() {
300+
val sut = fixture.getSut()
301+
sut.e(Throwable("throwable message"))
302+
303+
verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("throwable message"))
304+
}
305+
306+
@Test
307+
fun `Tree logs throwable and message`() {
308+
val sut = fixture.getSut()
309+
sut.e(Throwable("throwable message"), "My message")
310+
311+
verify(fixture.logs).log(eq(SentryLogLevel.ERROR), eq("My message\nthrowable message"))
312+
}
234313
}

0 commit comments

Comments
 (0)