Shiva Thapa
07/15/2026, 5:51 PMLog.d { } for debugging, structured Logger with typed key-value attributes for production
• Coroutine-aware context : withLogContext { } propagates LogContext safely across suspension points and thread hops (optional logger-coroutines module)
• Pluggable pipeline : per-tag level overrides, multiple sinks, custom formatters (Default / JSON / Compact / Pretty)
• Lazy message evaluation, real epoch-ms timestamps, thread names
• TestSink for asserting log output in unit tests
• Extensible LogSink : ship logs to Crashlytics, a log aggregator, a file, anywhere
Install (Maven Central):
commonMain.dependencies {
implementation("io.github.shivathapaa:logger:1.5.0")
implementation("io.github.shivathapaa:logger-coroutines:1.5.0") // optional, coroutine context
}
Docs include real-world patterns - a multiplatform Crashlytics sink (expect/actual) and a secret-sanitizing sink:
github.com/shivathapaa/KMP-Logger
Feedback and issues are very welcome.Stefan Oltmann
07/15/2026, 10:51 PMLog.i() as this is a clean code violation in my perspective.
It should be <http://Logger.info|Logger.info>() as known from SLF4J.Shiva Thapa
07/16/2026, 4:05 AMLoggerFactory.get("Auth").info { "..." }
It supports full-word levels (verbose, debug, info, warn, error, fatal), lazy message lambdas, structured attributes, and context.
Log.i() is just a lightweight, optional facade that mirrors android.util.Log (`v`/`d`/`i`/`w`/`e`) for quick debugging. Both APIs go through the same pipeline (filtering, sinks, context, etc.), so it's simply a different entry point, not a separate logging system.
So if you prefer the SLF4J style, <http://logger.info|logger.info> { ... } has you fully covered. Log.* is there purely as a convenience for those who prefer the Android-style API.altavir
07/16/2026, 6:37 AMShiva Thapa
07/16/2026, 4:37 PMrequestId, userId, that kind of thing - without making the logging API suspend. Ambient context in KMP generally leans on thread state, and that only really works on JVM/Android: ThreadContextElement doesn't exist on Native or JS, so nothing reinstalls the context when a coroutine resumes, and thread pools reuse threads. So the context is attached to the logger object instead:
val log = LoggerFactory.get("Api").withContext("requestId" to id)
log.info { "Starting" } // carries requestId - any platform, any dispatcher
It's a field on the logger, so there's no thread-local to lose and nothing to reinstall on resumption. Correct on Native and JS, from ordinary non-suspending code.
Around that: attributes: Map<String, Any?> and a LogContext live on the event itself, so sinks consume them as data instead of parsing them back out of a message string; sinks and formatters are pluggable; and the core artifact has exactly one dependency, kotlin-stdlib.
That's the whole pitch. It's young and I'm the only maintainer - worth knowing when you weigh it.
Mixed logging libraries? This is the sharper question, and the honest answer is that no KMP logger solves it today, mine included. There's no SLF4J for KMP - no cross-platform service loader, so no facade and no convergence point on Native/JS/Wasm. Every KMP logger is a leaf, not a hub. Adopting mine doesn't unify your logging: your own code goes through it, and your dependencies' logs keep going wherever they already went. I ship no bridges today.
What it does give you is a seam. LogSink receives the whole LogEvent - level, logger, message, throwable, attributes, context, thread, timestamp - so you can route its output into whatever pipeline you already run (e.g. a JVM sink mapping attributes and context onto SLF4J's MDC), and map a foreign SDK's events in through Log.*. That's glue you write per library, per target. I'd rather say that plainly than pretend the fragmentation goes away.
Both are the right questions to ask - thanks for pushing on it.