Skip to content

Skiley/kico

Repository files navigation

kico

Kotlin Integrated Coroutines Observability


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.).

Install

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.

Quick start

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"))

Per-class loggers

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")

Concepts

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 class
  • TimestampIso8601LogTransformer — adds an ISO-8601 timestamp
  • TraceDataLogTransformer — copies tags from the current Tracer span

LogDispatcher — writes the printed string somewhere. StandardOutputLogDispatcher routes to System.out / System.err by level.

Customizing emojis

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)
        }
    }
)

Tracing

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.

SLF4J + JBoss bridges

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.

Testing

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.WARN

License

MIT © Skiley.net

About

A small, dependency-light, coroutine-native Kotlin logging and tracing library for the JVM.

Topics

Resources

License

Stars

Watchers

Forks

Contributors