Avatar ·

Идиоматический способ логирования в Kotlin

Всем привет! Я новичок в Kotlin и перехожу с Java. В Java я привык использовать SLF4J с Logback и объявлять логгер так: ```java private static final Logger logger = LoggerFactory.getLogger(MyClass.class); ``` В Kotlin я вижу, что многие используют что-то вроде `companion object` для логгера. Но я не уверен, что это идиоматично. Плюс, я наткнулся на библиотеку `kotlin-logging`, которая вроде бы упрощает работу. Мой текущий код выглядит так: ```kotlin class MyService { companion object { private val logger = LoggerFactory.getLogger(MyService::class.java) } fun doSomething() { logger.info("Doing something") } } ``` Это работает, но мне кажется громоздким. Нужно ли каждый раз писать `companion object`? Есть ли более короткий и чистый способ? Я слышал про extension-функции и делегированные свойства, но не понимаю, как их применить к логированию. Также меня интересует: как правильно логировать в Kotlin, чтобы не было проблем с производительностью (например, лямбда-выражения для отложенного вычисления сообщений)? И стоит ли использовать `kotlin-logging` или лучше остаться на чистом SLF4J? Желательно увидеть примеры для разных случаев: классы, объекты, функции верхнего уровня. Заранее спасибо за помощь! <<>>

Идиоматический подход: делегированные свойства и kotlin-logging

В Kotlin лучший способ — использовать библиотеку kotlin-logging в сочетании с делегированными свойствами. Это дает минимальный бойлерплейт и полную безопасность типов.

1. Подключение зависимости

Добавьте в build.gradle.kts:

implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")

2. Базовое использование в классе

import mu.KotlinLogging

class MyService {
    private val logger = KotlinLogging.logger {}

    fun doSomething() {
        logger.info { "Doing something" }
    }
}

Здесь logger {} — это делегированное свойство, которое автоматически определяет имя класса. Никакого companion object не нужно.

3. Лямбда-выражения для производительности

Обратите внимание на синтаксис logger.info { ... } с фигурными скобками. Это лямбда, которая вычисляется только если уровень логирования включен. Аналог SLF4J logger.info("msg {}", expensive()):

logger.debug { "Сложное вычисление: ${expensiveCalculation()}" }

Если уровень DEBUG отключен, expensiveCalculation() не вызовется.

4. Для объектов (singleton)

В объектах Kotlin (например, для утилитных классов) логирование работает так же:

object MyUtils {
    private val logger = KotlinLogging.logger {}

    fun process() {
        logger.warn { "Это предупреждение из объекта" }
    }
}

5. Для функций верхнего уровня

Если вы используете функции вне классов, нужно указать имя вручную:

import mu.KLogger
import mu.KotlinLogging

private val logger: KLogger = KotlinLogging.logger("MyTopLevelFunctions")

fun topLevelFunction() {
    logger.error { "Ошибка в функции верхнего уровня" }
}

6. Альтернатива: делегированное свойство через lazy

Если вы не хотите использовать kotlin-logging, можно сделать свой делегат:

import org.slf4j.LoggerFactory

class MyService {
    private val logger by lazy { LoggerFactory.getLogger(this::class.java) }

    fun doSomething() {
        logger.info("Doing something")
    }
}

Но это не дает лямбда-синтаксиса для отложенного вычисления.

7. Что выбрать?

  • kotlin-logging — рекомендуемый идиоматический способ. Минимум кода, поддержка лямбд, автоматическое определение имени.
  • Чистый SLF4J — если вы не хотите добавлять зависимость, но тогда нужно использовать lazy или companion object.

8. Полный пример с разными уровнями

import mu.KotlinLogging

class OrderService {
    private val logger = KotlinLogging.logger {}

    fun processOrder(orderId: String) {
        logger.info { "Начало обработки заказа $orderId" }
        try {
            // бизнес-логика
            logger.debug { "Детали заказа: $orderId" }
        } catch (e: Exception) {
            logger.error(e) { "Ошибка при обработке заказа $orderId" }
        }
    }
}

Итог

Используйте kotlin-logging с KotlinLogging.logger {}. Это коротко, производительно и по-настоящему идиоматично для Kotlin.

<<>>

Альтернативный подход: inline-функции и макросы

Если вы хотите минимизировать зависимости и используете Kotlin 1.9+, можно создать свою утилиту на основе inline функций и reified типов. Это дает полный контроль без внешних библиотек.

import org.slf4j.LoggerFactory

inline fun  T.logger(): org.slf4j.Logger =
    LoggerFactory.getLogger(T::class.java)

class MyService {
    fun doSomething() {
        logger().info("Сообщение")
    }
}

Недостаток: каждый вызов logger() создает новый экземпляр логгера (хотя SLF4J кэширует их внутри). Для повышения производительности можно кэшировать через lazy:

class MyService {
    private val log by lazy { logger() }
    fun doSomething() {
        log.info("Сообщение")
    }
}

Этот подход хорош, если вы не хотите добавлять kotlin-logging, но все равно получаете чистый код. Однако он не поддерживает лямбда-синтаксис для отложенного вычисления (нужно использовать isDebugEnabled вручную).