Логирование в 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" }