Логирование в Kotlin приложениях: лучшие практики на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Логирование в Kotlin приложениях: лучшие практики на практике

Логирование — это не просто запись сообщений в консоль. В современных Kotlin-приложениях (серверные бэкенды, Android, Ktor, Spring Boot) правильно настроенное логирование является основой для мониторинга, отладки и аудита. Без него вы буквально работаете вслепую. В этой статье мы разберем лучшие практики логирования в Kotlin с реальными примерами кода и настройками production-уровня.

1. Выбор библиотеки: почему SLF4J + Logback — стандарт индустрии

В мире JVM стандартом де-факто является SLF4J (Simple Logging Facade for Java) в паре с Logback. Kotlin полностью совместим с Java-библиотеками, поэтому эта связка работает нативно. Для Kotlin существует удобная обертка kotlin-logging, которая добавляет extension-функции и inline-ленивость.

Подключение зависимостей (Gradle)

// build.gradle.kts
dependencies {
    implementation("io.github.microutils:kotlin-logging:3.0.5")
    implementation("ch.qos.logback:logback-classic:1.4.14")
}

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

import mu.KotlinLogging

private val logger = KotlinLogging.logger {}

fun processOrder(orderId: String) {
    logger.info { "Обработка заказа $orderId начата" }
    // бизнес-логика
    logger.debug { "Детали заказа: ${fetchOrderDetails(orderId)}" }
}

Обратите внимание на ленивое вычисление (лямбда {}): строка формируется только если уровень логирования активен. Это критически важно для производительности.

2. Уровни логирования: когда что использовать

Неправильный выбор уровня — одна из самых частых ошибок. Используйте эту таблицу как шпаргалку:

  • ERROR — только для фатальных ошибок, требующих немедленного вмешательства (исключения, потеря данных).
  • WARN — потенциально опасные ситуации, которые не ломают систему сейчас (например, превышение лимита попыток).
  • INFO — ключевые бизнес-события: старт/стоп сервиса, успешная регистрация, завершение платежа.
  • DEBUG — информация для разработчика: входные параметры, состояние объектов, шаги алгоритма.
  • TRACE — максимальная детализация, обычно для временного включения при поиске багов.

Пример правильного использования

class PaymentService(private val paymentGateway: PaymentGateway) {
    private val logger = KotlinLogging.logger {}

    fun processPayment(userId: String, amount: BigDecimal): PaymentResult {
        logger.info { "Начат платеж: userId=$userId, amount=$amount" }

        return try {
            val result = paymentGateway.charge(userId, amount)
            if (result.success) {
                logger.info { "Платеж успешен: transactionId=${result.transactionId}" }
            } else {
                logger.warn { "Платеж отклонен: userId=$userId, reason=${result.reason}" }
            }
            result
        } catch (e: PaymentException) {
            logger.error(e) { "Критическая ошибка платежного шлюза для userId=$userId" }
            throw e
        }
    }
}

3. Структурированное логирование: JSON и контекст

Современные системы (ELK, Grafana Loki, Datadog) лучше работают со структурированными логами. Logback умеет писать JSON, а Kotlin позволяет легко добавлять контекст через MDC (Mapped Diagnostic Context).

Настройка Logback для 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"/>
    </root>
</configuration>

Добавление контекста через MDC

import org.slf4j.MDC

class RequestFilter {
    fun filter(requestId: String, userId: String) {
        MDC.put("requestId", requestId)
        MDC.put("userId", userId)
        // все логи внутри этого потока будут содержать эти поля
        logger.info { "Обработка запроса" }
        // обязательно очищаем после завершения:
        MDC.clear()
    }
}

Результат в логах будет выглядеть так:

{"timestamp":"2024-01-15T10:30:00.123Z","level":"INFO","logger":"RequestFilter",
"message":"Обработка запроса","requestId":"abc-123","userId":"user-456"}

4. Антипаттерны и подводные камни

Даже с правильной библиотекой можно допустить ошибки, которые превратят логирование в проблему.

❌ Конкатенация строк

// ПЛОХО: строка собирается даже если уровень DEBUG выключен
logger.debug("User $userId logged in from IP: " + ipAddress)

// ХОРОШО: ленивое вычисление
logger.debug { "User $userId logged in from IP: $ipAddress" }

❌ Логирование чувствительных данных

// НИКОГДА так не делайте!
logger.info { "Пароль пользователя: $password" }

// Правильно: логируем только маскированные данные
val maskedPassword = password.take(2) + "***"
logger.debug { "Пароль (маскированный): $maskedPassword" }

❌ Избыточное логирование в циклах

// ПЛОХО: миллион логов в секунду
for (item in largeList) {
    logger.debug { "Обработка элемента $item" }
}

// ХОРОШО: логируем прогресс или используем счетчик
var processed = 0
for (item in largeList) {
    process(item)
    processed++
    if (processed % 1000 == 0) {
        logger.info { "Обработано $processed из ${largeList.size} элементов" }
    }
}

❌ Логирование исключений без stacktrace

// ПЛОХО: теряем стек вызовов
logger.error("Ошибка: ${exception.message}")

// ХОРОШО: передаем исключение как параметр
logger.error(exception) { "Ошибка при обработке userId=$userId" }                

Блоги

Рекомендаций по книгам