Два часа ночи. PagerDuty зовёт. «API возвращает 500 для /orders endpoint». Открываете Kibana, видите тысячи строк логов и понимаете: вы не можете найти причину ошибки, потому что логи — мусор. User created, Request processed, Error: null — без контекста, без correlation ID, без структурированных полей.
Хорошее логирование — это не про библиотеку, а про формат и дисциплину. Но библиотека определяет, насколько легко следовать дисциплине. В Kotlin есть несколько вариантов, и каждый подходит для разных сценариев.
SLF4J + Logback: стандарт JVM
SLF4J (Simple Logging Facade for Java) — фасад логирования, который существует с 2005 года. Он не пишет логи сам — он делегирует реализацию (Logback, Log4j2, JUL). SLF4J — это интерфейс, Logback — это реализация. Пара SLF4J + Logback — стандарт в Spring Boot и большинстве JVM-фреймворков.
import org.slf4j.LoggerFactory
class OrderService(private val repository: OrderRepository) {
private val logger = LoggerFactory.getLogger(OrderService::class.java)
suspend fun createOrder(request: CreateOrderRequest): Order {
logger.info("Creating order for user={}", request.userId)
val order = try {
repository.save(request.toDomain())
} catch (e: DuplicateOrderException) {
logger.warn("Duplicate order detected for user={}, cartId={}",
request.userId, request.cartId)
throw e
} catch (e: Exception) {
logger.error("Failed to create order for user={}", request.userId, e)
throw OrderCreationException(request.userId, e)
}
logger.info("Order created successfully: orderId={}, totalAmount={}",
order.id, order.totalAmount)
return order
}
}
// logback.xml
// <configuration>
// <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
// <encoder>
// <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
// </encoder>
// </appender>
// <root level="INFO">
// <appender-ref ref="CONSOLE" />
// </root>
// </configuration>
Плюсы SLF4J: универсальность (любой бэкенд: Logback, Log4j2, JUL, Logstash), огромная экосистема (Kibana, ELK, Datadog — всё интегрируется), параметризованное логирование ("user={}" вместо конкатенации строк), маркерное логирование для фильтрации.
Минусы: это Java-библиотека, не Kotlin-нативная. LoggerFactory.getLogger() — Java-паттерн. Нет string interpolation ("user=$userId" будет вычислять строку всегда, даже если уровень логирования — ERROR). Нет структурированного логирования «из коробки» (нужен Logstash Logback Encoder).
Проблема string interpolation
Kotlin-разработчики любят string interpolation: "User $userId created order $orderId". Но в логировании это ловушка:
// ❌ Плохо: строка вычисляется ВСЕГДА, даже если DEBUG не включён
logger.debug("Processing order: $order with items: ${order.items.map { it.name }}")
// Конкатенация, toString(), map() — всё выполняется при каждом вызове
// ✅ SLF4J: строка форматируется ТОЛЬКО если DEBUG включён
logger.debug("Processing order: {} with items: {}", order, order.items.map { it.name })
// Замечание: order.items.map { it.name } всё равно вычисляется!
// ✅ Правильно с SLF4J: выносим вычисления за пределы аргументов
val itemNames = order.items.map { it.name }
logger.debug("Processing order: {} with items: {}", order.id, itemNames)
// itemNames вычисляется всегда — но это дешевле, чем форматирование строки
// ✅ Если вычисления дорогие: ленивое вычисление через лямбду (не SLF4J, но обходной путь)
if (logger.isDebugEnabled) {
logger.debug("Order details: {}", expensiveOrderDetails(order))
}
Kotlin Logging: идиоматичная обёртка
Kotlin Logging — тонкая обёртка над SLF4J, которая добавляет идиоматичность Kotlin. Главный плюс — ленивое вычисление лямбд:
import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger {}
class PaymentService(private val gateway: PaymentGateway) {
fun processPayment(order: Order): PaymentResult {
logger.info { "Processing payment for order=${order.id}, amount=${order.totalAmount}" }
val result = try {
gateway.charge(order.totalAmount, order.paymentMethod)
} catch (e: PaymentDeclinedException) {
logger.warn(e) { "Payment declined for order=${order.id}: ${e.reason}" }
return PaymentResult.Declined(e.reason)
} catch (e: Exception) {
logger.error(e) { "Payment gateway error for order=${order.id}" }
return PaymentResult.Error(e.message ?: "Unknown error")
}
logger.info { "Payment successful: transactionId=${result.transactionId}, orderId=${order.id}" }
return PaymentResult.Success(result.transactionId)
}
}
Kotlin Logging использует лямбда-блоки { } для ленивых вычислений. Если уровень логирования INFO, а вы пишете logger.debug { expensiveComputation() } — лямбда не будет выполнена. Это устраняет главную проблему SLF4J в Kotlin.
Kotlin Logging — это просто обёртка над SLF4J. Она использует тот же бэкенд SLF4J (Logback, Log4j2). Все ваши logback.xml, appenders, filters продолжают работать. Вы не меняете инфраструктуру логирования — вы меняете только API для записи логов.
Структурированное логирование с Kotlin Logging
Для структурированного логирования (JSON-формат для Kibana/Datadog) нужен MDC (Mapped Diagnostic Context) из SLF4J:
import io.github.oshai.kotlinlogging.KotlinLogging
import org.slf4j.MDC
private val logger = KotlinLogging.logger {}
class OrderController(
private val orderService: OrderService
) {
fun createOrder(request: HttpRequest): HttpResponse {
// Устанавливаем correlation ID для всех логов в этом запросе
val correlationId = request.header("X-Correlation-Id") ?: UUID.randomUUID().toString()
MDC.put("correlationId", correlationId)
MDC.put("userId", request.userId.toString())
MDC.put("sourceIp", request.remoteAddress)
return try {
val order = orderService.createOrder(request.body)
logger.info { "Order created: orderId=${order.id}" }
HttpResponse.Created(order)
} finally {
MDC.clear() // Очищаем MDC после обработки запроса
}
}
}
// В logback.xml — JSON encoder для структурированного логирования
// <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
// <encoder class="net.logstash.logback.encoder.LogstashEncoder">
// <includeMdc>true</includeMdc>
// </encoder>
// </appender>
MDC автоматически добавляет поля в каждый log-запрос. В JSON-логе вы увидите: {"message": "Order created", "orderId": 123, "correlationId": "abc-123", "userId": 456}. В Kibana можно фильтровать по correlationId и видеть все логи одного запроса.
klogging: чистый Kotlin-логгер
klogging — относительно новый логгер, написанный на чистом Kotlin (без JVM, без SLF4J). Он поддерживает Kotlin Multiplatform и предоставляет нативный Kotlin API со структурированным логированием из коробки.
import klogging.Klogging
import klogging.KLogEvent
import klogging.Logger
class InventoryService : Klogging {
private val logger = logger()
fun reserveItem(productId: String, quantity: Int) {
logger.info("Reserving item" ,"productId" to productId, "quantity" to quantity)
val available = checkInventory(productId)
if (available < quantity) {
logger.warn(
"Insufficient inventory",
"productId" to productId,
"requested" to quantity,
"available" to available
)
throw InsufficientStockException(productId, available, quantity)
}
// ... резервирование ...
logger.info("Item reserved", "productId" to productId, "newStock" to (available - quantity))
}
}
klogging автоматически превращает именованные пары в структурированные поля. Каждое log-событие — это KLogEvent с timestamp, уровнем, сообщением, именем логгера и словарём структурированных полей. Бэкенды: консоль (цветная), файл, JUL, SLF4J, STDOUT (JSON).
Главное преимущество klogging — KMP (Kotlin Multiplatform). Если вы пишете общий код для JVM + iOS + JS — klogging работает везде. SLF4J привязан к JVM.
Минусы klogging: молодая библиотека (меньше сообщества, меньше интеграций), нет зрелой конфигурации через XML (как у Logback), нет маркерной фильтрации, нет MDC (вместо этого использует контекст корутин).
CoroutineContext + MDC: ловушки
MDC — thread-local хранилище. В мире корутин, где поток переключается, MDC теряется. Это критическая проблема:
// ❌ Проблема: MDC теряется при переключении Dispatchers
suspend fun processOrder(orderId: Long) {
MDC.put("orderId", orderId.toString())
// Меняем dispatcher — MDC может потеряться!
withContext(Dispatchers.IO) {
repository.findById(orderId) // MDC с orderId здесь может быть пустым!
}
// Dispatchers.Default — другой поток, MDC пуст
productService.validate(orderId) // MDC пуст!
}
// ✅ Решение: проброс MDC через CoroutineContext
val MDCContext = object : ThreadContextElement<Map<String, String>> {
override fun updateThreadContext(context: CoroutineContext): Map<String, String> {
val snapshot = MDC.getCopyOfContextMap() ?: emptyMap()
context[MDCKey]?.let { MDC.setContextMap(it) }
return snapshot
}
override fun restoreThreadContext(context: CoroutineContext, oldState: Map<String, String>) {
MDC.setContextMap(oldState)
}
private data class MDCKey(val contextMap: Map<String, String>) : CoroutineContext.Element {
override val key: CoroutineContext.Key<*> get() = Key
companion object Key : CoroutineContext.Key<MDCKey>
}
}
// Использование
suspend fun processOrder(orderId: Long) = withContext(MDCContext) {
MDC.put("orderId", orderId.toString())
MDC.put("correlationId", currentCorrelationId())
withContext(Dispatchers.IO) {
repository.findById(orderId) // MDC сохранён!
}
withContext(Dispatchers.Default) {
productService.validate(orderId) // MDC сохранён!
}
}
Или используйте готовые библиотеки: kotlinx-coroutines-slf4j (поддерживает автоматический проброс MDC) или Spring Boot 3.2+ (проброс MDC встроен в MDCContextHolder).
Сравнительная таблица
Фича | SLF4J | Kotlin Logging | klogging
----------------------------|------------|----------------|----------
Kotlin-нативный API | Нет | Да | Да
Поддержка KMP | Нет | Нет | Да
Ленивые вычисления | Вручную | Лямбда {} | Лямбда {}
Структурированное логирование| Через энкодер| Через MDC+энкодер| Встроено
Проброс MDC в корутинах | Вручную | Вручную | Вручную
Экосистема (Kibana, ELK) | Отлично | Отлично | Хорошо
Конфигурация Logback | XML/Groovy | XML/Groovy | Kotlin DSL
Зрелость | 19 лет | 5 лет | 2 года
Сообщество | Огромное | Большое | Маленькое
Размер библиотеки | 30 КБ | 15 КБ | 80 КБ
Мой вердикт
Для Ktor server-side — Kotlin Logging. Это идиоматичный Kotlin API поверх зрелой инфраструктуры SLF4J+Logback. Ленивые лямбды, MDC для структурированного логирования, все существующие интеграции (ELK, Datadog, Sentry) работают из коробки. Наш стандартный стек: Kotlin Logging + Logback + Logstash Encoder (JSON) + проброс MDC в корутинах.
Для Spring Boot — SLF4J напрямую. Spring Boot уже настраивает Logback с JSON-энкодером, и Kotlin Logging не добавляет существенной ценности. Параметризованного логирования SLF4J достаточно.
Для Kotlin Multiplatform — klogging. Единственный вариант, который работает на всех платформах. Но будьте готовы к меньшему количеству готовых интеграций и документации.
И главное: библиотека — это 10% успеха. Формат логов, correlation ID, структурированные поля, уровни логирования (не логируйте на INFO то, что нужно только при DEBUG) — вот что делает логи полезными при расследовании инцидентов.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье