Почему «молчаливое» Kotlin-приложение — это мина замедленного действия
Представьте: вы запустили сервис на Kotlin, он работает как часы. Пользователи довольны, метрики зелёные. Но в один прекрасный день нагрузка скачет, latency растёт, а вы понятия не имеете, почему. Без логирования и мониторинга вы — капитан корабля без карты и радара. В этой статье мы разберём, как превратить ваше приложение из «чёрного ящика» в прозрачную систему, где каждый сбой виден невооружённым глазом.
Мы не будем копировать документацию. Вместо этого — реальные кейсы, готовые сниппеты и архитектурные решения, которые работают в production. Поехали.
Слона делим на части: логирование vs мониторинг
Прежде чем писать код, давайте договоримся о терминах. Логирование — это запись событий (ошибок, предупреждений, отладочной информации) в структурированном или текстовом виде. Мониторинг — сбор метрик (время ответа, количество запросов, загрузка CPU) и их визуализация. В идеале они работают в связке: логи нужны для глубокого анализа инцидента, метрики — для быстрого обнаружения аномалий.
Настройка логирования в Kotlin: от простого к сложному
Выбираем библиотеку: SLF4J + Logback — золотой стандарт
Kotlin отлично дружит с Java-экосистемой, поэтому SLF4J (фасад) и Logback (реализация) — выбор №1. Они легковесны, производительны и поддерживают асинхронное логирование.
Добавьте в build.gradle.kts:
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
kotlin-logging — это тонкая обёртка над SLF4J, которая делает код чище. Сравните:
Без обёртки:
private val logger = LoggerFactory.getLogger(MyService::class.java)
logger.info("User {} logged in", userId)
С kotlin-logging:
private val logger = KotlinLogging.logger {}
logger.info { "User $userId logged in" }
Ленивое вычисление строк — бонус для производительности: строка соберётся только если уровень логирования включён.
Конфигурация Logback: структурированные логи — must have
Создайте logback.xml в src/main/resources:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Мы используем LogstashEncoder — он превращает логи в JSON. Это критически важно, если вы отправляете логи в Elasticsearch, Loki или любой другой агрегатор. JSON-логи легко парсятся, фильтруются и анализируются.
Не забудьте добавить зависимость:
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
Пишем осмысленные логи: контекст решает всё
Логи вида «Error occurred» бесполезны. Добавляйте MDC (Mapped Diagnostic Context) — это как теги для ваших логов.
import org.slf4j.MDC
class OrderService {
private val logger = KotlinLogging.logger {}
fun processOrder(orderId: String, userId: String) {
MDC.put("orderId", orderId)
MDC.put("userId", userId)
try {
logger.info { "Processing order" }
// бизнес-логика
} catch (e: Exception) {
logger.error(e) { "Order processing failed" }
} finally {
MDC.clear()
}
}
}
Теперь в агрегаторе логов вы сможете найти все записи по конкретному заказу или пользователю за секунду.
Мониторинг Kotlin-приложений: собираем метрики с Micrometer
Почему Micrometer?
Micrometer — это фасад для метрик, как SLF4J для логов. Он поддерживает все популярные бэкенды: Prometheus, Datadog, Graphite, New Relic. Вы пишете код один раз, а метрики отправляются куда угодно.
Подключите в build.gradle.kts:
implementation("io.micrometer:micrometer-core:1.12.0")
implementation("io.micrometer:micrometer-registry-prometheus:1.12.0")
Счётчики, таймеры и гистограммы на практике
Рассмотрим три самых полезных типа метрик.
1. Счётчик (Counter) — сколько раз что-то произошло.
class PaymentService(private val meterRegistry: MeterRegistry) {
private val paymentCounter = Counter.builder("payments.total")
.description("Total number of payments")
.register(meterRegistry)
fun processPayment(amount: BigDecimal) {
paymentCounter.increment()
// логика оплаты
}
}
2. Таймер (Timer) — измеряет время выполнения.
class ExternalApiClient(private val meterRegistry: MeterRegistry) {
private val apiTimer = Timer.builder("external.api.latency")
.description("API call latency")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
fun fetchData(): String {
return apiTimer.recordCallable {
// HTTP-запрос
"data"
}
}
}
3. Гистограмма (DistributionSummary) — распределение значений (размер ответа, количество элементов и т.д.).
val responseSize = DistributionSummary.builder("http.response.size")
.baseUnit("bytes")
.publishPercentiles(0.5, 0.95)
.register(meterRegistry)
Экспортируем метрики в Prometheus
Добавьте эндпоинт /actuator/prometheus с помощью Spring Boot Actuator или вручную:
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.micrometer.prometheus.PrometheusMeterRegistry
fun Route.prometheusEndpoint(registry: PrometheusMeterRegistry) {
get("/metrics") {
call.respondText(registry.scrape())
}
}
В Ktor это выглядит естественно. В Spring Boot — просто включите management.endpoints.web.exposure.include=prometheus в application.yml.
Advanced техники: корреляция логов и метрик
Самая частая проблема — вы видите всплеск ошибок на графике, но не знаете, какие именно логи к нему привели. Решение: единый идентификатор запроса (traceId).
Используйте OpenTelemetry