У нас 12 микросервисов. Типичный запрос от пользователя проходит через 4–5 из них: шлюз, сервис авторизации, основной бизнес-сервис, сервис уведомлений, сервис аналитики. Когда фронтенд жалуется, что «страница грузится 8 секунд», — первый вопрос: где тормозит? И вот тут начинается боль.
Мы сидели в Slack, передавали друг другу логи: «у меня запрос отработал за 200мс», «а у меня за 50мс», «а вот этот — 6 секунд». Без единого контекста. Без понимания, что это вообще один и тот же запрос. Потом кто-то находил correlation ID в заголовках, дальше ручной grep по логам... В общем, вы понимаете.
Что такое OpenTelemetry на практике
OpenTelemetry (OTel) — это стандарт для сбора телеметрии: трейсов, метрик и логов. Для распределённого трейсинга нас интересуют три понятия:
Trace — весь путь запроса через систему. Уникальный trace-id пробрасывается через все сервисы.
Span — единичная операция внутри трейса. HTTP-запрос, вызов БД, внешнее API — всё это спаны. У каждого спана есть родитель, поэтому строится дерево.
Context Propagation — механизм проброса trace-id и span-id между сервисами. Обычно через заголовки traceparent и tracestate.
Когда всё работает, в Jaeger или Grafana Tempo вы видите дерево вызовов и сразу понимаете, где узкое место. Это меняет правила игры.
Настройка в Spring Boot (Kotlin)
Со Spring Boot всё относительно просто благодаря авто-конфигурации. Добавляем зависимости:
// build.gradle.kts
dependencies {
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.5.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.36.0")
}И конфигурируем в application.yml:
otel:
exporter:
otlp:
endpoint: http://jaeger-collector:4317
service:
name: order-service
traces:
exporter: otlp
resource:
attributes:
deployment.environment: productionСтартер автоматически создаёт спаны для всех HTTP-запросов, вызовов JDBC, gRPC и даже для Redis. Можно буквально ничего не писать в коде — и уже получить базовое трейсирование.
Но «базовое» — это не всегда достаточно.
Кастомные спаны: когда автоматика не хватает
У нас есть важный бизнес-процесс — расчёт скидки. Он состоит из нескольких шагов: загрузка правил, проверка пользователя, применение промокода, расчёт итоговой цены. Если мы обернём всё это в один спан, то при проблемах не поймём, на каком именно шаге тормозим.
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.api.trace.StatusCode
class PricingService(
private val tracer: Tracer,
private val ruleRepository: RuleRepository,
private val userRepository: UserRepository,
private val promoService: PromoService
) {
fun calculatePrice(orderId: String, userId: String): PriceResult {
val span = tracer.spanBuilder("PricingService.calculatePrice")
.setAttribute("order.id", orderId)
.setAttribute("user.id", userId)
.startSpan()
return try {
val rules = span.withChild("loadRules") {
ruleRepository.findActive()
}
val user = span.withChild("fetchUser") {
userRepository.findById(userId)
}
val promoDiscount = span.withChild("applyPromo") {
promoService.getDiscount(orderId, user.tier)
}
val result = PriceResult(
basePrice = rules.basePrice,
discount = promoDiscount,
finalPrice = rules.basePrice - promoDiscount
)
span.setAttribute("price.final", result.finalPrice)
result
} catch (e: Exception) {
span.recordException(e)
span.status = StatusCode.ERROR
throw e
} finally {
span.end()
}
}
}
// Утилита для дочерних спанов
fun Span.withChild(name: String, block: () -> Unit) {
val child = tracer.spanBuilder(name)
.setParent(Context.current().with(this))
.startSpan()
try { block() } finally { child.end() }
}Обратите внимание: я добавил атрибуты order.id и user.id. Это критически важно — без атрибутов вы увидите в Jaeger безликие спаны без возможности фильтрации. Атрибуты — это поисковые фильтры вашей системы наблюдаемости.
Настройка в Ktor
С Ktor чуть больше ручной работы, но это не страшно. Используем opentelemetry-kotlin от JetBrains:
fun Application.configureTracing() {
val openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(
TracerProvider.builder()
.addSpanProcessor(
BatchSpanProcessor.builder(
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://jaeger-collector:4317")
.build()
).build()
)
.setResource(
Resource.getDefault().merge(
Resource.builder()
.put("service.name", "notification-service")
.put("deployment.environment", "production")
.build()
)
)
.build()
)
.buildAndRegisterGlobal()
val tracer = openTelemetry.getTracer("notification-service", "1.0.0")
intercept(ApplicationCallPipeline.Plugins) {
val span = tracer.spanBuilder(
"${call.request.httpMethod.value} ${call.request.uri}"
).setSpanKind(SpanKind.SERVER).startSpan()
try {
proceed()
span.status = if (call.response.status() < HttpStatusCode.InternalServerError)
SpanStatus.OK else SpanStatus.ERROR
} catch (e: Throwable) {
span.recordException(e)
span.status = SpanStatus.ERROR
throw e
} finally {
span.end()
}
}
}Экспорт в Jaeger
Jaeger поднимается одним docker-compose:
version: "3.8"
services:
jaeger:
image: jaegertracing/all-in-one:1.54
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
environment:
- COLLECTOR_OTLP_ENABLED=trueЧерез пару минут после деплоя вы открываете Jaeger UI, выбираете сервис, и видите полную картину. Первый раз, когда я увидел трейс нашего заказа — 7 сервисов, 23 спана, и чётко видно, что inventory-service делает 4 последовательных запроса в БД вместо одного — это был момент «ааа, вот оно что».
Практические советы
Первое: не пытайтесь трейсировать всё. Спаны для каждого лога — это шум. Трассируйте границы сервисов, внешние вызовы, тяжёлые вычисления. Если спанов слишком много — Jaeger становится нечитаемым.
Второе: атрибуты должны быть согласованы между сервисами. Договоритесь с командой, какие атрибуты использовать: order.id, а не orderId в одном сервисе и order_id в другом.
Третье: sampling. В продакшене не нужно трейсировать 100% запросов. Мы используем head-based sampling: 10% обычных запросов и 100% запросов с ошибками.
Четвёртое: логируйте trace-id в каждом логе. Это связывает логи с трейсами. Без этого вы будете переключаться между двумя инструментами.
Распределённый трейсинг — это не роскошь, а базовая необходимость в микросервисной архитектуре. Если у вас больше трёх сервисов и вы ещё не трейсируете — вы теряете часы на отладку каждую неделю.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье