Starting a kmp project - The one with logging - Episode 4

Generated by Gemini

TL;DR for logging

If you’d like to just see the implementation and move forward quickly, you can just check my PR. Otherwise, bear with me a couple of minutes for some decision points.

The story

Hi everyone, like in other posts, I’m going to share my journey with KMP, this time the one with logging. And like in the previous episodes, I will start with the Android side, as I’m coming from an Android background.

As many Android developers know, we are familiar with Timber library to log messages in Android for many reasons. Those are;

  • No configuration necessary (we simply plant a tree and use Timber compositely).
  • Can add additional loggers such as Crashlytics or replace/extend the builtin platform logger with something else.
  • Each logger can log at a different level as we have the control over logging.
  • Testable as we can also plant a test logger like mentioned here.

There has been a new library called logcat from square. It’s a bit different from Timber, but it’s also a great library. It has all the nice features of Timber like composition, logging, testability, definability of log level for each logger, but also more performant since it has a different approach to resolving tags than Timber (Timber captures the calling class name as a tag by creating a stacktrace, which can be expensive. logcat gets the calling context without creating a stacktrace). There is a nice discussion about it at Fragmented Podcast.

But when it comes to KMP, we again face a lot of new challenges as these sources can tell;

And because of these challenges, we see every day a new logging library popping up because many are thinking that none of the logging libraries out there is exactly meeting their criteria. So in this post I’m not going to talk about a new library I created. Somewhat I could resist the temptation, and also I don’t think I can create a perfect divine logging library that checks every box by myself, nor do I think there could be one since everyone’s needs are different.

At the time of writing this post, neither Timber nor logcat is KMP Native. So, I’ve looked for more and found these libraries;

  1. https://github.com/oshai/kotlin-logging
  2. https://github.com/klogging/klogging
  3. https://github.com/AAkira/Napier
  4. https://github.com/touchlab/Kermit

But before I dive into these libraries, I’d like to mention what I expect from a logging library in KMP.

  • Performant.
  • Provides the platform native loggers on each platform by default: Log on Android, os_log on iOS, SLF4J on JVM and console on WasmJs.
  • No configuration or easy configuration.
  • Composite logging (should be able to add multiple loggers like Crashlytics logger on release build on top of Android Log.
  • Each logger should be able to log at a different level (I should be able to ignore VERBOSE and DEBUG levels on release builds for instance).
  • Testable.

kotlin-logging:

In Kotlin Logging library, in order to use Android Log, we need to set system property.

object Static {
    init {
        // Configure kotlin-logging to use Android's native logging
        System.setProperty("kotlin-logging-to-android-native", "true")
    }
}
private val static = Static

And even so, our DEBUG logs aren’t logged to logcat because library is checking Log.isLoggable right here, but that always returns false for DEBUG and TRACE levels since on Android, default log level was set to INFO as mentioned here. We could change it by setting System properties at runtime before logging anything when initializing our loggers, but I’d prefer not to use system environments on android apps and rather control it just inside the app so it’s sandboxed.

It has also different api for Android than other platforms to set the log levels.

klogging:

This is an interesting library. It’s a pure-Kotlin logging library that provides asynchronous logging. It also provides direct logging, but it unfortunately doesn’t support Logback which is a very popular logging framework among Backend developers (I think). It is also not composite and a bit different from how I’m used to logging things.

Napier:

This looks robust and offers Crashlytics logging per platform. It looks like it hasn’t been updated for a while, and has few contributors. Not sure if it’s fixed, but I’ve also seen someone reporting that logging non-fatal errors wasn’t working well. But otherwise it checks all the boxes I’ve mentioned before.

Kermit:

Kermit by default, includes a LogWriter instance with min Verbose severity level that supports each platform, such as on Android it writes to Logcat, on iOS to os_log, and for JS it writes to console. It supports composite logging and setting log level per logger. It offers Crashlytics logging per platform too. There is a nice article about implementing Crashlytics logging here. There is also a nice discussion about how this is different from Napier. It looks like it has many maintainers and is actively maintained. As far as I could see, the only downside of it is that by default it doesn’t support tags whereas Square’s logcat library had the perfect solution about it.

I’m not going to fully compare Kermit and Napier (you can see the comparison here and also internet), but I thought attaching some screenshots here would be useful.

iOS
Kermit Napier
Kermit Napier
Android
Kermit Napier
Kermit Napier
Wasm
Kermit Napier
Kermit Napier
Desktop
Kermit Napier
Kermit Napier
Server

On the server side, I use Ktor, which is using logback underneath via slf4j.

Fatal exception logging

On the android side, uncaught exceptions are thrown up on the stack, eventually wind up in a catch-all handler. Logcat shows the stacktrace, but on desktop things are a bit different. The app will still continue to run, and we will not see the stacktrace of the exception in the console.

A good thing is that Sentry will upload those exceptions to the server, so we can see them in the dashboard.

Final thoughts

I think each library has its own advantages and disadvantages. Choosing the right library requires a lot of checks as listed here.

Among those libraries, I’ve chosen Kermit library since it was the closest to what I was looking for. But in my opinion, it is still not a good idea to use a third party library directly all over the source code, in terms of maintainability and changeability. That’s why I’ve created a simple facade logging that can register (plant) many different loggers with different log levels (I kinda copied the nice design of Timber and logcat). This way, I can still use the Kermit library as a platform logger and interact only with my own interface at the rest of the codebase. You can check the implementation here. The only problem I’ve at the moment with it is that I’m not able to set the tag perfectly for each platform;

internal fun Any.resolveTagName(): String? =
    this::class.simpleName?.substringBefore("$$")?.substringAfter('$')

(It works on Android, but not on Wasm). I’ll watch the logcat library if it comes with a nice solution for that.

References and articles;

And an update on the Checklist 🎉 ;

  • Core
    • ✅ Dependency management: Renovate
    • ✅ Lint
    • ✅ Static code analysis: Detekt
    • ✅ Build info
    • ✅ Logging
    • Error reporting
    • Analytics
    • Tracing
    • Network
    • Benchmarking
    • Build conventions
    • Flavors
    • Mocks
    • Test fixtures
    • Preferences
    • Storage
    • DI
    • Feature flags (local & remote)
    • Deep linking
    • Push notifications
    • TimeProvider
    • Local Formatters
    • Coroutine Dispatchers & Test helper
    • Unit testing
    • Test coverage
    • Obfuscation & Shrinking
    • Pipelines
    • Releasing
    • Force updates
  • UI
    • Design system
    • Gallery App
    • Navigation
    • Baseline profiles
    • Compose compiler metrics
    • Previews
    • Network image loading
    • supportsDynamicTheming
    • Status bar color changing
    • App settings with Resource Environment
      • l10n
      • i18n
    • Testing
      • UI Testing
      • Compose Screenshot testing

Hope to see you in the next episode.

Until then may the force be with you… 🖖