
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):
val formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
AndroidLogcatLogger.install(
minPriority = LogPriority.DEBUG,
formatStrategy = formatStrategy,
)iOS (app startup):
val formatStrategy = IosLogcatFormatStrategy.Builder<IosLogcatLogStrategy>()
.logStrategy(IosLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
IosLogcatLogger.install(
minPriority = LogPriority.DEBUG,
formatStrategy = formatStrategy,
)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 diskStrategy = 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()
val formatStrategy = NonFormatStrategy(diskStrategy)
DiskLogger.installOnApp(LogPriority.WARN, formatStrategy)Builder options:
logFileDirectory(String)
logFileGenerator(LogFileGenerator)
DefaultLogFileGenerator (daily folder + date files).logFileMaxSize(Long)
logFileMaxTime(Long)
logFileMaxSizeResolver(LogFileMaxSizeResolver)
logBufferMaxSize(Int)
Manual flush:
val diskLogger = DiskLogger(LogPriority.WARN, NonFormatStrategy(diskStrategy))
LogcatLogger.install(diskLogger)
// ...
diskLogger.flush()val androidLogger = AndroidLogcatLogger(
minPriority = LogPriority.DEBUG,
formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.build()
)
val diskLogger = DiskLogger(
minPriority = LogPriority.WARN,
formatStrategy = NonFormatStrategy(diskStrategy)
)
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):
val formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
AndroidLogcatLogger.install(
minPriority = LogPriority.DEBUG,
formatStrategy = formatStrategy,
)iOS (app startup):
val formatStrategy = IosLogcatFormatStrategy.Builder<IosLogcatLogStrategy>()
.logStrategy(IosLogcatLogStrategy())
.timeStampPattern(
pattern = "uuuu-MM-dd HH:mm:ss.SSS",
timeZone = TimeZone.currentSystemDefault(),
)
.build()
IosLogcatLogger.install(
minPriority = LogPriority.DEBUG,
formatStrategy = formatStrategy,
)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 diskStrategy = 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()
val formatStrategy = NonFormatStrategy(diskStrategy)
DiskLogger.installOnApp(LogPriority.WARN, formatStrategy)Builder options:
logFileDirectory(String)
logFileGenerator(LogFileGenerator)
DefaultLogFileGenerator (daily folder + date files).logFileMaxSize(Long)
logFileMaxTime(Long)
logFileMaxSizeResolver(LogFileMaxSizeResolver)
logBufferMaxSize(Int)
Manual flush:
val diskLogger = DiskLogger(LogPriority.WARN, NonFormatStrategy(diskStrategy))
LogcatLogger.install(diskLogger)
// ...
diskLogger.flush()val androidLogger = AndroidLogcatLogger(
minPriority = LogPriority.DEBUG,
formatStrategy = AndroidLogcatFormatStrategy.Builder<AndroidLogcatLogStrategy>()
.logStrategy(AndroidLogcatLogStrategy())
.build()
)
val diskLogger = DiskLogger(
minPriority = LogPriority.WARN,
formatStrategy = NonFormatStrategy(diskStrategy)
)
LogcatLogger.install(androidLogger, diskLogger)Apache-2.0. See LICENSE.