Kotlin Tips and Tricks You May Not Know: #1 — Kotlin Logging
Kotlin-Logging simplifies logging with idiomatic Kotlin syntax

Introduction
Logging is a crucial part of every application, but traditional Java-based logging can be verbose and inefficient in Kotlin. Kotlin-Logging simplifies logging with cleaner, more expressive syntax and improves performance through lazy message evaluation, seamlessly integrating with popular logging backends like SLF4J and Logback.
In this article, you will learn how Kotlin-Logging makes logging easier and more efficient, from lazy and eager message evaluation to handling exceptions more idiomatically. We will also show how Kotlin-Logging supports advanced features like structured logging and MDC for richer, more traceable logs.
What is Kotlin-Logging?
Kotlin-Logging is a simple wrapper around popular Java logging libraries like SLF4J or Logback. Its main advantage is that it allows you to write logging statements that are cleaner and more Kotlin-like.
Seamless integration: If you are already using SLF4J or Logback in your projects, adding Kotlin-Logging is straightforward. You can keep using the same logging backend while enjoying Kotlin’s more expressive syntax.
You can add Kotlin-Logging and Logback to your Kotlin project as follows:
dependencies {
implementation("io.github.oshai:kotlin-logging-jvm:7.0.0")
implementation("org.slf4j:slf4j-api:2.0.16")
implementation("ch.qos.logback:logback-classic:1.5.11")
}
Chapter 3 of Kotlin Crash Course helps you set up a simple development environment to try out logging.
Idiomatic Logging in Kotlin
Kotlin-Logging makes logging much cleaner and more idiomatic by allowing lazy message evaluation. You can write logs like this:
import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger {}
fun main() {
val compliment = "Great job"
logger.debug { "Generating compliment: $compliment" } // Lazy evaluation, only executed if the log level is enabled
logger.info { "Compliment generated: $compliment" } // Lazy evaluation, only executed if the log level is enabled
}
Lazy evaluation means that the message inside {}
is only evaluated when the log level is actually enabled, reducing the performance cost of logging in production.
Handling Exceptions in Kotlin-Logging
Kotlin-Logging also provides a clean way to log exceptions. You can log errors in an expressive and idiomatic way by passing the exception into the log statement. It also allows you to combine exceptions with lazy log messages:
try {
// Some risky code
} catch (e: Exception) {
logger.error(e) { "An error occurred during execution" } // Logs both the exception and a message
}
This approach ensures that you not only capture the stack trace of the exception but also log a meaningful message. In Java, this would typically look like:
logger.error("An error occurred during execution", e);
With Kotlin-Logging, the syntax is cleaner and better suited to Kotlin’s style.
Adding Metadata
Kotlin-Logging allows you to enhance log entries with additional metadata, which is useful for tracking context across requests and operations. By attaching key-value pairs, such as customer IDs or order numbers, you can provide more meaningful logs that are easier to trace.
Structured Logging
Structured logging lets you embed metadata directly into log messages, making it ideal for scenarios where specific context is crucial. Structured logging becomes especially valuable in distributed systems or microservices, where logs with rich metadata can help pinpoint issues across different services or components.
For example, when processing an order, you can log customer and order IDs for better traceability:
import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger {}
fun processOrder(order: Order) {
// process order
// something failed
logException(order, Exception("Order processing exception"))
}
fun logException(order: Order, t: Throwable) {
logger.atError {
message = "Order processing failed"
cause = t
payload = buildMap(capacity = 2) {
put("customerId", order.customerId)
put("orderId", order.orderId)
}
}
}
fun main() {
processOrder(Order("C123", "O98765"))
}
data class Order(val customerId: String, val orderId: String)
By using structured logging, we avoid constructing complex log messages manually. Instead, we pass relevant metadata, such as customerId
and orderId
, as key-value pairs in the log, which makes the logs easier to parse and trace.
The output will look like this:
10:04:48.685 [main] ERROR LoggingExample -customerId="C123" orderId="O98765"- Order processing failed
java.lang.Exception: Order processing exception
at LoggingExampleKt.processOrder(LoggingExample.kt:12)
at LoggingExampleKt.main(LoggingExample.kt:27)
at LoggingExampleKt.main(LoggingExample.kt)
Mapped Diagnostic Context (MDC)
While structured logging works well for adding metadata to specific log entries, in situations where you need to consistently log the same metadata across multiple log statements — such as in long-running processes or within the same thread — MDC (Mapped Diagnostic Context) becomes a more powerful tool. Instead of manually attaching metadata to each log entry, MDC automatically adds shared context data to all log messages within a given execution flow. More details about MDC can be found in the SLF4J manual.
MDC allows you to add implicit context data, such as customerId
or orderId
, to all log messages in a thread or execution block. This is particularly useful when tracking context across multiple log statements within the same flow or request, as it simplifies the logging process and ensures consistency across related logs.
In Kotlin-Logging, MDC properties can be set using the withLoggingContext
function. This allows you to capture and propagate important metadata, without needing to pass it explicitly in every log statement.
Let us update our previous example to use MDC:
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext
private val logger = KotlinLogging.logger {}
fun processOrder(order: Order) =
withLoggingContext("customerId" to order.customerId, "orderId" to order.orderId) {
// process order
// something failed
logException(Exception("Order processing exception"))
}
// no need to repeat order details for every log line
fun logException(t: Throwable) {
logger.error (t) { "Order processing failed" }
}
fun main() {
processOrder(Order("C123", "O98765"))
}
data class Order(val customerId: String, val orderId: String)
To display the MDC properties in your logs, you need to configure your logger. For example, if you are using Logback, you will need to add configuration similar to the following in your logback.xml
file:
<configuration>
<!-- Console appender for logging to the console -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Use %X to include all MDC key-value pairs -->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X - %msg%n</pattern>
</encoder>
</appender>
<!-- Root logger configuration -->
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
In this example, MDC automatically adds customerId
and orderId
to all log entries within the withLoggingContext
block, avoiding the need to repeat the metadata for every log statement. The output will include the following information:
10:08:37.070 [main] ERROR LoggingExampleMDC - customerId=C123, orderId=O98765 - Order processing failed
java.lang.Exception: Order processing exception
at LoggingExampleMDCKt.processOrder(LoggingExampleMDC.kt:12)
at LoggingExampleMDCKt.main(LoggingExampleMDC.kt:21)
at LoggingExampleMDCKt.main(LoggingExampleMDC.kt)
This way, any logs within the context block will include these additional MDC properties, making it easier to track related logs across different services or requests.
MDC and Coroutines
MDC uses ThreadLocal
to store context information, therefore when using coroutines, the context must be explicitly propagated between threads.
Kotlin coroutines provide built-in support for MDC via the MDCContext
function, which ensures that the MDC data is transported between coroutine contexts. For example:
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
import kotlinx.coroutines.slf4j.MDCContext
private val logger = KotlinLogging.logger {}
fun main() = runBlocking {
withLoggingContext("customerId" to "C123", "orderId" to "O98765") {
launch(MDCContext()) {
logger.info { "Starting coroutine for order processing" }
// Simulate order processing
logger.info { "Order processed with status: Shipped" }
}
}
}
In this case, the MDCContext()
ensures that the customerId
and orderId
MDC properties are carried into the coroutine, allowing you to maintain the same context across concurrent operations.
To use MDC with coroutines, you need to add the following dependency to your project:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:<version>")
}
This ensures that your application can propagate MDC data correctly between coroutines.
Additional Information
- Older versions of Kotlin-Logging: Version 5 brought package structure changes and no longer bundles SLF4J, see change log for more information. It is not backward compatible, but you can run older and newer versions side by side.
- Emerging alternative — Klogging: A new Kotlin logging library offering a seamless structured logging with coroutine support. Still in development, Klogging shows promise for future projects but may not yet be stable for production use.
Conclusion
Kotlin-Logging is a powerful yet simple tool for adding structured, efficient logging to your Kotlin applications. By integrating with popular backends like SLF4J and Logback, Kotlin-Logging retains all the capabilities you are used to but with cleaner, more idiomatic syntax. Whether you are building a simple application or managing complex, distributed systems, Kotlin-Logging can simplify your logging practices. Its seamless integration with existing logging frameworks like SLF4J and Logback, combined with structured logging and MDC, ensures that your logs are not only cleaner and more efficient but also more informative and easy to trace.
Tip recap:
- Use Kotlin-Logging to simplify logging with clean syntax and improve performance with lazy message evaluation.
- Use structured logging when you need to add specific metadata (like customer ID or order number) to individual log entries for better traceability.
- Use MDC when you want to attach the same metadata across multiple log messages in the same workflow, ensuring consistent context without repeating data.
Try Kotlin-Logging in your next Kotlin project to enhance your logging experience!
For more Kotlin tips and tricks, visit my Medium blog homepage.