Почему observability — это must-have для Kotlin-приложений
Представьте: ваше Kotlin-приложение работает в production, и вдруг пользователи жалуются, что запросы выполняются по 10 секунд. Вы заходите в логи — тишина. Метрики показывают 100% CPU, но не говорят, какой именно endpoint тормозит. Знакомо?
Добро пожаловать в мир, где observability (наблюдаемость) — не опция, а необходимость. В отличие от традиционного мониторинга, который просто отвечает «да/нет» на вопрос «жива ли система», observability позволяет задавать вопросы, о которых вы раньше не догадывались. И Kotlin, с его лаконичностью и мощной экосистемой, идеально подходит для внедрения этой культуры.
В этой статье мы разберем три столпа observability — трейсинг, метрики и логи — и покажем, как подружить их с Kotlin. Никакой воды, только реальные примеры кода.
Трейсинг: путешествие запроса по микросервисам
Распределенный трейсинг (tracing) — это как GPS для вашего запроса. Вы видите, через какие сервисы прошел запрос, сколько времени занял каждый шаг и где произошла задержка. Для Kotlin лучший друг в этом деле — OpenTelemetry (OTel).
Настройка OpenTelemetry в Kotlin
Добавьте зависимости в build.gradle.kts:
dependencies {
implementation("io.opentelemetry:opentelemetry-api:1.36.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.36.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.36.0")
implementation("io.opentelemetry:opentelemetry-extension-kotlin:1.36.0")
}
Теперь создадим простой трейсер:
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.context.Scope
class OrderService(private val tracer: Tracer = GlobalOpenTelemetry.getTracer("order-service")) {
fun processOrder(orderId: String) {
val span: Span = tracer.spanBuilder("processOrder")
.setAttribute("order.id", orderId)
.startSpan()
span.use { // use() автоматически закроет span
try {
// Имитация работы
Thread.sleep(100)
validateOrder(orderId)
chargePayment(orderId)
} catch (e: Exception) {
span.recordException(e)
span.setStatus(StatusCode.ERROR, "Order processing failed")
throw e
}
}
}
private fun validateOrder(orderId: String) {
val span: Span = tracer.spanBuilder("validateOrder")
.setAttribute("order.id", orderId)
.startSpan()
span.use {
Thread.sleep(50) // Имитация валидации
}
}
private fun chargePayment(orderId: String) {
val span: Span = tracer.spanBuilder("chargePayment")
.setAttribute("order.id", orderId)
.startSpan()
span.use {
Thread.sleep(150) // Имитация платежа
}
}
}
Обратите внимание на span.use — это extension-функция из opentelemetry-extension-kotlin, которая автоматически закрывает span после выполнения блока. Красота Kotlin в действии!
Контекстная передача через корутины
В Kotlin мы активно используем корутины. OpenTelemetry отлично с ними дружит через механизм Context:
import kotlinx.coroutines.withContext
import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
suspend fun processOrderAsync(orderId: String) = withContext(Context.current()) {
val span = tracer.spanBuilder("processOrderAsync").startSpan()
span.use {
// Ваша асинхронная логика
}
}
Трейсинг раскрывает полную картину: вы видите, что 80% времени уходит на платежный шлюз, а не на валидацию. Теперь вы знаете, что оптимизировать.
Метрики: цифры, которые говорят громче слов
Метрики — это количественная характеристика системы: количество запросов в секунду, время ответа, загрузка CPU. В Kotlin-мире стандарт — Micrometer (часто в связке с Spring Boot Actuator или Ktor).
Сбор метрик с Micrometer
Добавьте зависимости:
dependencies {
implementation("io.micrometer:micrometer-core:1.12.0")
implementation("io.micrometer:micrometer-registry-prometheus:1.12.0")
}
Создадим метрики для HTTP-сервера на Ktor:
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import java.util.concurrent.TimeUnit
class MetricsService {
private val registry: MeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
// Счетчик запросов
private val requestCounter = registry.counter("http.requests.total", "endpoint", "/api/orders")
// Гистограмма времени ответа
private val responseTime: Timer = registry.timer("http.response.time")
fun recordRequest() {
requestCounter.increment()
}
fun recordResponseTime(duration: Long, unit: TimeUnit) {
responseTime.record(duration, unit)
}
fun scrapeMetrics(): String {
return (registry as PrometheusMeterRegistry).scrape()
}
}
// Использование в Ktor
fun Application.module() {
val metricsService = MetricsService()
routing {
get("/api/orders") {
val start = System.nanoTime()
// Обработка запроса
metricsService.recordRequest()
metricsService.recordResponseTime(System.nanoTime() - start, TimeUnit.NANOSECONDS)
call.respondText("OK")
}
get("/metrics") {
call.respondText(metricsService.scrapeMetrics(), ContentType.Text.Plain)
}
}
}
Теперь Prometheus будет забирать метрики с /metrics, а Grafana рисовать красивые дашборды. Вы увидите, как растет latency после каждого деплоя — и сможете откатиться до того, как пользователи начнут писать в поддержку.
Логирование: от «println» к структурированным логам
Логи — это дневник приложения. Но простой println в production не пройдет. Нам нужны структурированные логи (JSON), которые можно парсить и искать. В Kotlin используем Logback + Kotlin Logging.
Настройка Kotlin Logging
Добавьте зависимости:
dependencies {
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
}
Конфигурация logback.xml для JSON-логов:
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>false</includeContext>
</encoder