A small, dependency-light, coroutine-native Kotlin logging and tracing library for the JVM.
kico provides a structured Logger abstraction, pluggable log printers and
transformers, a lightweight tracing API, and bridges that let it act as an
SLF4J backend and a JBoss Logging provider. Built on
top of coroutines from the ground up (coroutine-based span tags, etc.).
Gradle (Kotlin DSL):
dependencies {
implementation("net.skiley:kico:0.2.0")
}Maven:
<dependency>
<groupId>net.skiley</groupId>
<artifactId>kico</artifactId>
<version>0.2.0</version>
</dependency>Requires JVM 11 or newer.
Build a StandardLogger from a dispatcher, a printer, a minimum level, and any
transformers you want:
import java.time.Instant
import net.skiley.kico.logging.LogLevel
import net.skiley.kico.logging.StandardLogger
import net.skiley.kico.logging.dispatching.StandardOutputLogDispatcher
import net.skiley.kico.logging.printing.SimpleLogPrinter
import net.skiley.kico.logging.transforming.TimestampIso8601LogTransformer
import net.skiley.kico.tracing.SpanTag
val logger = StandardLogger(
dispatcher = StandardOutputLogDispatcher(),
minimumLoggingLevel = LogLevel.INFO,
printer = SimpleLogPrinter(),
transformers = listOf(
TimestampIso8601LogTransformer(getNow = Instant::now)
)
)
logger.info("hello, world")
logger.warn("something looks off", SpanTag("user.id") to 42)
logger.error("boom", IllegalStateException("bad state"))Wrap a shared root logger so every call from a given class is automatically tagged with that class's name. This pattern composes the building blocks above into the typical "give me a logger for this class" idiom:
import net.skiley.kico.logging.Logger
import net.skiley.kico.logging.transforming.SourceLocationLogTransformer
class LoggerFactory(
private val rootLogger: Logger
) {
fun create(className: String): Logger =
rootLogger.withTransformer(SourceLocationLogTransformer(className))
inline fun <reified T : Any> create(): Logger =
create(T::class.simpleName ?: "UnknownClass")
}
val factory = LoggerFactory(rootLogger)
val log = factory.create<MyService>()
log.info("ready")Logger — what application code calls. Every call accepts a message, an
optional Throwable, and any number of (SpanTag, Any?) pairs. Built-in
implementations: StandardLogger, NoOpLogger, DelegatedLogger (deferred
binding), and RecordingLogger (for testing).
LogPrinter — turns a populated LogData into a string. Built-ins:
SimpleLogPrinter (human-readable, with an optional emoji column) and
JsonLogPrinter (delegates JSON encoding to a JsonLogDataSerializer you
provide, so you can use whichever JSON library you want).
LogTransformer — enriches LogData before it reaches the printer.
Built-ins:
SourceLocationLogTransformer— adds the source classTimestampIso8601LogTransformer— adds an ISO-8601 timestampTraceDataLogTransformer— copies tags from the currentTracerspan
LogDispatcher — writes the printed string somewhere.
StandardOutputLogDispatcher routes to System.out / System.err by level.
SimpleLogPrinter accepts an optional getEmojiFor: (LogData) -> String? to
override the leading emoji. Return null to suppress it. Reuse
SimpleLogPrinter.DefaultEmojiFor as a fallback:
import net.skiley.kico.logging.printing.SimpleLogPrinter
import net.skiley.kico.tracing.CommonSpanTags
val printer = SimpleLogPrinter(
getEmojiFor = { data ->
when (data[CommonSpanTags.sourceClassTag]) {
"PaymentService" -> "💸"
"AuthService" -> "🔐"
else -> SimpleLogPrinter.DefaultEmojiFor(data)
}
}
)The Tracer API is a thin, coroutine-aware abstraction over spans:
import net.skiley.kico.tracing.SpanTag
import net.skiley.kico.tracing.Tracer
import net.skiley.kico.tracing.TracingMode
suspend fun handleRequest(tracer: Tracer) {
val span = tracer.makeSpan(name = "handle-request", tracingMode = TracingMode.RootSpan)
tracer.withSpan(span) {
tracer.spanTag(SpanTag("user.id"), 42)
// ... your work ...
}
}Combine with TraceDataLogTransformer and every log call inside the span
inherits its tags — so request-scoped context flows through your logs
automatically.
kico ships an SLF4JServiceProvider and a JBoss LoggerProvider, wired
through META-INF/services. Adding kico to the classpath alongside
slf4j-api is enough — third-party libraries that log through SLF4J will route
into kico.
To direct those calls somewhere meaningful, set the global delegate once at startup:
import net.skiley.kico.logging.util.KicoSlf4jLogger
KicoSlf4jLogger.delegateTo(rootLogger)Calls made before the delegate is set are queued and replayed when it is — so libraries that log during their own classloading do not lose anything.
RecordingLogger is a Logger that captures every call into an inspectable
entries: List<RecordedEntry> — handy for asserting that your code logs what
it should:
import io.kotest.matchers.shouldBe
import net.skiley.kico.logging.LogLevel
import net.skiley.kico.logging.util.RecordingLogger
val logger = RecordingLogger()
codeUnderTest(logger)
val entry = logger.entries.single()
entry.message shouldBe "expected"
entry.level shouldBe LogLevel.WARNMIT © Skiley.net