DEV Community

Cover image for Debugging Kotlin Multiplatform Apps with Better Observability
Shiva Thapa
Shiva Thapa

Posted on

Debugging Kotlin Multiplatform Apps with Better Observability

We’ve all been there. You have a Kotlin Multiplatform (KMP) app running beautifully on Android, but your iOS and other platform users are reporting a cryptic crash that you just can’t reproduce. You look at your logs, and all you see is a sea of println statements or disjointed Logcat/NSLog entries that tell you what happened, but never why.

In the multiplatform world, fragmented logging isn’t just a nuisance — it’s a blind spot. If your observability strategy changes every time you cross a platform boundary, you’re not just writing code; you’re building a puzzle you can’t solve.

That’s why I built KMP-Logger. It’s not just another logging wrapper; it’s a structured event pipeline designed to give you a single source of truth across Android, iOS, JVM, Web, and Desktop.

The “Chaos” of String-Based Logging

Most loggers treat logs as strings. But strings are for humans to read after things go wrong. Events are for systems to analyze before things get out of hand.

When you treat a log as a structured event, you gain four superpowers:

  1. Searchability: Filter by specific User IDs or Request IDs across thousands of logs.

  2. Efficiency: Messages aren’t even built if the log level is disabled.

  3. Context: Metadata follows your execution flow through nested calls.

  4. Consistency: Your JSON logs in Data log look the same whether they came from an iPhone or a Browser.

1. Choosing Your Entry Point: Simple vs. Structured

KMP-Logger respects where you are in your journey. Sometimes you’re prototyping at 2 AM and just need to see if a variable changed. Other times, you’re architecting a financial app that requires strict audit trails.

The “I just need to see this” API

For those quick moments, use the Log singleton. It’s zero-config and handles platform-native tagging automatically.

// Effortless tagging based on your class name
class AuthViewModel {
  private val log = Log.withClassTag()
  fun onLogin() {
    log.d("User tapped login button")
  }
}
class AuthViewModel {
  fun onLogin() {
    loggerD("User tapped login button") // Directly takes AuthViewModel as tag
  }
}
class AuthViewModel {
  fun onLogin() {
    Log.d("User tapped login button") // Simple logging, no initilization required
    // OR,
    Log.d("User tapped login button", "LoginTAG")
  }
}
Enter fullscreen mode Exit fullscreen mode

The “This is for Production” API
When you’re ready for the big leagues, the StructuredLogger lets you attach deep metadata (Attributes) that travel with your log.

val logger = LoggerFactory.get("Checkout")
logger.info(attrs = { attr("cart_id", "cart_99") }) {
  "Processing payment for user_123"
}
Enter fullscreen mode Exit fullscreen mode

2. The Secret Sauce: Context Propagation

Imagine you’re debugging a multi-step checkout flow. Passing a transactionId through ten different functions just for logging is a nightmare.

KMP-Logger’s LogContext acts like a “scaffold” for your logs. You set the context once, and every log emitted within that scope — no matter how deep — automatically inherits that metadata.

val sessionContext = LogContext(mapOf("sessionId" to "session_789"))
LogContextHolder.withContext(sessionContext) {
  // Every log inside here now knows the sessionId
  apiService.call()
  dbRepository.save()
}
Enter fullscreen mode Exit fullscreen mode

3. A Pipeline That Grows With You

KMP-Logger isn’t a dead end. It’s a pipeline where you control the Style (Formatters) and the Destination (Sinks).

Smart Formatting

Want emojis in your console but clean JSON in your server?

  • PrettyLogFormatter: For developers who like their logs readable and visual.
  • JsonLogFormatter: For machine ingestion and cloud-native observability.

Pluggable Sinks

Logs should go where they are useful. Out of the box, you get native platform sinks, but the real power is in the custom implementations.

The Firebase Bridge
Need breadcrumbs for your crash reports? It’s a few lines of code:

class FirebaseLogSink : LogSink {
  override fun emit(event: LogEvent) {
    if (event.level < LogLevel.WARN) return
    val message = LogFormatters.json(false).format(event)
    FirebaseCrashlytics.getInstance().log(message)
  }
}
Enter fullscreen mode Exit fullscreen mode

The “Privacy First” Sink
In the age of GDPR, you can’t just log everything. You can create a SanitizingSink that acts as a middleware, redacting sensitive keys like password or apiKey before they ever leave the device.

4. Precision Control (No More Log Noise)
Stop drowning in logs that you don’t always require. KMP-Logger allows you to set global defaults while providing surgical overrides for specific modules.

LoggerConfig.Builder()
.minLevel(LogLevel.INFO) // Stay quiet by default
.override("Database", LogLevel.VERBOSE) // Listen closely to DB queries
.override("NoisyLogs", LogLevel.OFF) // Silence the chatter
.build()
Enter fullscreen mode Exit fullscreen mode

Conclusion: Observability is a Mindset
Logging isn’t optional. println doesn’t scale and it doesn’t tell you what actually went wrong.
A structured, multiplatform logging pipeline makes debugging predictable and your app more reliable.
KMP-Logger gives you consistent visibility across platforms — whether you’re starting fresh or untangling a legacy KMP codebase.

Ready to see clearly? Read full documentation here:
KMP-Logger on GitHub
Klibs.io

Happy building!

Top comments (0)