Logging in Kotlin Applications: Best Practices in Practice
Logging is not just about writing messages to the console. In modern Kotlin applications (server backends, Android, Ktor, Spring Boot), properly configured logging is the foundation for monitoring, debugging, and auditing. Without it, you are literally working blind. In this article, we will break down best practices for logging in Kotlin with real code examples and production-level configurations.
1. Choosing a Library: Why SLF4J + Logback is the Industry Standard
In the JVM world, the de facto standard is SLF4J (Simple Logging Facade for Java) paired with Logback. Kotlin is fully compatible with Java libraries, so this combination works natively. For Kotlin, there is a convenient wrapper kotlin-logging that adds extension functions and inline laziness.
Adding Dependencies (Gradle)
// build.gradle.ktsdependencies { implementation("io.github.microutils:kotlin-logging:3.0.5") implementation("ch.qos.logback:logback-classic:1.4.14")}Basic Usage
import mu.KotlinLogging
private val logger = KotlinLogging.logger {}
fun processOrder(orderId: String) { logger.info { "Processing order $orderId started" } // business logic logger.debug { "Order details: ${fetchOrderDetails(orderId)}" }}Note the lazy evaluation (lambda {}): the string is formed only if the logging level is active. This is critically important for performance.
2. Log Levels: When to Use What
Choosing the wrong level is one of the most common mistakes. Use this table as a cheat sheet:
- ERROR — only for fatal errors requiring immediate intervention (exceptions, data loss).
- WARN — potentially dangerous situations that do not break the system now (e.g., exceeding the attempt limit).
- INFO — key business events: service start/stop, successful registration, payment completion.
- DEBUG — information for the developer: input parameters, object state, algorithm steps.
- TRACE — maximum detail, usually for temporary enabling when hunting bugs.
Example of Correct Usage
class PaymentService(private val paymentGateway: PaymentGateway) { private val logger = KotlinLogging.logger {}
fun processPayment(userId: String, amount: BigDecimal): PaymentResult { logger.info { "Payment started: userId=$userId, amount=$amount" }
return try { val result = paymentGateway.charge(userId, amount) if (result.success) { logger.info { "Payment successful: transactionId=${result.transactionId}" } } else { logger.warn { "Payment declined: userId=$userId, reason=${result.reason}" } } result } catch (e: PaymentException) { logger.error(e) { "Critical payment gateway error for userId=$userId" } throw e } }}3. Structured Logging: JSON and Context
Modern systems (ELK, Grafana Loki, Datadog) work better with structured logs. Logback can write JSON, and Kotlin makes it easy to add context via MDC (Mapped Diagnostic Context).
Configuring Logback for JSON (logback.xml)
<?xml version="1.0" encoding="UTF-8"?><configuration> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.JsonEncoder"/> </appender>
<root level="INFO"> <appender-ref ref="JSON"/>