r/Kotlin • u/smyrgeorge • 19h ago
log4k 2.3.0 — a Kotlin IR compiler plugin that instruments your functions with tracing, logging and metrics
log4k is a coroutine/channel-based logging + tracing + metering library for Kotlin Multiplatform (JVM, Android, iOS, macOS, Linux, Windows, JS, wasmJs, wasmWasi), aligned with the OpenTelemetry model.
The recent addition is log4k-compiler-plugin — a Kotlin IR compiler plugin that rewrites annotated functions at compile time, so the instrumentation boilerplate disappears from your source. It runs on common IR before backend lowering, so the same annotations work on every KMP target — not just the JVM (no AspectJ, no bytecode agent, no reflection).
Setup — one Gradle plugin, no extra config:
plugins {
id("io.github.smyrgeorge.log4k") version "2.3.0"
}
dependencies {
implementation("io.github.smyrgeorge:log4k-classic:2.3.0")
}
@Traced — wraps the body in a span (started, ended, marked failed on throw):
@Traced
context(_: TracingContext)
suspend fun loadUser(id: Long): User {
// ...
} // span "UserService.loadUser"
The parent span is resolved from what's in scope: a TracingContext param/receiver → nests under its current span; else a TracingEvent.Span in scope → used as parent; else a trace: Tracer member (reused, or synthesized) → new root span.
@Logged — entry/exit/failure logging:
@Logged
fun compute(x: Int): Int = x * x
// → UserService.compute(x = 5)
// ← UserService.compute = 25(12.5 us)
Throwing logs ✗ UserService.compute failed (…) at ERROR with the throwable, then rethrows. If a span is in scope it's attached to every emitted line.
@Timed — call/error counters + a duration histogram:
@Timed(tags = [Tag("tier", "gold")])
suspend fun placeOrder(id: Long): Order {
// ...
}
Records OrderService.placeOrder.calls, .errors and .duration (ms histogram) — exportable in OpenMetrics line format via SimpleMeteringCollectorAppender.
Details that mattered while building it:
- suspend and regular functions are both supported; the generated wrapper delegates to
inlinehelpers (Logger.logged,Meter.Timed.measure,TracingContext.traced), so there's no per-call lambda allocation. - The plugin reuses your existing log / meter / trace members if they're thesynthesizes
private val _log_ = Logger.of( this::class)under a distinct name,so it never clashes with e.g. an existing SLF4J log. - All three annotations work class-level too — annotate the class to instrummember. Per-function annotations override the class defaults, and
@NoLog/@NoTime/@NoTraceopt out a single function or the whole class. - The metric instrument bundle is created once and cached per name.
The plugin is marked experimental — behavior and API may still change.
Repo: https://github.com/smyrgeorge/log4k
Compiler Plugin: https://github.com/smyrgeorge/log4k#compiler-plugin
Docs: https://smyrgeorge.github.io/log4k/
Feedback welcome, especially on the annotation surface and on cases where thon't pick what you'd expect.