comet
0.3.0indexedLightweight observability for coroutines offering real‑time lifecycle tracing, P50/P90/P99 metrics, failure rates, flexible sampling strategies, pluggable exporters, and live trace visualization.
Lightweight observability for coroutines offering real‑time lifecycle tracing, P50/P90/P99 metrics, failure rates, flexible sampling strategies, pluggable exporters, and live trace visualization.
Coroutine Telemetry a Kotlin Multiplatform library for observing structured concurrency in Kotlin Coroutines. It enables tracing and visualization of coroutine execution by exposing coroutine hierarchies, lifecycles, suspension points, execution timing, and failure propagation across platforms, enabling deep analysis of coroutine behavior.
https://github.com/user-attachments/assets/900caecc-4b20-4dfb-ba57-ac44d622b9e3
See comet-demo for a full KMP sample app (Android + iOS) demonstrating Comet and comet-visualizer integration with real API calls and some coroutine patterns.
// build.gradle.kts
dependencies {
implementation("io.github.pandubaraja:comet:0.3.0")
}
comet.traced() to Your Launchesimport io.pandu.Comet
import io.pandu.core.telemetry.exporters.CallbackCoroutineTelemetryExporter
import kotlinx.coroutines.*
// 1. Create Comet instance (once, at app startup)
comet = Comet.create {
exporter(CallbackCoroutineTelemetryExporter(
onEvent = { event -> println() },
onMetrics = { metrics -> println() }
))
bufferSize()
}
comet.start()
scope.launch(comet.traced()) {
launch(CoroutineName()) { }
async(CoroutineName()) { }
}
// BEFORE: No tracing
viewModelScope.launch {
val user = userRepo.get(id)
}
// AFTER: Just add comet.traced() - done!
viewModelScope.launch(comet.traced("load-user")) {
val user = userRepo.get(id) // Now traced!
}
class UserPresenter(private val scope: CoroutineScope) {
private val comet = Comet.create {
exporter(CallbackCoroutineTelemetryExporter { event -> println(event) })
bufferSize(8192)
}.also { it.start() }
{
scope.launch(comet.traced()) {
user = async(CoroutineName()) { api.getUser(id) }
settings = async(CoroutineName()) { api.getSettings(id) }
updateUI(user.await(), settings.await())
}
}
}
withContext with CometWhen switching dispatchers with withContext, use .traced() to preserve tracing:
scope.launch(comet.traced("operation")) {
// Use .traced() to keep tracing when switching dispatchers
withContext(Dispatchers.IO.traced()) {
val data = async(CoroutineName("fetch")) { api.getData() }
data.await()
}
}
Why?
withContextreplaces the coroutine interceptor..traced()re-wraps the new dispatcher with Comet's telemetry. Without it, child coroutines insidewithContextwon't be traced.
import io.pandu.sampling.strategy.*
// Always sample (development only)
samplingStrategy(AlwaysSamplingStrategy)
samplingStrategy(NeverSamplingStrategy)
samplingStrategy(ProbabilisticSamplingStrategy())
samplingStrategy(RateLimitedSamplingStrategy(maxPerSecond = ))
samplingStrategy(OperationBasedSamplingStrategy(
rules = listOf(
OperationBasedSamplingStrategy.OperationRule(Regex(), ),
OperationBasedSamplingStrategy.OperationRule(Regex(), ),
),
defaultRate =
))
samplingStrategy(CompositeSamplingStrategy(
strategies = listOf(
OperationBasedSamplingStrategy(...),
RateLimitedSamplingStrategy(maxPerSecond = )
),
mode = CompositeSamplingStrategy.Mode.ANY
))
Enable includeStackTrace(true) to capture where coroutines are created:
val comet = Comet.create {
includeStackTrace(true) // Enables source file and line number capture
}
When enabled, CoroutineStarted events include creationStackTrace with the call site information. This is useful for debugging and visualization tools like comet-visualizer.
withSpan for Suspend FunctionsUse withSpan to trace blocks within a suspend function:
import io.pandu.core.tools.withSpan
suspend fun processOrder(orderId: String) = withSpan("process-order") {
val order = withSpan("fetch-order") {
orderRepo.get(orderId)
}
withSpan("validate") {
validator.validate(order)
}
withSpan("save") {
orderRepo.save(order)
}
}
Use comet-visualizer for real-time trace visualization in your browser
The visualizer provides:
| Platform | Status | Notes |
|---|---|---|
| JVM | Full |
Copyright 2025
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
class UserViewModel : ViewModel() {
private val comet = Comet.create {
exporter(CallbackCoroutineTelemetryExporter(
onEvent = { event -> Log.d("Comet", event.toString()) }
))
bufferSize(8192)
}.also { it.start() }
fun loadUser(id: String) {
// Just add comet.traced() - works with viewModelScope!
viewModelScope.launch(comet.traced("load-user")) {
// Use CoroutineName for meaningful span names
val user = async(CoroutineName("fetch-user")) { userRepo.get(id) }
val prefs = async(CoroutineName("fetch-prefs")) { prefsRepo.get(id) }
_state.value = UserState(user.await(), prefs.await())
}
}
// Access metrics anytime
fun getStats() = comet.metrics
override fun onCleared() {
viewModelScope.launch { comet.shutdown() }
}
}
| Full stack traces, thread info |
| Android | Full | Full stack traces, thread info |
| iOS | Partial | Limited stack traces |
Surfaced from shared tags and platforms — no rankings paid for.