
Span-aware tracing with structured, low-overhead logfmt lines carrying trace/span IDs; reconstructs end-to-end call trees via CLI, supports context propagation across threads/async hops and pluggable backends.
KmperTrace is a tracing and structured logging toolkit for Android, iOS/Swift, Desktop, and Wasm. It helps you reconstruct end-to-end execution flows from plain logs.
When you're chasing a bug that hops across coroutines, threads, and platforms, plain logs are not enough. KmperTrace lets you Wrap important operations in spans, emit structured log lines with trace/span IDs, and then use the CLI to rebuild the full call flow from a logfile and even by directly streaming from your device.
Example: two overlapping downloads, nested repository/DB calls, and an injected failure - all reconstructed from plain log output.
End-to-end execution flows from plain logs.
KmperTrace encodes trace/span IDs and span start/end markers into log lines, so you can
reconstruct readable traces without needing a collector or observability backend first.
Works across Android, iOS/Swift, Desktop, and Wasm.
KmperTrace fits naturally in Kotlin Multiplatform and Android projects and can also be consumed
from regular Swift iOS projects via the provided XCFramework.
Lightweight and simple.
No dependencies on external observability systems or collectors. You get readable, structured
log lines with trace/span IDs that can be processed offline or shipped to any backend.
Callback-friendly context bridging.
Capture a TraceSnapshot inside a span and re-install it in non-coroutine callbacks
(Handler/Executor/SDK listeners) so logs stay attached to the originating span.
Journey-friendly root spans.
Wrap user/system triggers in LogContext.journey(...) so each trace starts with an explicit
"why", not just a method name (see docs/Journeys.md).
Developer‑friendly tooling.
A CLI (kmpertrace-cli) that can ingest a flat logfile and render readable trace trees
Pluggable sinks.
Platform‑native log sinks by default (Logcat, NSLog/print, stdout/console), with hooks to add
your own LogSink implementations.
kmpertrace-runtime/
Kotlin Multiplatform runtime with:
traceSpan, KmperTracer),Log, components/operations),kmpertrace-cli/
JVM CLI that:
trace,Pure iOS consumer?
See docs/IOS-XCFramework.md for using the prebuilt XCFramework (manual drag/drop or SwiftPM
binary target from the release assets).
KMP app with Swift host code?
See docs/IOS-KMP-Swift.md for making KmperTraceSwift available to Swift via your KMP framework.
Install kmpertrace-cli
The CLI requires Java 17+ (java available on PATH or via JAVA_HOME).
Recommended (release installer):
macOS/Linux:
curl -fsSL https://github.com/pluralfusion/kmpertrace/releases/latest/download/install.sh | shWindows (PowerShell):
iwr https://github.com/pluralfusion/kmpertrace/releases/latest/download/install.ps1 -UseBasicParsing | iexVerify install:
kmpertrace-cli --helpOptional (build from latest source):
git clone https://github.com/pluralfusion/kmpertrace.git
cd kmpertrace
./gradlew :kmpertrace-cli:installDist
./kmpertrace-cli/build/install/kmpertrace-cli/bin/kmpertrace-cli --helpAdd the runtime dependency
In your KMP project, add kmpertrace-runtime to the source sets where you want tracing/logging:
commonMain {
dependencies {
implementation("dev.goquick:kmpertrace-runtime:<version>")
}
}Configure KmperTrace at startup
Somewhere in your app initialization (per process):
fun App() {
LaunchedEffect(Unit) {
KmperTrace.configure(
minLevel = Level.DEBUG,
serviceName = "sample-app",
)
}
}Wrap work in spans and log
suspend fun refreshAll() = traceSpan(component = "ProfileViewModel", operation = "refreshAll") {
Log.i { "Refreshing profile..." }
repository.loadProfile()
repository.loadContacts()
repository.loadActivity()
Log.i { "Refresh complete" }
}All logs inside traceSpan { ... } will carry trace/span IDs so the CLI can reconstruct
the tree.
Run your app and collect logs for Android (non-interactive mode)
Run the app as usual; KmperTrace will emit structured log lines to the platform backend (Logcat, NSLog, stdout, etc.).
To collect logs for already running app, launch:
adb logcat --pid=$(adb shell pidof -s dev.goquick.kmpertrace.sampleapp) > /tmp/kmpertrace.log
(change package to your app's package name)
After that keep using the app to collect logs into a file. Press Ctrl+C to stop log collection.
Or (this is what I usually do) just copy/paste to file from Android Studio's Logcat view.
Visualize with the CLI (non-interactive mode)
Run the CLI to visualize logs from an existing file:
kmpertrace-cli print --file /path/to/your.log --color=onOr visualize logs in real-time from adb logcat (drop adb logcat -c if you don't want to clear
the log buffer first):
adb logcat -c && adb logcat -v epoch --pid="$(adb shell pidof dev.goquick.kmpertrace.sampleapp)" \
| kmpertrace-cli print --follow --color=on(replace dev.goquick.kmpertrace.sampleapp with your app's package name)
You'll see per‑trace trees similar to the screenshot from the beginning of this README, with spans, durations, log lines, and error stack traces.
We have experimental interactive mode in kmpertrace-cli. E.g. to run it for adb logs you can run:
kmpertrace-cli tui --source adb --adb-pkg dev.goquick.kmpertrace.sampleappor for iOS:
kmpertrace-cli tui --source ios --ios-proc SampleAppThis tool was tested on MacOS and Linux. Non-interactive print mode (or piping logs into tui --source stdin/file) should work on Windows. The interactive single-key raw mode doesn’t (Windows lacks the POSIX stty path), so Windows will fall back to the line-buffered input: type the letter and press Enter. ANSI styling works best in Windows Terminal/PowerShell with VT enabled (modern Windows does this by default); classic cmd.exe may look worse but still functions.
See docs/CLI-UserGuide.md for current flags and interactive keys.
Spans can have key/value attributes that show up next to span names in the CLI (useful for small,
high-signal identifiers like jobId, http.status, cache.hit).
Normal vs debug attributes
attributes = mapOf("jobId" to "123").?: attributes = mapOf("?userEmail" to "a@b.com").KmperTrace.configure(emitDebugAttributes = true).CLI rendering
--span-attrs on
a (status bar shows [a] attrs=off|on)? prefix (e.g. ?userEmail=a@b.com).Wire format and key rules
a:<key> (normal) and d:<key> (debug).[A-Za-z0-9_.-] (after optional leading ?); invalid keys are emitted
as invalid_<...> with invalid characters replaced by _.For more details, see docs/Tracing.md.
Below is a code snippet that triggers a download when a button is pressed, fetches JSON via Ktor, parses it, and logs each important step with KmperTrace.
@Serializable
data class Profile(val name: String, val downloads: Int)
@Composable
fun DownloadButton(client: HttpClient, url: String) {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { downloadAndParse(client, url) }
}) {
Text("Download profile")
}
}
private suspend fun downloadAndParse(client: HttpClient, url: String) =
traceSpan(component = "Downloader", operation = "DownloadProfile") {
val log = Log.forComponent("Downloader")
log.i { "Button tapped: start download $url" }
val bytes: ByteArray = traceSpan("FetchHttp") {
log.i { "HTTP GET $url" }
val b: ByteArray = client.get(url).body()
log.d { "Fetched ${b.size} bytes" }
b
}
val payload = traceSpan("ParseJson") {
log.d { "Decoding JSON payload" }
runCatching {
Json.decodeFromString<Profile>(bytes.decodeToString())
}.getOrElse { error ->
log.e(error) { "Failed to decode profile" }
throw error
}
}
log.i { "Parsed profile name=${payload.name} downloads=${payload.downloads}" }
payload
}Every log line inside traceSpan carries trace/span IDs; the CLI can reconstruct the full
flow when you collect logs from your app.
Sample output from the CLI will look like this in case of success:
trace 1234abcd...
└─ Downloader.DownloadProfile (85 ms)
├─ ℹ️ 12:00:01.234 Downloader: Button tapped: start download https://api.example.com/profile.json
├─ Downloader.FetchHttp (42 ms)
│ ├─ ℹ️ 12:00:01.235 Downloader: HTTP GET https://api.example.com/profile.json
│ └─ 🔍 12:00:01.277 Downloader: Fetched 512 bytes
├─ Downloader.ParseJson (18 ms)
│ └─ 🔍 12:00:01.295 Downloader: Decoding JSON payload
└─ ℹ️ 12:00:01.303 Downloader: Parsed profile name=Alex downloads=42
or it will look like this in case of failure:
trace 1234abcd...
└─ Downloader.DownloadProfile (65 ms)
├─ ℹ️ 12:00:01.234 Downloader: Button tapped: start download https://api.example.com/profile.json
├─ Downloader.FetchHttp (40 ms)
│ ├─ ℹ️ 12:00:01.235 Downloader: HTTP GET https://api.example.com/profile.json
│ └─ 🔍 12:00:01.275 Downloader: Fetched 512 bytes
└─ ❌ Downloader.ParseJson (23 ms)
├─ 🔍 12:00:01.295 Downloader: Decoding JSON payload
└─ ❌ 12:00:01.318 Downloader: span end: Downloader.ParseJson
java.lang.IllegalStateException: Failed to decode profile
at kotlinx.serialization.json.JsonDecoder....
at dev.goquick.kmpertrace.sampleapp.DownloadButtonKt.decodeProfile(DownloadButton.kt:42)
at dev.goquick.kmpertrace.sampleapp.DownloadButtonKt.downloadAndParse(DownloadButton.kt:32)
at ...
KmperTrace is early‑stage and APIs may change before 1.0. Feedback, issues, and ideas for
integrations (sinks, exporters, IDE plugins) are very welcome.
KmperTrace is a tracing and structured logging toolkit for Android, iOS/Swift, Desktop, and Wasm. It helps you reconstruct end-to-end execution flows from plain logs.
When you're chasing a bug that hops across coroutines, threads, and platforms, plain logs are not enough. KmperTrace lets you Wrap important operations in spans, emit structured log lines with trace/span IDs, and then use the CLI to rebuild the full call flow from a logfile and even by directly streaming from your device.
Example: two overlapping downloads, nested repository/DB calls, and an injected failure - all reconstructed from plain log output.
End-to-end execution flows from plain logs.
KmperTrace encodes trace/span IDs and span start/end markers into log lines, so you can
reconstruct readable traces without needing a collector or observability backend first.
Works across Android, iOS/Swift, Desktop, and Wasm.
KmperTrace fits naturally in Kotlin Multiplatform and Android projects and can also be consumed
from regular Swift iOS projects via the provided XCFramework.
Lightweight and simple.
No dependencies on external observability systems or collectors. You get readable, structured
log lines with trace/span IDs that can be processed offline or shipped to any backend.
Callback-friendly context bridging.
Capture a TraceSnapshot inside a span and re-install it in non-coroutine callbacks
(Handler/Executor/SDK listeners) so logs stay attached to the originating span.
Journey-friendly root spans.
Wrap user/system triggers in LogContext.journey(...) so each trace starts with an explicit
"why", not just a method name (see docs/Journeys.md).
Developer‑friendly tooling.
A CLI (kmpertrace-cli) that can ingest a flat logfile and render readable trace trees
Pluggable sinks.
Platform‑native log sinks by default (Logcat, NSLog/print, stdout/console), with hooks to add
your own LogSink implementations.
kmpertrace-runtime/
Kotlin Multiplatform runtime with:
traceSpan, KmperTracer),Log, components/operations),kmpertrace-cli/
JVM CLI that:
trace,Pure iOS consumer?
See docs/IOS-XCFramework.md for using the prebuilt XCFramework (manual drag/drop or SwiftPM
binary target from the release assets).
KMP app with Swift host code?
See docs/IOS-KMP-Swift.md for making KmperTraceSwift available to Swift via your KMP framework.
Install kmpertrace-cli
The CLI requires Java 17+ (java available on PATH or via JAVA_HOME).
Recommended (release installer):
macOS/Linux:
curl -fsSL https://github.com/pluralfusion/kmpertrace/releases/latest/download/install.sh | shWindows (PowerShell):
iwr https://github.com/pluralfusion/kmpertrace/releases/latest/download/install.ps1 -UseBasicParsing | iexVerify install:
kmpertrace-cli --helpOptional (build from latest source):
git clone https://github.com/pluralfusion/kmpertrace.git
cd kmpertrace
./gradlew :kmpertrace-cli:installDist
./kmpertrace-cli/build/install/kmpertrace-cli/bin/kmpertrace-cli --helpAdd the runtime dependency
In your KMP project, add kmpertrace-runtime to the source sets where you want tracing/logging:
commonMain {
dependencies {
implementation("dev.goquick:kmpertrace-runtime:<version>")
}
}Configure KmperTrace at startup
Somewhere in your app initialization (per process):
fun App() {
LaunchedEffect(Unit) {
KmperTrace.configure(
minLevel = Level.DEBUG,
serviceName = "sample-app",
)
}
}Wrap work in spans and log
suspend fun refreshAll() = traceSpan(component = "ProfileViewModel", operation = "refreshAll") {
Log.i { "Refreshing profile..." }
repository.loadProfile()
repository.loadContacts()
repository.loadActivity()
Log.i { "Refresh complete" }
}All logs inside traceSpan { ... } will carry trace/span IDs so the CLI can reconstruct
the tree.
Run your app and collect logs for Android (non-interactive mode)
Run the app as usual; KmperTrace will emit structured log lines to the platform backend (Logcat, NSLog, stdout, etc.).
To collect logs for already running app, launch:
adb logcat --pid=$(adb shell pidof -s dev.goquick.kmpertrace.sampleapp) > /tmp/kmpertrace.log
(change package to your app's package name)
After that keep using the app to collect logs into a file. Press Ctrl+C to stop log collection.
Or (this is what I usually do) just copy/paste to file from Android Studio's Logcat view.
Visualize with the CLI (non-interactive mode)
Run the CLI to visualize logs from an existing file:
kmpertrace-cli print --file /path/to/your.log --color=onOr visualize logs in real-time from adb logcat (drop adb logcat -c if you don't want to clear
the log buffer first):
adb logcat -c && adb logcat -v epoch --pid="$(adb shell pidof dev.goquick.kmpertrace.sampleapp)" \
| kmpertrace-cli print --follow --color=on(replace dev.goquick.kmpertrace.sampleapp with your app's package name)
You'll see per‑trace trees similar to the screenshot from the beginning of this README, with spans, durations, log lines, and error stack traces.
We have experimental interactive mode in kmpertrace-cli. E.g. to run it for adb logs you can run:
kmpertrace-cli tui --source adb --adb-pkg dev.goquick.kmpertrace.sampleappor for iOS:
kmpertrace-cli tui --source ios --ios-proc SampleAppThis tool was tested on MacOS and Linux. Non-interactive print mode (or piping logs into tui --source stdin/file) should work on Windows. The interactive single-key raw mode doesn’t (Windows lacks the POSIX stty path), so Windows will fall back to the line-buffered input: type the letter and press Enter. ANSI styling works best in Windows Terminal/PowerShell with VT enabled (modern Windows does this by default); classic cmd.exe may look worse but still functions.
See docs/CLI-UserGuide.md for current flags and interactive keys.
Spans can have key/value attributes that show up next to span names in the CLI (useful for small,
high-signal identifiers like jobId, http.status, cache.hit).
Normal vs debug attributes
attributes = mapOf("jobId" to "123").?: attributes = mapOf("?userEmail" to "a@b.com").KmperTrace.configure(emitDebugAttributes = true).CLI rendering
--span-attrs on
a (status bar shows [a] attrs=off|on)? prefix (e.g. ?userEmail=a@b.com).Wire format and key rules
a:<key> (normal) and d:<key> (debug).[A-Za-z0-9_.-] (after optional leading ?); invalid keys are emitted
as invalid_<...> with invalid characters replaced by _.For more details, see docs/Tracing.md.
Below is a code snippet that triggers a download when a button is pressed, fetches JSON via Ktor, parses it, and logs each important step with KmperTrace.
@Serializable
data class Profile(val name: String, val downloads: Int)
@Composable
fun DownloadButton(client: HttpClient, url: String) {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { downloadAndParse(client, url) }
}) {
Text("Download profile")
}
}
private suspend fun downloadAndParse(client: HttpClient, url: String) =
traceSpan(component = "Downloader", operation = "DownloadProfile") {
val log = Log.forComponent("Downloader")
log.i { "Button tapped: start download $url" }
val bytes: ByteArray = traceSpan("FetchHttp") {
log.i { "HTTP GET $url" }
val b: ByteArray = client.get(url).body()
log.d { "Fetched ${b.size} bytes" }
b
}
val payload = traceSpan("ParseJson") {
log.d { "Decoding JSON payload" }
runCatching {
Json.decodeFromString<Profile>(bytes.decodeToString())
}.getOrElse { error ->
log.e(error) { "Failed to decode profile" }
throw error
}
}
log.i { "Parsed profile name=${payload.name} downloads=${payload.downloads}" }
payload
}Every log line inside traceSpan carries trace/span IDs; the CLI can reconstruct the full
flow when you collect logs from your app.
Sample output from the CLI will look like this in case of success:
trace 1234abcd...
└─ Downloader.DownloadProfile (85 ms)
├─ ℹ️ 12:00:01.234 Downloader: Button tapped: start download https://api.example.com/profile.json
├─ Downloader.FetchHttp (42 ms)
│ ├─ ℹ️ 12:00:01.235 Downloader: HTTP GET https://api.example.com/profile.json
│ └─ 🔍 12:00:01.277 Downloader: Fetched 512 bytes
├─ Downloader.ParseJson (18 ms)
│ └─ 🔍 12:00:01.295 Downloader: Decoding JSON payload
└─ ℹ️ 12:00:01.303 Downloader: Parsed profile name=Alex downloads=42
or it will look like this in case of failure:
trace 1234abcd...
└─ Downloader.DownloadProfile (65 ms)
├─ ℹ️ 12:00:01.234 Downloader: Button tapped: start download https://api.example.com/profile.json
├─ Downloader.FetchHttp (40 ms)
│ ├─ ℹ️ 12:00:01.235 Downloader: HTTP GET https://api.example.com/profile.json
│ └─ 🔍 12:00:01.275 Downloader: Fetched 512 bytes
└─ ❌ Downloader.ParseJson (23 ms)
├─ 🔍 12:00:01.295 Downloader: Decoding JSON payload
└─ ❌ 12:00:01.318 Downloader: span end: Downloader.ParseJson
java.lang.IllegalStateException: Failed to decode profile
at kotlinx.serialization.json.JsonDecoder....
at dev.goquick.kmpertrace.sampleapp.DownloadButtonKt.decodeProfile(DownloadButton.kt:42)
at dev.goquick.kmpertrace.sampleapp.DownloadButtonKt.downloadAndParse(DownloadButton.kt:32)
at ...
KmperTrace is early‑stage and APIs may change before 1.0. Feedback, issues, and ideas for
integrations (sinks, exporters, IDE plugins) are very welcome.