Skip to content

Commit 41c4ac8

Browse files
committed
Implement SentryLoggerApi for Kotlin Multiplatform with platform-specific loggers for JVM and Apple. Add logging capabilities with structured message formatting and attributes. Update SentryOptions to include log enabling feature. Enhance SentryBridge to provide logger access. Include no-op logger for unsupported platforms.
1 parent 22cc85d commit 41c4ac8

File tree

16 files changed

+366
-0
lines changed

16 files changed

+366
-0
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package io.sentry.kotlin.multiplatform
2+
3+
import cocoapods.Sentry.SentrySDK
4+
5+
/**
6+
* Apple platform implementation of [SentryLoggerApi] that wraps the Cocoa SDK's SentryLogger.
7+
*
8+
* This implementation mirrors the Java SDK behavior by:
9+
* - Formatting messages using Java-style format specifiers (%s, %d, etc.)
10+
* - Adding sentry.message.template and sentry.message.parameter.X attributes
11+
*/
12+
internal class CocoaSentryLogger(private val cocoaLogger: cocoapods.Sentry.SentryLogger) : SentryLoggerApi {
13+
14+
override fun trace(message: String?, vararg args: Any?) {
15+
if (message == null) return
16+
val attributes = buildAttributes(message, args)
17+
val formattedMessage = formatMessage(message, args)
18+
cocoaLogger.trace(formattedMessage, attributes)
19+
}
20+
21+
override fun debug(message: String?, vararg args: Any?) {
22+
if (message == null) return
23+
val attributes = buildAttributes(message, args)
24+
val formattedMessage = formatMessage(message, args)
25+
cocoaLogger.debug(formattedMessage, attributes)
26+
}
27+
28+
override fun info(message: String?, vararg args: Any?) {
29+
if (message == null) return
30+
val attributes = buildAttributes(message, args)
31+
val formattedMessage = formatMessage(message, args)
32+
cocoaLogger.info(formattedMessage, attributes)
33+
}
34+
35+
override fun warn(message: String?, vararg args: Any?) {
36+
if (message == null) return
37+
val attributes = buildAttributes(message, args)
38+
val formattedMessage = formatMessage(message, args)
39+
cocoaLogger.warn(formattedMessage, attributes)
40+
}
41+
42+
override fun error(message: String?, vararg args: Any?) {
43+
if (message == null) return
44+
val attributes = buildAttributes(message, args)
45+
val formattedMessage = formatMessage(message, args)
46+
cocoaLogger.error(formattedMessage, attributes)
47+
}
48+
49+
override fun fatal(message: String?, vararg args: Any?) {
50+
if (message == null) return
51+
val attributes = buildAttributes(message, args)
52+
val formattedMessage = formatMessage(message, args)
53+
cocoaLogger.fatal(formattedMessage, attributes)
54+
}
55+
56+
override fun log(level: SentryLogLevel, message: String?, vararg args: Any?) {
57+
when (level) {
58+
SentryLogLevel.TRACE -> trace(message, *args)
59+
SentryLogLevel.DEBUG -> debug(message, *args)
60+
SentryLogLevel.INFO -> info(message, *args)
61+
SentryLogLevel.WARN -> warn(message, *args)
62+
SentryLogLevel.ERROR -> error(message, *args)
63+
SentryLogLevel.FATAL -> fatal(message, *args)
64+
}
65+
}
66+
67+
/**
68+
* Formats the message using Java-style format specifiers (%s, %d, etc.)
69+
* This mirrors the behavior of Java's String.format
70+
*/
71+
private fun formatMessage(template: String, args: Array<out Any?>): String {
72+
if (args.isEmpty()) return template
73+
74+
var result = template
75+
var argIndex = 0
76+
77+
// Simple regex to match format specifiers like %s, %d, %f, etc.
78+
val formatPattern = Regex("%[sdfiboxXeEgGaAcCbBhH%]")
79+
80+
result = formatPattern.replace(result) { matchResult ->
81+
when (matchResult.value) {
82+
"%%" -> "%" // Escaped percent sign
83+
else -> {
84+
if (argIndex < args.size) {
85+
val arg = args[argIndex++]
86+
arg?.toString() ?: "null"
87+
} else {
88+
matchResult.value // Keep the placeholder if no arg available
89+
}
90+
}
91+
}
92+
}
93+
94+
return result
95+
}
96+
97+
/**
98+
* Builds the attributes map mirroring Java SDK behavior:
99+
* - sentry.message.template: The original template string
100+
* - sentry.message.parameter.0, .1, etc.: The parameter values
101+
*/
102+
private fun buildAttributes(template: String, args: Array<out Any?>): Map<Any?, Any> {
103+
if (args.isEmpty()) return emptyMap()
104+
105+
val attributes = mutableMapOf<Any?, Any>()
106+
107+
// Add the message template (like Java SDK does)
108+
attributes["sentry.message.template"] = template
109+
110+
// Add each parameter with its index (like Java SDK does)
111+
args.forEachIndexed { index, arg ->
112+
val value = arg ?: "null"
113+
attributes["sentry.message.parameter.$index"] = value
114+
}
115+
116+
return attributes
117+
}
118+
}
119+
120+
internal actual fun loggerFactory(): SentryLoggerApi {
121+
return CocoaSentryLogger(SentrySDK.logger())
122+
}

sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.apple.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.kotlin.multiplatform.extensions
22

33
import cocoapods.Sentry.SentryHttpStatusCodeRange
4+
import cocoapods.Sentry.experimental
45
import io.sentry.kotlin.multiplatform.CocoaSentryOptions
56
import io.sentry.kotlin.multiplatform.SentryEvent
67
import io.sentry.kotlin.multiplatform.SentryOptions
@@ -34,6 +35,7 @@ internal fun CocoaSentryOptions.applyCocoaBaseOptions(kmpOptions: SentryOptions)
3435
cocoaOptions.enableWatchdogTerminationTracking = kmpOptions.enableWatchdogTerminationTracking
3536
cocoaOptions.appHangTimeoutInterval = kmpOptions.appHangTimeoutIntervalMillis.toDouble()
3637
cocoaOptions.diagnosticLevel = kmpOptions.diagnosticLevel.toCocoaSentryLevel()
38+
cocoaOptions.experimental().setEnableLogs(kmpOptions.enableLogs)
3739
kmpOptions.sampleRate?.let {
3840
cocoaOptions.sampleRate = NSNumber(double = it)
3941
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.sentry.kotlin.multiplatform
2+
3+
import io.sentry.logger.ILoggerApi
4+
import io.sentry.kotlin.multiplatform.extensions.toJvmSentryLogLevel
5+
6+
/**
7+
* JVM implementation of [SentryLoggerApi] that wraps the Java SDK's [ILoggerApi].
8+
*
9+
* This is a thin wrapper that delegates all calls to the underlying Java logger,
10+
* converting KMP types to Java types as needed.
11+
*/
12+
internal class JvmSentryLogger(private val jvmLogger: ILoggerApi) : SentryLoggerApi {
13+
14+
override fun trace(message: String?, vararg args: Any?) {
15+
jvmLogger.trace(message, *args)
16+
}
17+
18+
override fun debug(message: String?, vararg args: Any?) {
19+
jvmLogger.debug(message, *args)
20+
}
21+
22+
override fun info(message: String?, vararg args: Any?) {
23+
jvmLogger.info(message, *args)
24+
}
25+
26+
override fun warn(message: String?, vararg args: Any?) {
27+
jvmLogger.warn(message, *args)
28+
}
29+
30+
override fun error(message: String?, vararg args: Any?) {
31+
jvmLogger.error(message, *args)
32+
}
33+
34+
override fun fatal(message: String?, vararg args: Any?) {
35+
jvmLogger.fatal(message, *args)
36+
}
37+
38+
override fun log(level: SentryLogLevel, message: String?, vararg args: Any?) {
39+
jvmLogger.log(level.toJvmSentryLogLevel(), message, *args)
40+
}
41+
}

sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/SentryBridge.commonJvm.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ internal actual class SentryBridge actual constructor(private val sentryInstance
6767
Sentry.setUser(user?.toJvmUser())
6868
}
6969

70+
71+
actual fun logger(): SentryLoggerApi {
72+
return JvmSentryLogger(Sentry.logger())
73+
}
74+
7075
actual fun isCrashedLastRun(): Boolean {
7176
return Sentry.isCrashedLastRun() ?: false
7277
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.sentry.kotlin.multiplatform
2+
3+
internal actual fun loggerFactory(): SentryLoggerApi {
4+
return JvmSentryLogger(io.sentry.Sentry.logger())
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.sentry.kotlin.multiplatform.extensions
2+
3+
import io.sentry.kotlin.multiplatform.SentryLogLevel
4+
5+
/**
6+
* Converts KMP [SentryLogLevel] to Java SDK's SentryLogLevel.
7+
*/
8+
internal fun SentryLogLevel.toJvmSentryLogLevel(): io.sentry.SentryLogLevel {
9+
return when (this) {
10+
SentryLogLevel.TRACE -> io.sentry.SentryLogLevel.TRACE
11+
SentryLogLevel.DEBUG -> io.sentry.SentryLogLevel.DEBUG
12+
SentryLogLevel.INFO -> io.sentry.SentryLogLevel.INFO
13+
SentryLogLevel.WARN -> io.sentry.SentryLogLevel.WARN
14+
SentryLogLevel.ERROR -> io.sentry.SentryLogLevel.ERROR
15+
SentryLogLevel.FATAL -> io.sentry.SentryLogLevel.FATAL
16+
}
17+
}
18+
19+
/**
20+
* Converts Java SDK's SentryLogLevel to KMP [SentryLogLevel].
21+
*/
22+
internal fun io.sentry.SentryLogLevel.toKmpSentryLogLevel(): SentryLogLevel {
23+
return when (this) {
24+
io.sentry.SentryLogLevel.TRACE -> SentryLogLevel.TRACE
25+
io.sentry.SentryLogLevel.DEBUG -> SentryLogLevel.DEBUG
26+
io.sentry.SentryLogLevel.INFO -> SentryLogLevel.INFO
27+
io.sentry.SentryLogLevel.WARN -> SentryLogLevel.WARN
28+
io.sentry.SentryLogLevel.ERROR -> SentryLogLevel.ERROR
29+
io.sentry.SentryLogLevel.FATAL -> SentryLogLevel.FATAL
30+
}
31+
}

sentry-kotlin-multiplatform/src/commonJvmMain/kotlin/io/sentry/kotlin/multiplatform/extensions/SentryOptionsExtensions.jvm.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal fun JvmSentryOptions.applyJvmBaseOptions(kmpOptions: SentryOptions) {
3333
jvmOptions.sampleRate = kmpOptions.sampleRate
3434
jvmOptions.tracesSampleRate = kmpOptions.tracesSampleRate
3535
jvmOptions.setDiagnosticLevel(kmpOptions.diagnosticLevel.toJvmSentryLevel())
36+
jvmOptions.logs.isEnabled = kmpOptions.enableLogs
3637
jvmOptions.setBeforeBreadcrumb { jvmBreadcrumb, _ ->
3738
if (kmpOptions.beforeBreadcrumb == null) {
3839
jvmBreadcrumb

sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryBridge.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ internal expect class SentryBridge(sentryInstance: SentryInstance = SentryPlatfo
2828

2929
fun setUser(user: User?)
3030

31+
fun logger(): SentryLoggerApi
32+
3133
fun isCrashedLastRun(): Boolean
3234

3335
fun isEnabled(): Boolean

sentry-kotlin-multiplatform/src/commonMain/kotlin/io/sentry/kotlin/multiplatform/SentryKMP.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ public object Sentry {
140140
bridge.setUser(user)
141141
}
142142

143+
/**
144+
* Returns the Sentry logger API for sending structured logs.
145+
*
146+
* On JVM, this returns the Java SDK's ILoggerApi directly (via typealias),
147+
* meaning calls go directly to the Java implementation with zero wrapper overhead.
148+
*
149+
* Usage:
150+
* ```
151+
* Sentry.logger().info("A simple log message")
152+
* Sentry.logger().error("A %s log message", "formatted")
153+
* Sentry.logger().log(SentryLogLevel.DEBUG, "Log at specific level")
154+
* ```
155+
*
156+
* @return The logger API for sending structured logs
157+
*/
158+
public fun logger(): SentryLoggerApi {
159+
return loggerFactory()
160+
}
161+
143162
/**
144163
* Returns true if the app crashed during last run.
145164
*/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.sentry.kotlin.multiplatform
2+
3+
/**
4+
* The log level for Sentry structured logs.
5+
*
6+
* These levels are used with the [SentryLoggerApi.log] method to specify
7+
* the severity of log messages.
8+
*/
9+
public enum class SentryLogLevel {
10+
TRACE,
11+
DEBUG,
12+
INFO,
13+
WARN,
14+
ERROR,
15+
FATAL
16+
}

0 commit comments

Comments
 (0)