Logging in Kotlin Applications: Best Practices
Logging is not just about recording events, but a key tool for debugging, monitoring, and analyzing application performance. In the Kotlin ecosystem, thanks to full compatibility with Java, all popular libraries such as SLF4J and Logback are available. However, Kotlin offers its own idiomatic approaches that make code cleaner and safer. In this article, we will cover the best practices for logging in Kotlin: from choosing a library to configuring structured logs for production.
1. Choosing a Logging Library for Kotlin
The most popular stack for Kotlin applications is SLF4J (facade) + Logback (implementation). However, for Kotlin, it is recommended to use the kotlin-logging wrapper, which adds inline functions and avoids unnecessary overhead.
// build.gradle.ktsdependencies { implementation("io.github.microutils:kotlin-logging:3.0.5") implementation("ch.qos.logback:logback-classic:1.4.14")}Advantages of kotlin-logging:
- Lazy logger initialization — the logger is created only on first access.
- Inline functions — the logging level check occurs at compile time, reducing overhead.
- Support for suspend functions — correct operation in coroutines.
Example of logger declaration:
import mu.KotlinLogging
private val logger = KotlinLogging.logger {}2. Correct Logging Levels
Using the correct level is the foundation of readable logs. In Kotlin, as in Java, 5 levels are available: TRACE, DEBUG, INFO, WARN, ERROR.
Recommendations:
- TRACE — only for detailed debugging (e.g., method entry/exit).
- DEBUG — information for the developer (variable values, object state).
- INFO — business events (request processed, user authorized).
- WARN — abnormal situations that do not break the application (connection retry).
- ERROR — exceptions and failures requiring attention.
Example with different levels:
logger.debug { "Processing request: $requestId" }logger.info { "User ${user.name} successfully registered" }logger.warn { "Connection pool is 90% full, current size: $poolSize" }logger.error(ex) { "Error saving order ${orderId}" }Important: In Kotlin, to pass an exception, use logger.error(throwable) { "message" }, not logger.error("message", throwable) — this is more idiomatic.
3. Structured Logging (JSON)
In modern microservice architectures, text logs are inconvenient for analysis. It is better to use structured logs in JSON format. This makes it easy to filter logs in ELK, Grafana, or Splunk.
Configuring Logback for JSON logs:
// logback.xml<configuration> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender>
<root level="INFO"> <appender-ref ref="JSON"/> </root></configuration>To add custom fields, use MDC (Mapped Diagnostic Context):
import org.slf4j.MDC
fun processOrder(orderId: String) { MDC.put("orderId", orderId) MDC.put("userId", currentUserId()) logger.info { "Starting order processing" } // ... logic MDC.clear()}