Kotlin Observability: трейсинг, метрики и логи на практике

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

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

Начать курс

Почему 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                

Блоги

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