console

Gesture-triggered debug overlay to inspect logs, grouped events, HTTP traffic, and live session state; supports custom log types/renderers, real-time search/filters, custom triggers, and zero-overhead release stubs.

Android JVMJVMKotlin/Native
GitHub stars3
AuthorsThernal
Dependents0
Creation date3 months ago

Last activity24 days ago
Latest release0.2.8 (about 1 month ago)

Console

A gesture-triggered in-app devtool for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state β€” without touching production code.

Maven Central License: MIT Kotlin Compose Multiplatform Platform


Features at a glance

πŸ“‹ 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

Quick start

1. Apply the settings plugin

Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.

// settings.gradle.kts
plugins {
    id("io.github.thernal.console") version "<version>"
}

2. Add dependencies

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-bundle-ui:<version>")
    releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}

console-bundle-ui brings in every console module and addon at once. Prefer hand-picking? Depend on the individual artifacts instead (see Modules):

dependencies {
    debugImplementation("io.github.thernal:console-ui:<version>")
    debugImplementation("io.github.thernal:console-logging-ui:<version>")
    releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}

3. Wrap your root composable

@Composable
fun App() {
    ConsoleProvider {
        YourAppContent()
    }
}

4. Open the console

Swipe ↑ ↓ ← β†’ anywhere on screen (default gesture).


Logging

Basic logs

// 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)
}

Log levels

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

Log grouping

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)
}

Custom log types

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(),
) : Log

Send it like any other log:

Console.notify {
    AnalyticsLog(
        message = "screen_view",
        eventName = "screen_view",
        params = mapOf("screen_name" to "Checkout"),
    )
}

Custom log renderer

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()

Search & filter

The log list filters in real time as you type. The query matches against message, tag, and level name.


Triggers

Default trigger

The built-in gesture is a swipe sequence: ↑ ↓ ← β†’

ConsoleProvider { YourAppContent() }  // default trigger, no configuration needed

Custom swipe sequence

ConsoleProvider(
    trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
    YourAppContent()
}

Fully custom trigger

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() })
    }
}

Network inspector

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.

OkHttp

// 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()

Ktor

// 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)
}

Sensitive headers

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
    }
}

Details panel

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")

Stepper

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.


Crash reports

Streams every log to disk as it happens and captures uncaught exceptions, so after a crash you can relaunch the app and inspect the session locally β€” the fatal stack trace plus the logs that led up to it β€” without waiting for a remote crash-reporting round-trip.

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-crash-report-ui:<version>")
}

No code required. Past sessions appear in the Crashes tab, classified as Confirmed (trace captured), Probable (died in the foreground β€” native crash/OOM/ANR), or Safe (killed in the background; hidden by default, toggleable from the in-console settings screen). Opening a session shows its logs β€” network logs render with their full detail, the fatal stack trace arrives as the final Fatal-level entry β€” and each log opens its own detail screen. Sessions are deleted with a swipe, and persistence is tunable from the settings screen or in code:

CrashReports.updateConfig {
    copy(bodyPolicy = CrashBodyPolicy.None, maxSessions = 5)
}

See addons/crash-report-ui for the save-filter, redactor, and custom log codec details.


Custom tabs

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 tab
  • overlay() β€” inject a floating composable on top of the console UI

Modules

Bundles

One-dependency aggregators. Both exclude the design system (add console-components yourself if you want the Ds* primitives) and the HTTP interceptors (add the console-network-ktor / console-network-okhttp module matching your client).

Artifact Description
io.github.thernal:console-bundle-core:<version> Everything a plain JVM/domain module can depend on β€” console-core/io/runtime + every addon -core module (no Compose)
io.github.thernal:console-bundle-ui:<version> The full console β€” console-bundle-core + the UI shell and every addon

Core

Artifact Description
io.github.thernal:console-core:<version> Foundation β€” Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline)
io.github.thernal:console-io:<version> Shared file-system infrastructure β€” ConsoleFileSystem, AppendSink (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

Addons

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
io.github.thernal:console-crash-report-core:<version> Crash session serialization + LogCodec registry
io.github.thernal:console-crash-report-ui:<version> Crashes tab β€” streaming persistence, crash capture, session list/detail
io.github.thernal:console-settings-api:<version> Settings registration contract β€” SettingsSection, entry DSL, SettingsRegistry
io.github.thernal:console-settings-api-noop:<version> No-op stub for production builds
io.github.thernal:console-settings-ui:<version> Settings tab β€” persisted per-addon overrides, entry renderers, General section

License

MIT β€” see LICENSE.

Android JVMJVMKotlin/Native
GitHub stars3
AuthorsThernal
Dependents0
Creation date3 months ago

Last activity24 days ago
Latest release0.2.8 (about 1 month ago)

Console

A gesture-triggered in-app devtool for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state β€” without touching production code.

Maven Central License: MIT Kotlin Compose Multiplatform Platform


Features at a glance

πŸ“‹ 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

Quick start

1. Apply the settings plugin

Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.

// settings.gradle.kts
plugins {
    id("io.github.thernal.console") version "<version>"
}

2. Add dependencies

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-bundle-ui:<version>")
    releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}

console-bundle-ui brings in every console module and addon at once. Prefer hand-picking? Depend on the individual artifacts instead (see Modules):

dependencies {
    debugImplementation("io.github.thernal:console-ui:<version>")
    debugImplementation("io.github.thernal:console-logging-ui:<version>")
    releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}

3. Wrap your root composable

@Composable
fun App() {
    ConsoleProvider {
        YourAppContent()
    }
}

4. Open the console

Swipe ↑ ↓ ← β†’ anywhere on screen (default gesture).


Logging

Basic logs

// 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)
}

Log levels

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

Log grouping

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)
}

Custom log types

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(),
) : Log

Send it like any other log:

Console.notify {
    AnalyticsLog(
        message = "screen_view",
        eventName = "screen_view",
        params = mapOf("screen_name" to "Checkout"),
    )
}

Custom log renderer

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()

Search & filter

The log list filters in real time as you type. The query matches against message, tag, and level name.


Triggers

Default trigger

The built-in gesture is a swipe sequence: ↑ ↓ ← β†’

ConsoleProvider { YourAppContent() }  // default trigger, no configuration needed

Custom swipe sequence

ConsoleProvider(
    trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
    YourAppContent()
}

Fully custom trigger

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() })
    }
}

Network inspector

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.

OkHttp

// 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()

Ktor

// 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)
}

Sensitive headers

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
    }
}

Details panel

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")

Stepper

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.


Crash reports

Streams every log to disk as it happens and captures uncaught exceptions, so after a crash you can relaunch the app and inspect the session locally β€” the fatal stack trace plus the logs that led up to it β€” without waiting for a remote crash-reporting round-trip.

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-crash-report-ui:<version>")
}

No code required. Past sessions appear in the Crashes tab, classified as Confirmed (trace captured), Probable (died in the foreground β€” native crash/OOM/ANR), or Safe (killed in the background; hidden by default, toggleable from the in-console settings screen). Opening a session shows its logs β€” network logs render with their full detail, the fatal stack trace arrives as the final Fatal-level entry β€” and each log opens its own detail screen. Sessions are deleted with a swipe, and persistence is tunable from the settings screen or in code:

CrashReports.updateConfig {
    copy(bodyPolicy = CrashBodyPolicy.None, maxSessions = 5)
}

See addons/crash-report-ui for the save-filter, redactor, and custom log codec details.


Custom tabs

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 tab
  • overlay() β€” inject a floating composable on top of the console UI

Modules

Bundles

One-dependency aggregators. Both exclude the design system (add console-components yourself if you want the Ds* primitives) and the HTTP interceptors (add the console-network-ktor / console-network-okhttp module matching your client).

Artifact Description
io.github.thernal:console-bundle-core:<version> Everything a plain JVM/domain module can depend on β€” console-core/io/runtime + every addon -core module (no Compose)
io.github.thernal:console-bundle-ui:<version> The full console β€” console-bundle-core + the UI shell and every addon

Core

Artifact Description
io.github.thernal:console-core:<version> Foundation β€” Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline)
io.github.thernal:console-io:<version> Shared file-system infrastructure β€” ConsoleFileSystem, AppendSink (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

Addons

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
io.github.thernal:console-crash-report-core:<version> Crash session serialization + LogCodec registry
io.github.thernal:console-crash-report-ui:<version> Crashes tab β€” streaming persistence, crash capture, session list/detail
io.github.thernal:console-settings-api:<version> Settings registration contract β€” SettingsSection, entry DSL, SettingsRegistry
io.github.thernal:console-settings-api-noop:<version> No-op stub for production builds
io.github.thernal:console-settings-ui:<version> Settings tab β€” persisted per-addon overrides, entry renderers, General section

License

MIT β€” see LICENSE.