· 7 min read Posted by Kevin Galligan
Kermit and Crashlytics
Kermit is a Kotlin Multiplatform logging library. The log is configured on each platform to write to various, potentially platform-specific, outputs, but can be called from shared “common” Kotlin code.
Crash reporting tools like Crashlytics allow you to get error reports from remote devices running your software. These tools are very common in mobile app development, and in most cases are critical for monitoring application health in production.
Crash reporting for KMP involves a setup similar to other platform-specific libraries that you want to use in a KMP context. You configure each platform according to the documentation from the software vendor, then have some common Kotlin code that can interact with the platform-specific library.
Crash reporting is a bit of a special case for iOS. It’s a long story, but the summary is the JVM and Kotlin throw unchecked exceptions up the stack, and if unhandled, eventually wind up in a catch-all handler. On iOS, exceptions don’t work the same way. When a crash happens the runtime essentially stops and a handler is called. The crash library gathers the state of each thread’s stack and sends that to the server.
If you call a Kotlin function on iOS and an exception is thrown from somewhere in that Kotlin call stack, the Kotlin runtime will bubble the exception up the stack like it would on the JVM. At the “border”, where you first called the Kotlin code, the Kotlin runtime will trigger a crash. If you look at the crash reports you’ll see konan::abort()
. That’s the Kotlin runtime (internally called “konan”) force-killing the process.
That “works”, and if you don’t need symbolicated Kotlin crash info, then you’re done! However, all of the crash reports will lose potentially vital info. Specifically the Kotlin stack and the exception message.
To get that info, we register an uncaught exception handler with the Kotlin runtime, and when a crash originates in Kotlin code, a custom handled error is sent with the Kotlin stack info and exception message.
The only major negative here is that each crash will result in 2 separate reports for the same event. One fatal crash that ends with konan::abort()
and a non-fatal report with the Kotlin stack and message. There is no way to send a custom fatal crash report, and no way to modify the fatal crash, so we wind up with 2. Hopefully, as KMP becomes more popular, vendors will add direct support for Kotlin. Alternatively, the Crashlytics client is open source, so maybe somebody from the community wants to give it a shot?
Setup
First let’s get the dependencies squared away. You can see sample code here:
Add the Kermit dependencies to the common source set.
implementation("co.touchlab:kermit:1.0.3")
implementation("co.touchlab:kermit-crashlytics:1.0.3")
Just FYI, if you’ll be exporting Kermit to iOS, you’ll need to use
api
instead ofimplementation
.
You should add CrashlyticsLogWriter
to the Logger
config. You can do that with a static logger instance, or add it to the global logger:
Logger.addLogWriter(CrashlyticsLogWriter())
The log writer adds log statements to the Crashlytics client, so when you send a crash report, you’ll see those statements in the report. You can configure the minimum severity, the minimum crash severity, and whether tags are included in the log statement. By default, the minimum severity is INFO, the minimum crash severity is WARN, and tags are included.
The minimum severity is the log level that will get logged to Crashlytics. The default again is INFO, so DEBUG statements won’t be sent to Crashlyics, unless you alter the default configuration.
The minimum crash severity is the minimum log severity that will trigger a handled exception being sent to Crashlytics. By default, it is set to WARN, which means that exception log statements of WARN, ERROR, and ASSERT create handled exceptions in Crashlytics.
val ex = SomeException()
Logger.e(ex) { "We have a problem..." }
If you want to log exceptions but not have them create Crashlytics exceptions, make sure to keep them below the minimum.
val ex = NotImportantException()
Logger.i(ex) { "We have a problem..." }
iOS Crash Handler
On iOS, to capture Kotlin crashes, we need a little extra setup. Kermit defines a convenience function to set up the default handler.
setupCrashlyticsExceptionHook(Logger)
Call that function and pass in a logger instance (in this case, I passed in the global Logger
). This function is only defined for iOS!!! You should define a function in your iOS source set and call it on app start (from Swift/Objc).
When a crash happens in Kotlin code and bubbles up to Swift/Objc, Kermit will now send a handled report to Crashlytics before allowing execution to continue and the app to crash.
Configuration Notes
Kermit Crashlytics integration on iOS uses cinterop to talk to the native client. Crashlytics itself needs to be added and linked in your build. The details of how you add and link binary on iOS are a little beyond the scope of this post, but be aware that simply adding the kermit-crashlytics
dependency won’t include the iOS Crashlytics SDK.
You have 2 basic choices of framework type: static and dynamic. We tend to use static in our builds, but you can use either.
Static
Static is somewhat easier. You only need to link to the Crashlytics binary when actually building your iOS app, or when running Kotlin tests (and in that case, only if you actually use CrashlyticsLogWriter
in your tests, which wouldn’t really make sense).
If your tests are not building because the linker can’t find Crashlytics, you can add our stub dependency. It won’t work if you try to use it, but it will satisfy the linker.
dependencies {
implementation("co.touchlab:kermit-crashlytics-test:1.0.3")
}
You’ll still need to add Crashlytics to your iOS project, but you can do that with Cocoapods or SPM.
Dynamic
Dynamic frameworks are a little more complicated. The Crashlytics binary needs to be available at compile time. It is relatively easy to do this with Cocoapods.
cocoapods {
//Other config
framework {
isStatic = false
}
pod("FirebaseCrashlytics")
}
However, that will cause Kotlin to generate a whole different cinterop definition for Crashlytics. That’s not a huge technical problem, but since we don’t use it, it’s a waste. On our near-term todo list is adding a way to use cocoapods as strictly a dependency integrator with the ability to disable things like the cinterop, but for now, you’ll need to live with the imperfections (or wire up Crashlytics yourself).
I should mention, it is also possible to use pod(“FirebaseCrashlytics”)
for static frameworks rather than our test stubs, but that will also generate the extra cinterop code.
Usage
Log messages above the minimum severity will be sent to the Crashlytics log. If a log call includes an exception and is at or above the minimum exception severity, that call will trigger a handled error report with Crashlytics.
For hard crashes on iOS, as mentioned, you will get 2 reports. One “fatal” one “non-fatal”. The non-fatal one has the extra Kotlin stack and info, and you’ll want to be able to navigate between them. To do that, the crash handler generates a random id and adds it to Crashlytics key/value pairs. You can search for matching reports with that key.
Find the crash key
Search for matching reports
Again, not ideal, but functional. When you get a chance, tweet at the various vendors to let them know you really want first-party KMP crash reporting support. For now, you need to search to find associated reports.
Assuming you have your project set up correctly, you should start to see symbolicated crash reports for Kotlin on iOS. Correctly configuring Crashlytics can sometimes be fairly difficult, so make sure to test that your reports are being sent to the Crashlytics back end, and that your debug/dsym info is getting there.
If your stack traces look like this, the server doesn’t have some debug info.
Missing dsyms?
The Kotlin stack trace should look more like this, with Kotlin source line numbers.
Same exception, with debug info.
The Kotlin/Native compiler generates a “standard” Xcode framework, so you shouldn’t need any custom configuration to get debug info uploaded. It is one of those things that can be difficult to configure correctly, Kotlin or no Kotlin, and you don’t want to have a production problem and no way to see what’s happening, so just make sure it’s working as expected!
Work at Touchlab!
If you want to help define the future of Kotlin, and like living a bit on the edge technically speaking, please reach out!