Загрузка
Все статьи

Логирование в Kotlin: klogging vs SLF4J + Kotlin Logging — что выбрать

Логирование — первая линия расследования при P0-инцидентах. Но в Kotlin есть нюансы: string interpolation, ленивые вычисления, структурированное логирование, correlation ID. Сравниваю klogging, SLF4J + Logback, Kotlin Logging и объясняю, почему формат логов важнее библиотеки.

Два часа ночи. 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) — вот что делает логи полезными при расследовании инцидентов.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

12 июня 2026 11 мин

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин