Log Monitor¶
View your app's logs without ADB or platform-specific consoles. Level filters, full-text search, copyable stacktraces, and metadata-aware entries. Built-in bridge for Kermit by Touchlab; any logging library plugs in via the LogCollector interface.
Platforms¶
Features¶
- Color-coded levels — V / D / I / W / E / A badges adapt to the active theme.
- Level filter chips — toggle each level on / off independently.
- Full-text search — filter by tag or message text.
- Error counter — at-a-glance count of
ERROR/ASSERTentries. - Copyable detail view — message, stacktrace, timestamp, optional metadata.
- Multi-library bridges — Kermit out of the box; Timber, SLF4J, java.util.logging, anything that exposes a writer via the simple
LogCollectorinterface. - Configurable retention — auto-prune by
Duration. Total entries capped at 1 000.
Modules¶
| Module | Purpose |
|---|---|
:plugins:log-monitor:api |
Core data model, LogMonitorStore, LogCollector interface. |
:plugins:log-monitor:ui |
Compose UI + LogMonitorPlugin (the SidekickPlugin impl). |
:plugins:log-monitor:kermit |
Kermit LogWriter bridge. |
:plugins:log-monitor:noop |
Release stub for all three above — same FQNs, empty bodies. No SQLDelight database, LogMonitorLogWriter.log() discards entries, LogMonitorStore.record() is a no-op. Swap in via releaseImplementation on Android or a build property on other targets. See Release builds. |
Setup¶
1. Add dependencies¶
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(platform("dev.parez.sidekick:bom:2026.05.17"))
// `compileOnly` keeps the real jars off Android release's runtime
// classpath, where they would collide with the noop variant.
compileOnly("dev.parez.sidekick:log-monitor-ui")
compileOnly("dev.parez.sidekick:log-monitor-kermit") // optional — Kermit bridge
}
}
}
dependencies {
implementation(platform("dev.parez.sidekick:bom:2026.05.17"))
debugImplementation("dev.parez.sidekick:shell")
releaseImplementation("dev.parez.sidekick:noop")
// Release Android: swap the recording trio (api + ui + kermit) for the
// single `log-monitor-noop` module, which exposes the same FQNs but strips
// SQLDelight and makes record / log calls empty.
debugImplementation("dev.parez.sidekick:log-monitor-ui")
debugImplementation("dev.parez.sidekick:log-monitor-kermit")
releaseImplementation("dev.parez.sidekick:log-monitor-noop")
}
debugImplementation / releaseImplementation are Android-only. For Desktop / iOS / JS / Wasm, see Release builds › Non-Android targets for the property-gated swap recipe.
Omit log-monitor-kermit if you're not using Kermit; see Advanced › Custom logging library.
For multi-module apps where the calling code is in a KMP library, see Installation › Multi-module KMP app for the compileOnly / per-target split.
2. Wire into Sidekick¶
@Composable
fun App() {
val logPlugin = remember { LogMonitorPlugin() }
var sidekickVisible by remember { mutableStateOf(false) }
Box(Modifier.fillMaxSize()) {
MyAppContent()
SmallFloatingActionButton(
onClick = { sidekickVisible = true },
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
) {
Icon(Icons.Default.BugReport, contentDescription = "Open Sidekick")
}
AnimatedVisibility(visible = sidekickVisible, /* enter, exit */) {
Sidekick(
plugins = listOf(logPlugin),
actions = {
IconButton(onClick = { sidekickVisible = false }) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
},
)
}
}
}
3. Install the Kermit bridge¶
The bridge forwards every Kermit log call into the Log Monitor. Wire it once at app startup — typically in your App composable or Application.onCreate():
val logPlugin = remember {
LogMonitorPlugin().also {
Logger.setLogWriters(
platformLogWriter(),
LogMonitorLogWriter(), // defaults to the LogMonitorStore singleton
)
}
}
All Logger.d(...), Logger.i(...), Logger.e(...) calls now appear in the Sidekick log panel automatically.
Alternative: configure a custom Logger instance¶
If you already build a Kermit Logger in DI with mutableLoggerConfigInit(...) — typical when you have multiple writers (Crashlytics, an in-app DB, etc.) — add LogMonitorLogWriter() to the same writer list:
val config = mutableLoggerConfigInit(
platformLogWriter(),
DatabaseLogWriter(...),
CrashlyticsLogWriter(),
LogMonitorLogWriter(),
)
val logger = Logger(config = config, tag = "MyApp")
Conditional bridge in release builds¶
If you use the releaseImplementation("dev.parez.sidekick:log-monitor-noop") swap shown above, no further gating is needed — LogMonitorLogWriter() resolves to the noop's empty subclass in release builds, every call to log(...) discards the entry, and LogMonitorStore.record(...) is also a no-op. Verified by inspecting the merged release DEX: LogMonitorDatabase symbols are absent entirely.
If you can't use the noop swap (e.g. you ship the real log-monitor-kermit in release for parity testing, or you're on a target without an automatic swap), gate the writer on build type instead:
val writers = buildList {
add(platformLogWriter())
add(CrashlyticsLogWriter())
if (!appProperties.isRelease) add(LogMonitorLogWriter())
}
Logger(config = mutableLoggerConfigInit(*writers.toTypedArray()), tag = "MyApp")
Configuration¶
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.days
LogMonitorPlugin(retentionPeriod = 24.hours)
| Example | Behaviour |
|---|---|
1.hours (default) |
Keep the last hour of entries. |
24.hours |
Keep the last day. |
7.days |
Keep the last week. |
Duration.INFINITE |
Never prune by age. |
The store also caps total entries at 1 000 (oldest pruned first), regardless of retention.
UI¶
The panel adapts to the available width:
| Width | Layout |
|---|---|
| < 600 dp | Single pane — tap an entry to see its detail. |
| 600 – 840 dp | Two panes at 40 / 60 split. |
| ≥ 840 dp | Two panes — list fixed at 360 dp. |
List view features:
- Color-coded level badges (V = gray, D = green, I = blue, W = amber, E = red, A = red).
- Level filter chips — toggle each log level on / off.
- Search — filter by tag or message text.
- Error count indicator.
Detail view shows:
- Full message (copyable).
- Stacktrace (copyable, if present).
- Timestamp.
- Metadata table (if present).
Advanced¶
Custom logging library¶
Implement the LogCollector interface — or just pass the singleton LogMonitorStore directly to your writer, since the store implements LogCollector:
fun interface LogCollector {
fun log(level: LogLevel, tag: String, message: String, throwable: Throwable?)
}
val logPlugin = remember { LogMonitorPlugin() }
MyLoggingSDK.addWriter(LogMonitorStore) // LogMonitorStore implements LogCollector
For maximum control, call LogMonitorStore.record(...) directly:
LogMonitorStore.record(
level = LogLevel.INFO,
tag = "MyTag",
message = "Something happened",
throwable = null,
metadata = mapOf("requestId" to "abc-123"), // optional
)
Timber example (Android)¶
class SidekickTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val level = when (priority) {
Log.VERBOSE -> LogLevel.VERBOSE
Log.DEBUG -> LogLevel.DEBUG
Log.INFO -> LogLevel.INFO
Log.WARN -> LogLevel.WARN
Log.ERROR -> LogLevel.ERROR
Log.ASSERT -> LogLevel.ASSERT
else -> LogLevel.DEBUG
}
LogMonitorStore.record(level = level, tag = tag ?: "App", message = message, throwable = t)
}
}
Plant the tree at startup:
val logPlugin = remember { LogMonitorPlugin() }
LaunchedEffect(Unit) {
Timber.plant(SidekickTree())
}
Note
Timber is Android-only. For multiplatform projects, prefer the Kermit bridge.