A gesture-triggered debug overlay for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state — without touching production code.
| 📋 Log viewer | Filterable log list — levels, tags, full-text search |
| 🎨 Custom log types | Define your own Log subtypes with their own UI renderers |
| 🔍 Search & filter | Real-time full-text search across message, tag, and level |
| 🔗 Log grouping | Link related logs with a shared groupId |
| 🌐 Network inspector | OkHttp + Ktor interceptors with headers, body, duration, status |
| 🗂️ Details panel | Live key/value sidebar for session info, flags, build metadata |
| ⏸️ Stepper | Pause log processing and replay events one-by-one |
| 🖥️ Custom tabs | Add your own full screens to the Console navigation bar |
| 👆 Custom triggers | Swap the default swipe for any gesture — double-tap, shake, etc. |
| 🚫 Zero prod cost | Noop stubs with identical APIs — no UI, no overhead in release builds |
Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.
// settings.gradle.kts
plugins {
id("io.github.thernal.console") version "<version>"
}// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-ui:<version>")
debugImplementation("io.github.thernal:console-logging-ui:<version>")
releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}@Composable
fun App() {
ConsoleProvider {
YourAppContent()
}
}Swipe ↑ ↓ ← → anywhere on screen (default gesture).
// Fire-and-forget — thread-safe, non-blocking
Console.notify {
Log(message = "Payment initiated", tag = "Payments", level = LogLevel.Info)
}
// Suspending — preserves ordering when called sequentially inside a coroutine
Console.asyncNotify {
Log(message = "Token refreshed", tag = "Auth", level = LogLevel.Success)
}| Level | Typical use |
|---|---|
None |
No level indicator |
Verbose |
Trace-level detail |
Debug |
Development information |
Info |
General flow events |
Success |
Completed operations |
Warning |
Recoverable issues |
Error |
Failures that were handled |
Fatal |
Unrecoverable crashes |
Logs with the same groupId are visually linked — useful for correlating events like a network request and its response.
val id = Uuid.random().toString()
Console.notify {
Log(message = "→ POST /auth/login", tag = "Network", level = LogLevel.Debug, groupId = id)
}
// ... later ...
Console.notify {
Log(message = "← 200 OK (143ms)", tag = "Network", level = LogLevel.Success, groupId = id)
}Implement Log to carry structured data through the pipeline.
data class AnalyticsLog(
override val message: String,
override val level: LogLevel = LogLevel.Debug,
val eventName: String,
val params: Map<String, Any> = emptyMap(),
override val id: String = Uuid.random().toString(),
override val tag: String? = "Analytics",
override val groupId: String? = null,
override val timestamp: Instant = Clock.System.now(),
) : LogSend it like any other log:
Console.notify {
AnalyticsLog(
message = "screen_view",
eventName = "screen_view",
params = mapOf("screen_name" to "Checkout"),
)
}Implement LogRenderer so AnalyticsLog entries have their own item and detail screens, then register it for the type:
@file:OptIn(ConsoleInternalApi::class) // LogRendererRegistry.register is a first-party API
object AnalyticsLogRenderer : LogRenderer {
@Composable
override fun Item(log: Log, modifier: Modifier) {
if (log !is AnalyticsLog) return
AnalyticsLogItem(log, modifier)
}
@Composable
override fun Detail(log: Log) {
if (log !is AnalyticsLog) return
AnalyticsLogDetail(log)
}
}
object AnalyticsAddon : ConsoleAddon {
override fun onInstall() {
LogRendererRegistry.register<AnalyticsLog>(AnalyticsLogRenderer)
}
}
// Call once at startup
AnalyticsAddon.install()The log list filters in real time as you type. The query matches against message, tag, and level name.
The built-in gesture is a swipe sequence: ↑ ↓ ← →
ConsoleProvider { YourAppContent() } // default trigger, no configuration neededConsoleProvider(
trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
YourAppContent()
}ConsoleTrigger is a fun interface — any Modifier extension that calls onDetected() qualifies:
// Double-tap anywhere on screen
val doubleTapTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onDetected() })
}
}
ConsoleProvider(trigger = doubleTapTrigger) {
YourAppContent()
}// Long-press trigger
val longPressTrigger = ConsoleTrigger { onDetected ->
pointerInput(Unit) {
detectTapGestures(onLongPress = { onDetected() })
}
}Captures HTTP traffic and renders it in the log list with method, status code, URL, headers, body, and round-trip duration. Tap any entry to see the full request/response detail.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-okhttp:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = OkHttpClient.Builder()
.addInterceptor(ConsoleNetworkOkHttpInterceptor())
.build()// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-network-core:<version>")
debugImplementation("io.github.thernal:console-network-ktor:<version>")
debugImplementation("io.github.thernal:console-network-ui:<version>")
}val client = HttpClient {
install(ConsoleNetworkKtorPlugin)
}Authorization, Cookie, Set-Cookie, X-Api-Key, and Proxy-Authorization are masked with *** by default (SensitiveHeaders.DEFAULT).
// Custom names and mask string
ConsoleNetworkOkHttpInterceptor(
sensitiveHeaders = SensitiveHeaders(
names = setOf("authorization", "x-session-token"),
mask = "[redacted]",
)
)
// Disable masking entirely
ConsoleNetworkOkHttpInterceptor(sensitiveHeaders = SensitiveHeaders.NONE)
// Same API for Ktor
HttpClient {
install(ConsoleNetworkKtorPlugin) {
sensitiveHeaders = SensitiveHeaders.NONE
}
}A live key/value sidebar for session info, feature flags, user context, or any ambient state — visible at a glance without scrolling through logs.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-details-ui:<version>")
releaseImplementation("io.github.thernal:console-details-core-noop:<version>")
}// Upsert — updates in place if the key already exists
ConsoleDetails.put("User" to "alice@example.com")
ConsoleDetails.put("Environment" to "staging")
ConsoleDetails.put("Feature:NewCheckout" to "enabled")
// Remove
ConsoleDetails.remove("Environment")Pauses log processing and lets you replay events one-by-one — useful for stepping through complex async flows that would otherwise scroll past instantly.
// build.gradle.kts
dependencies {
debugImplementation("io.github.thernal:console-stepper-ui:<version>")
}No code required. Once the module is on the classpath, the stepper control appears automatically as a floating overlay inside the console. Tap Pause to freeze the pipeline, Step to advance one event at a time, and Resume to return to live mode.
Add your own full-screen view to the Console navigation bar by implementing ConsoleAddon.
// 1. Define the tab
object MetricsTab : ConsoleTab {
override val title = "Metrics"
override val icon = Icons.Default.BarChart
override val order = 10 // lower = further left in the nav bar
@Composable
override fun Content() {
MetricsScreen()
}
}
// 2. Expose it via an addon
object MetricsAddon : ConsoleAddon {
override fun tab(): ConsoleTab = MetricsTab
}
// 3. Install once at app startup
MetricsAddon.install()Beyond tabs, ConsoleAddon also supports:
navGraph()— register a full navigation sub-graph behind your taboverlay()— inject a floating composable on top of the console UI
| Artifact | Description |
|---|---|
io.github.thernal:console-core:<version> |
Foundation — Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline) |
io.github.thernal:console-runtime:<version> |
Console singleton + log pipeline (depends on console-core) |
io.github.thernal:console-api:<version> |
Addon contracts — ConsoleAddon, ConsoleTab, LogRenderer, LogRendererRegistry (depends on console-core, not runtime) |
io.github.thernal:console-ui:<version> |
Compose UI shell — ConsoleProvider, navigation, overlay |
io.github.thernal:console-ui-noop:<version> |
No-op stub for production builds |
| Artifact | Description |
|---|---|
io.github.thernal:console-logging-ui:<version> |
Log list, log detail screen, BasicLog renderer |
io.github.thernal:console-details-ui:<version> |
Live key/value Details panel |
io.github.thernal:console-details-core-noop:<version> |
No-op stub for production builds |
io.github.thernal:console-network-core:<version> |
Shared network log types |
io.github.thernal:console-network-okhttp:<version> |
OkHttp interceptor |
io.github.thernal:console-network-ktor:<version> |
Ktor plugin |
io.github.thernal:console-network-ui:<version> |
Network log UI renderer |
io.github.thernal:console-stepper-ui:<version> |
Pause-and-step log replay |
MIT — see LICENSE.