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;
- How To Do Logging In Java
- How to configure SLF4J with different logger implementations
- Idiomatic way of logging in Kotlin
- Best practices for loggers
- I am creating the Klogging logging library in Kotlin
- Logs Management in a Kotlin Multiplatform project
- Logging In Compose Multiplatform
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;
- https://github.com/oshai/kotlin-logging
- https://github.com/klogging/klogging
- https://github.com/AAkira/Napier
- 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 andconsole
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 AndroidLog
. - Each logger should be able to log at a different level (I should be able to ignore
VERBOSE
andDEBUG
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 |
---|---|
![]() |
![]() |
Android
Kermit | Napier |
---|---|
![]() |
![]() |
Wasm
Kermit | Napier |
---|---|
![]() |
![]() |
Desktop
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;
- Logs Management in a Kotlin Multiplatform project
- Fragmented - logcat - a new look at logging with Piwai from Square
- How To Do Logging In Java
- Best practices for loggers
- Choosing libraries
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… 🖖