
Lightweight logging API offering lazy-evaluated message blocks, automatic call-site tag inference, configurable format strategies, disk logging with rotation/cleanup, and multiple concurrent loggers.
📝 A lightweight Kotlin Multiplatform (KMP) logging API with lazy evaluation, configurable formatting, disk logging, and tag inference.
logcat { } is lazy: message blocks run only when a logger is installed and loggable.AndroidLogcatFormatStrategy / IosLogcatFormatStrategy: platform-style console output with configurable fields.PrettyFormatStrategy: bordered output with thread info and call stack.NonFormatStrategy: no formatting; forwards raw messages to the underlying LogStrategy.Throwable.asLog() for readable stack traces.Add the dependency in commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.airsaid:logcat:$version")
}
}
}Make sure you have Maven Central:
repositories {
mavenCentral()
}Android (e.g., Application.onCreate):
AndroidLogcatLogger.installOnDebuggableApp(
application = this,
minPriority = LogPriority.DEBUG,
) {
AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
}installOnDebuggableApp skips installation when the application is not debuggable. Use
AndroidLogcatLogger.install only when logcat output in non-debuggable builds is intentional.
iOS (app startup):
IosLogcatLogger.install(
minPriority = LogPriority.DEBUG,
) {
IosLogcatFormatStrategy.Builder<IosLogcatLogStrategy>()
.logStrategy(IosLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
}Convenience installers are idempotent and use a private installation key. The first successful
call wins; later calls reuse the installed logger without evaluating their factory. To change its
configuration, uninstall the existing logger first (or call LogcatLogger.uninstallAll() during a
full logging reset), then install it again. To install multiple independently configured loggers of
the same type, construct them explicitly and use LogcatLogger.install(...).
IosLogcatLogStrategy() marks dynamic unified log content as private by default. To make logs
publicly readable, opt in only when messages are known not to contain sensitive data:
IosLogcatLogStrategy(IosLogcatPrivacy.PUBLIC)class Foo {
fun bar() {
logcat { "Default log" }
logcat(LogPriority.INFO) { "Info log" }
logcat(tag = "CustomTag") { "Custom tag log" }
}
}
logcat("StandaloneTag") { "Log in a top-level function" }
try {
error("boom")
} catch (t: Throwable) {
logcat { t.asLog() }
}Note: logcat { } is an Any extension. For top-level functions (no this), use the
logcat(tag) { } overload.
The Android artifact ships with a custom lint check that errors when app code calls
android.util.Log directly. Prefer routing logs through kmp-logcat so messages stay lazy,
consistently formatted, and controlled by the installed loggers.
// Error: LogcatSystemLogUsage
Log.d(tag, msg)
// Quick fix
logcat(tag, LogPriority.DEBUG) { msg }Throwable overloads are also supported when they can be safely converted:
// Error: LogcatSystemLogUsage
Log.e(tag, msg, throwable)
// Quick fix
logcat(tag, LogPriority.ERROR) { msg + "\n" + throwable.asLog() }This lint check only runs for Android lint. It does not affect common or iOS source sets.
The library's own Android implementation uses android.util.Log internally by design.
If direct platform logging is intentional in your app, suppress the error with
@SuppressLint("LogcatSystemLogUsage") or configure the issue in lint.xml.
Formats output similar to platform consoles. You can toggle fields:
showTimeStamp(Boolean): include/exclude the timestamp.timeStampPattern(String, TimeZone): customize timestamp output with a Unicode pattern and timezone.timeStampFormatter((Instant) -> String): fully custom timestamp formatter.showProcessId(Boolean): include/exclude the process id.showThreadInfo(Boolean): include/exclude thread name and id.showTag(Boolean): include/exclude the log tag.showLevel(Boolean): include/exclude the log priority.By default, timestamp uses Instant.toString() (ISO-8601 UTC).
Adds borders, thread info, and call stack lines:
showThreadInfo(Boolean): include/exclude thread info.methodCount(Int): number of call stack lines to print.methodOffset(Int): offset into the call stack.Bypasses formatting and delegates directly to the underlying LogStrategy.
Disk logging is provided by DiskLogStrategy + DiskLogger and supports writing logs to disk:
val diskLogger = DiskLogger.installOnApp(minPriority = LogPriority.WARN) {
NonFormatStrategy(
DiskLogStrategy.Builder()
.logFileDirectory(logDirectory)
.logFileGenerator(DefaultLogFileGenerator())
.logFileMaxSize(1024L * 1024L * 100L) // 100MB
.logFileMaxTime(7L * 24L * 60L * 60L * 1000L) // 7 days
.logFileMaxSizeResolver(AvailableSpaceLogFileMaxSizeResolver())
.logBufferMaxSize(10 * 1024) // 10K chars
.build()
)
}Keep DiskLogStrategy.Builder().build() inside the factory so repeated installation attempts do
not allocate unused disk logging resources.
Builder options:
logFileDirectory(String)
logFileGenerator(LogFileGenerator)
DefaultLogFileGenerator (daily folder + date files).logFileMaxSize(Long)
logFileMaxTime(Long)
logFileMaxSizeResolver(LogFileMaxSizeResolver)
logBufferMaxSize(Int)
Disk log calls are queued on a private serial background worker. Threshold-based flushes do not
perform directory traversal or file writes on the calling thread, and accepted logs are processed
in submission order. flush() and close() synchronously wait for previously accepted logs to be
written, so avoid calling them frequently from the main thread.
Manual flush:
// ...
diskLogger.flush()val androidLogger = AndroidLogcatLogger(
minPriority = LogPriority.DEBUG,
formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.build()
)
val diskLogger = DiskLogger(
minPriority = LogPriority.WARN,
formatStrategy = NonFormatStrategy(
DiskLogStrategy.Builder()
.logFileDirectory(logDirectory)
.build()
)
)
LogcatLogger.install(androidLogger, diskLogger)Apache-2.0. See LICENSE.
📝 A lightweight Kotlin Multiplatform (KMP) logging API with lazy evaluation, configurable formatting, disk logging, and tag inference.
logcat { } is lazy: message blocks run only when a logger is installed and loggable.AndroidLogcatFormatStrategy / IosLogcatFormatStrategy: platform-style console output with configurable fields.PrettyFormatStrategy: bordered output with thread info and call stack.NonFormatStrategy: no formatting; forwards raw messages to the underlying LogStrategy.Throwable.asLog() for readable stack traces.Add the dependency in commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.airsaid:logcat:$version")
}
}
}Make sure you have Maven Central:
repositories {
mavenCentral()
}Android (e.g., Application.onCreate):
AndroidLogcatLogger.installOnDebuggableApp(
application = this,
minPriority = LogPriority.DEBUG,
) {
AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
}installOnDebuggableApp skips installation when the application is not debuggable. Use
AndroidLogcatLogger.install only when logcat output in non-debuggable builds is intentional.
iOS (app startup):
IosLogcatLogger.install(
minPriority = LogPriority.DEBUG,
) {
IosLogcatFormatStrategy.Builder<IosLogcatLogStrategy>()
.logStrategy(IosLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
}Convenience installers are idempotent and use a private installation key. The first successful
call wins; later calls reuse the installed logger without evaluating their factory. To change its
configuration, uninstall the existing logger first (or call LogcatLogger.uninstallAll() during a
full logging reset), then install it again. To install multiple independently configured loggers of
the same type, construct them explicitly and use LogcatLogger.install(...).
IosLogcatLogStrategy() marks dynamic unified log content as private by default. To make logs
publicly readable, opt in only when messages are known not to contain sensitive data:
IosLogcatLogStrategy(IosLogcatPrivacy.PUBLIC)class Foo {
fun bar() {
logcat { "Default log" }
logcat(LogPriority.INFO) { "Info log" }
logcat(tag = "CustomTag") { "Custom tag log" }
}
}
logcat("StandaloneTag") { "Log in a top-level function" }
try {
error("boom")
} catch (t: Throwable) {
logcat { t.asLog() }
}Note: logcat { } is an Any extension. For top-level functions (no this), use the
logcat(tag) { } overload.
The Android artifact ships with a custom lint check that errors when app code calls
android.util.Log directly. Prefer routing logs through kmp-logcat so messages stay lazy,
consistently formatted, and controlled by the installed loggers.
// Error: LogcatSystemLogUsage
Log.d(tag, msg)
// Quick fix
logcat(tag, LogPriority.DEBUG) { msg }Throwable overloads are also supported when they can be safely converted:
// Error: LogcatSystemLogUsage
Log.e(tag, msg, throwable)
// Quick fix
logcat(tag, LogPriority.ERROR) { msg + "\n" + throwable.asLog() }This lint check only runs for Android lint. It does not affect common or iOS source sets.
The library's own Android implementation uses android.util.Log internally by design.
If direct platform logging is intentional in your app, suppress the error with
@SuppressLint("LogcatSystemLogUsage") or configure the issue in lint.xml.
Formats output similar to platform consoles. You can toggle fields:
showTimeStamp(Boolean): include/exclude the timestamp.timeStampPattern(String, TimeZone): customize timestamp output with a Unicode pattern and timezone.timeStampFormatter((Instant) -> String): fully custom timestamp formatter.showProcessId(Boolean): include/exclude the process id.showThreadInfo(Boolean): include/exclude thread name and id.showTag(Boolean): include/exclude the log tag.showLevel(Boolean): include/exclude the log priority.By default, timestamp uses Instant.toString() (ISO-8601 UTC).
Adds borders, thread info, and call stack lines:
showThreadInfo(Boolean): include/exclude thread info.methodCount(Int): number of call stack lines to print.methodOffset(Int): offset into the call stack.Bypasses formatting and delegates directly to the underlying LogStrategy.
Disk logging is provided by DiskLogStrategy + DiskLogger and supports writing logs to disk:
val diskLogger = DiskLogger.installOnApp(minPriority = LogPriority.WARN) {
NonFormatStrategy(
DiskLogStrategy.Builder()
.logFileDirectory(logDirectory)
.logFileGenerator(DefaultLogFileGenerator())
.logFileMaxSize(1024L * 1024L * 100L) // 100MB
.logFileMaxTime(7L * 24L * 60L * 60L * 1000L) // 7 days
.logFileMaxSizeResolver(AvailableSpaceLogFileMaxSizeResolver())
.logBufferMaxSize(10 * 1024) // 10K chars
.build()
)
}Keep DiskLogStrategy.Builder().build() inside the factory so repeated installation attempts do
not allocate unused disk logging resources.
Builder options:
logFileDirectory(String)
logFileGenerator(LogFileGenerator)
DefaultLogFileGenerator (daily folder + date files).logFileMaxSize(Long)
logFileMaxTime(Long)
logFileMaxSizeResolver(LogFileMaxSizeResolver)
logBufferMaxSize(Int)
Disk log calls are queued on a private serial background worker. Threshold-based flushes do not
perform directory traversal or file writes on the calling thread, and accepted logs are processed
in submission order. flush() and close() synchronously wait for previously accepted logs to be
written, so avoid calling them frequently from the main thread.
Manual flush:
// ...
diskLogger.flush()val androidLogger = AndroidLogcatLogger(
minPriority = LogPriority.DEBUG,
formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.build()
)
val diskLogger = DiskLogger(
minPriority = LogPriority.WARN,
formatStrategy = NonFormatStrategy(
DiskLogStrategy.Builder()
.logFileDirectory(logDirectory)
.build()
)
)
LogcatLogger.install(androidLogger, diskLogger)Apache-2.0. See LICENSE.