Avatar ·

Идиоматичный способ логирования в Kotlin: как правильно и без boilerplate?

Привет, коллеги! Я новичок в Kotlin (перешёл с Java) и не могу понять, как правильно организовать логирование в проекте. В Java я привык писать что-то вроде: ```kotlin private static final Logger log = LoggerFactory.getLogger(MyClass.class); ``` А потом использовать `log.info(...)`. В Kotlin это выглядит громоздко и некрасиво: приходится каждый раз указывать `javaClass` или `this::class.java`. Получается много boilerplate-кода. Я пробовал писать так: ```kotlin class MyService { private val logger = LoggerFactory.getLogger(MyService::class.java) fun doSomething() { logger.info("Doing something") } } ``` Но это всё равно повторяется в каждом классе. Слышал про kotlin-logging и про extension properties, но не до конца понимаю, как это работает идиоматично. Подскажите, пожалуйста, как принято логировать в Kotlin-проектах, чтобы код был чистым, кратким и без лишних зависимостей? Может, есть какой-то стандартный паттерн или библиотека, которая считается лучшей практикой? <<>>

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

Действительно, в Kotlin не принято писать private val logger = LoggerFactory.getLogger(...) в каждом классе. Это нарушает принцип DRY и выглядит архаично. Есть несколько элегантных решений, и самое популярное из них — использование extension properties вместе с библиотекой kotlin-logging (или даже без неё, используя встроенные средства).

1. Extension property (встроенное решение, без библиотек)

Вы можете объявить extension property для любого класса, который наследуется от Any (а это все классы). Самый простой способ — использовать lazy-делегат:

import org.slf4j.Logger
import org.slf4j.LoggerFactory

// Extension property для любого класса
val <T : Any> T.logger: Logger
    get() = LoggerFactory.getLogger(this::class.java)

// Использование:
class MyService {
    fun doSomething() {
        logger.info("Doing something from ${this::class.simpleName}")
    }
}

fun main() {
    MyService().doSomething()
    // Выведет: Doing something from MyService
}

Как это работает:
- <T : Any> T.logger — это extension property, доступная для любого ненулевого объекта.
- this::class.java получает Java-класс объекта, что идеально подходит для SLF4J.
- Свойство вычисляется каждый раз при обращении (не lazy). Если хотите ленивую инициализацию, можно сделать так:

val <T : Any> T.logger: Logger by lazy {
    LoggerFactory.getLogger(this::class.java)
}

2. Библиотека kotlin-logging (рекомендуется)

Это де-факто стандарт для логирования в Kotlin. Она предоставляет ещё более чистый синтаксис и дополнительные фичи, такие как лямбды для ленивого вычисления сообщений (чтобы не платить за строковую конкатенацию, если уровень логирования не включён).

Добавьте зависимость в build.gradle.kts:

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

Теперь ваш код будет выглядеть так:

import mu.KotlinLogging

private val logger = KotlinLogging.logger {}

class MyService {
    fun doSomething() {
        logger.info { "Doing something" }  // Обратите внимание на фигурные скобки!
        logger.debug { "Complex message: ${heavyOperation()}" } // Ленивое вычисление
    }
}

Преимущества:
- KotlinLogging.logger {} — это макрос, который автоматически подставляет имя класса, где он вызван (через inline-функцию).
- Лямбда { "message" } гарантирует, что строка будет вычислена только если соответствующий уровень логирования активен.
- Не нужно писать extension properties вручную.

3. Для Android (если вы не используете SLF4J)

В Android-разработке часто используют android.util.Log. Для него можно сделать аналогичный extension:

inline fun <reified T> T.logDebug(message: String) {
    android.util.Log.d(T::class.java.simpleName, message)
}

// Использование:
class MyActivity : AppCompatActivity() {
    fun test() {
        logDebug("Activity created")
    }
}

Итоговая рекомендация

  • Для серверных проектов (Ktor, Spring Boot): используйте kotlin-logging + SLF4J. Это идиоматично, производительно и широко принято в сообществе.
  • Для небольших проектов или если не хотите библиотек: напишите один extension property val Any.logger в общем файле (например, Logging.kt).
  • Избегайте: объявления LoggerFactory.getLogger(...) в каждом классе — это не Kotlin-way.

Также обратите внимание, что в Kotlin принято использовать ленивые вычисления для сообщений (лямбды). Это позволяет избежать затрат на формирование строки, если уровень логирования не включён (например, debug в продакшене). Библиотека kotlin-logging поддерживает это из коробки.

<<>>

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

Если вы не хотите использовать extension properties или внешние библиотеки, можно обойтись inline-функцией с reified-типом. Это особенно удобно, когда вам нужно логировать не из экземпляра класса, а из статического контекста (companion object).

import org.slf4j.Logger
import org.slf4j.LoggerFactory

// Inline функция с reified типом
inline fun <reified T> loggerFor(): Logger {
    return LoggerFactory.getLogger(T::class.java)
}

// Использование:
class MyService {
    companion object {
        private val logger = loggerFor<MyService>()
    }

    fun doWork() {
        logger.info("Working...")
    }
}

Плюсы этого подхода:
- reified позволяет получить класс без рефлексии (благодаря inline-механизму).
- Не нужно писать ::class.java каждый раз.
- Можно использовать в companion object для статического логирования (например, для фабричных методов).

Минусы:
- Нужно явно указывать тип: loggerFor<MyService>() — чуть более многословно, чем logger extension.
- Не работает для объектов, у которых нет экземпляра (например, внутри object без companion).

Этот способ хорошо сочетается с первым: вы можете использовать extension property для экземпляров, а loggerFor — для статических контекстов. Но в большинстве случаев достаточно первого решения (extension property) — оно проще и короче.