SLF4J Fluent Logging API in Kotlin
2 minutes read in Kotlin HighlightsSLF4J 2.0.0 introduced new logging API:
logger.atInfo().log("Hello world")
New fluent api looks more verbose in simple cases like this. Let’s compare it to traditional API:
// traditional
logger.info("Hello world")
// fluent
logger.atInfo().log("Hello world")
You may think that more advanced use-cases will be more readable or shorter. Let’s check:
logger.debug("Temperature set to {}. Old temperature was {}.", newT, oldT)
logger.atDebug().addArgument(newT).addArgument(oldT).log("Temperature set to {}. Old temperature was {}.")
logger.atDebug().log("Temperature set to {}. Old temperature was {}.", newT, oldT)
logger.atDebug().addArgument(newT).log("Temperature set to {}. Old temperature was {}.", oldT)
But actually not. Traditional API more readable and shorter than new one, so what the point of this shiny-new-fluent API?
First, it laziness, you can use Supplier
to provide argument:
fun newT() = // costly computation
logger.atDebug().addArgument { newT() }.log("Temperature set to {}. Old temperature was {}.", oldT)
Second, now it’s much easier to extend API, for example, it’s possible to provide multiple Markers
:
logger.atInfo().addMarker(DB).addMarker(SECURITY).log("SQL Injection attempt: '{}'", sql)
SLF4J and Kotlin
Yes, this new fluent API accommodates a variety of present and future features without combinatorial explosion, and without requiring separate logging facades. But currently I feel like Kotlin inline extension functions solve main problem (laziness) of traditional API in simple an elegant way, without any overhead:
// Lazy debug
logger.debug { "Temperature set to ${newT()}. Old temperature was $oldT." }
// vs fluent:
logger.atDebug().addArgument { newT() }.log("Temperature set to {}. Old temperature was {}.", oldT)
// first statement, decompiled to java:
if (logger.isDebugEnabled()) {
String var6 = "Temperature set to " + newT() + ". Old temperature was " + oldT + '.';
logger.debug(var6);
}
// debug function source
inline fun Logger.debug(msg: () -> String) {
if (isDebugEnabled) {
debug(msg())
}
}
Conclusion
- New API enables new and future features, but sacrifices brevity and readability;
- Kotlin nicely solves existing API problems, without runtime overhead, thanks to inline functions.