Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

OpenTelemetry и распределённая трассировка: меньше время на поиск проблем и точные SLA без привязки к вендору

Разработка и технологии14 февраля 2026 г.
Покажу, как за 1–2 дня внедрить распределённую трассировку с OpenTelemetry, связать логи и метрики с запросами, настроить разумное семплирование и не утонуть в счёте за хранение. Будут готовые файлы для запуска локально, примеры кода на Node.js/Express и практические советы по безопасности данных.
OpenTelemetry и распределённая трассировка: меньше время на поиск проблем и точные SLA без привязки к вендору

Оглавление

  • Зачем бизнесу распределённая трассировка
  • Как устроен OpenTelemetry и чем он удобен
  • Базовый стенд: Collector + Jaeger через Docker Compose
  • Быстрое внедрение в Node.js/Express: код и логи с trace_id
  • Обогащение спанов: база, внешние вызовы, бизнес-метки
  • Связка логов и метрик с трассами без боли
  • Семплирование: как удержать стоимость и не потерять пользу
  • Защита данных: маскирование, политика хранения, доступ
  • Алерты и SLO на основе трассировок
  • Частые ошибки и как их избежать
  • Чек-лист внедрения и оценка эффекта

Зачем бизнесу распределённая трассировка

Когда продукт разрастается, «где тормозит?» превращается в игру «угадай микросервис». Логи расползаются по разным хранилищам, метрики показывают только симптомы, а время расследования инцидента растёт. Распределённая трассировка отвечает на конкретные вопросы:

  • какой путь прошёл запрос от шлюза до базы;
  • где именно потратили время — сеть, база, сторонний API, вычисления;
  • какой процент ошибок и в каких узлах;
  • как это влияет на SLA/SLO.

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

Как устроен OpenTelemetry и чем он удобен

OpenTelemetry (OTel) — открытый стандарт и набор библиотек для телеметрии (трассы, метрики, логи). Он не привязывает к конкретному вендору: можно отправлять данные в Jaeger, Tempo, ClickHouse-стек, Grafana Cloud, Honeycomb, любой совместимый бэкенд.

Ключевые элементы:

  • SDK/автоинструментация — библиотека в вашем коде, которая создаёт «спаны» (отрезки работы) и собирает атрибуты.
  • Экспортёр — отправляет данные из приложения в Collector.
  • Collector — отдельный сервис: принимает, фильтрует, семплирует, анонимизирует и перенаправляет данные в хранилища.
  • Бэкенд трассировок — визуализация и поиск (например, Jaeger).

Плюс OTel — единый формат: легко менять хранилище, не трогая код.

Базовый стенд: Collector + Jaeger через Docker Compose

Ниже — минимальный, но полезный стенд. Collector делает батчинг, «хвостовое» семплирование (по результату трассы) и маскирует чувствительные атрибуты. Jaeger — для просмотра трасс.

# docker-compose.yaml
version: "3.9"
services:
  jaeger:
    image: jaegertracing/all-in-one:1.49
    ports:
      - "16686:16686"   # веб-интерфейс
      - "4317:4317"     # OTLP gRPC (если отправлять напрямую)
      - "4318:4318"     # OTLP HTTP
    environment:
      - LOG_LEVEL=info

  otel-collector:
    image: otel/opentelemetry-collector:0.91.0
    command: ["--config=/etc/otelcol/config.yaml"]
    volumes:
      - ./otel-collector.yaml:/etc/otelcol/config.yaml:ro
    ports:
      - "4317:4317"   # OTLP gRPC приём от приложений
      - "55679:55679" # zpages (диагностика, опционально)
    depends_on:
      - jaeger

Файл конфигурации Collector:

# otel-collector.yaml
receivers:
  otlp:
    protocols:
      grpc: {}

processors:
  batch: {}
  # Хвостовое семплирование: оставляем ошибки и долгие трассы целиком
  tail_sampling:
    decision_wait: 2s
    num_traces: 10000
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow
        type: latency
        latency:
          threshold_ms: 1000
      - name: basic_sample
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
  # Маскирование чувствительных данных
  attributes/sanitize:
    actions:
      - key: http.request.header.authorization
        action: delete
      - key: user.email
        action: delete
      - key: db.statement
        action: update
        value: "SQL hidden" # чтобы не утащить PII в свободной форме

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

extensions:
  zpages: {}

service:
  extensions: [zpages]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/sanitize, tail_sampling, batch]
      exporters: [otlp/jaeger]

Запустить:

docker compose up -d
open http://localhost:16686

Быстрое внедрение в Node.js/Express: код и логи с trace_id

Установим зависимости:

npm i express pino pino-http pg
npm i -D @opentelemetry/sdk-node @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/semantic-conventions

Инициализация OTel (otel.js):

// otel.js
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'

const exporter = new OTLPTraceExporter({
  // Collector слушает на 4317 (gRPC)
  url: 'http://localhost:4317'
})

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'checkout-api',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.2.3',
    'deployment.environment': 'dev'
  }),
  traceExporter: exporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': {
        ignoreIncomingPaths: ['/health', '/metrics']
      },
      '@opentelemetry/instrumentation-pg': {
        enhancedDatabaseReporting: true
      }
    })
  ]
})

sdk.start()
  .then(() => console.log('OTel started'))
  .catch((err) => console.error('OTel error', err))

// При завершении приложения корректно завершим SDK
process.on('SIGTERM', async () => {
  await sdk.shutdown()
  process.exit(0)
})

Приложение (index.js) с привязкой trace_id к логам:

// index.js
import './otel.js' // важно подключить до остального кода
import express from 'express'
import pino from 'pino'
import pinoHttp from 'pino-http'
import { context, trace } from '@opentelemetry/api'
import pkg from 'pg'

const { Pool } = pkg
const app = express()
const logger = pino({ level: 'info' })

// Добавляем trace_id в каждый лог запроса
app.use(pinoHttp({
  logger,
  customProps: () => {
    const span = trace.getSpan(context.active())
    const traceId = span ? span.spanContext().traceId : undefined
    return traceId ? { trace_id: traceId } : {}
  }
}))

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres'
})

app.get('/health', (_req, res) => res.send('ok'))

app.get('/order/:id', async (req, res) => {
  const span = trace.getSpan(context.active())
  // Атрибуты конечного пользователя по стандарту OTel
  if (span) span.setAttribute('enduser.id', req.header('x-user-id') || 'anon')

  // Ручной дочерний спан для бизнес-логики
  const tracer = trace.getTracer('checkout-biz')
  await tracer.startActiveSpan('load-order', async (child) => {
    try {
      const { rows } = await pool.query('SELECT id, status, total FROM orders WHERE id = $1', [req.params.id])
      if (rows.length === 0) {
        child.setStatus({ code: 2, message: 'not found' }) // ERROR
        res.status(404).send({ error: 'order not found' })
        return
      }
      child.setAttribute('order.status', rows[0].status)
      res.send(rows[0])
    } catch (e) {
      child.recordException(e)
      child.setStatus({ code: 2, message: e.message })
      res.status(500).send({ error: 'db error' })
    } finally {
      child.end()
    }
  })
})

app.get('/ship/:id', async (_req, res) => {
  // Внешний вызов; http-инструментация добавит спан и заголовки трассировки автоматически
  const r = await fetch('https://httpbin.org/delay/1')
  res.json({ ok: r.ok })
})

app.listen(3000, () => logger.info('listening on 3000'))

Что получаем сразу:

  • Спаны на входящие HTTP-запросы и исходящие вызовы, SQL-запросы (pg), внутренние участки кода.
  • Автоматическую передачу контекста между обработчиками.
  • Логи с trace_id: легко открыть трассу из лог-строки (или наоборот — найти лог по trace_id).

Обогащение спанов: база, внешние вызовы, бизнес-метки

  • База данных. Включённый enhancedDatabaseReporting добавляет атрибуты: имя БД, схему, операцию (SELECT/INSERT), длительность. Текст запроса мы в Collector заменяем на «SQL hidden», чтобы не утащить чувствительные данные.
  • Внешние вызовы. Инструментация HTTP автоматически проставляет заголовок traceparent — ваш партнёр или другой ваш сервис увидит общую трассу (если тоже понимает W3C Trace Context).
  • Бизнес-метки. Добавляйте атрибуты вроде order.status, cart.items_count, payment.provider. Но избегайте высокой изменчивости (миллионы значений) — это бьёт по хранению и скорости поиска.

Связка логов и метрик с трассами без боли

  • Логи. Мы уже добавили trace_id в логи через pino-http. Настройте парсинг этого поля в вашей системе логов — и один клик будет вести в Jaeger (часто это делается ссылкой-шаблоном в интерфейсе).
  • Метрики. Если вы используете Prometheus/Grafana, можно хранить связь через «экземпляры» (exemplars) — точечные ссылки из гистограмм задержек на конкретные трассы. В Node это проще сделать через OTel-метрики и экспортер в Prometheus; на старте достаточно иметь гистограмму http_server_duration_seconds из автоинструментации OTel. Тогда из алерта на «медиана выросла» можно сразу открыть «плохие» трассы.

Семплирование: как удержать стоимость и не потерять пользу

Без семплинга счёт за хранение вырастет быстро. Правило:

  • На уровне SDK включаем «родительское» семплирование с базовой долей (например, 10%). Это даёт непрерывную выборку и целостность трассы при микросервисной связке.
  • На уровне Collector — хвостовое семплирование: сохраняем все трассы с ошибками и долгие; остальное — по доле.

Это уже реализовано в otel-collector.yaml: ошибки и долгие — 100%, остальное — 10%.

Сигналы для подстройки:

  • Если время расследования всё ещё велико — увеличиваем долю или добавляем политику на конкретный маршрут (например, /checkout).
  • Если хранилище растёт слишком быстро — уменьшаем долю для «зелёных» трасс, но не для ошибок.

Защита данных: маскирование, политика хранения, доступ

  • Маскирование. В Collector удаляем или переписываем поля: токены, почты, номера карт, свободный текст SQL. Лучше маскировать ближе к источнику.
  • Хранение. Разделяйте retention: ошибки и инциденты — дольше (например, 30–90 дней), остальное — короче (7–14 дней).
  • Доступ. Не всем нужны полные трассы. Ограничьте поиск по атрибутам пользователя и загрузку «сырого» содержимого спанов.

Алерты и SLO на основе трассировок

  • SLI «Latency»: процент запросов быстрее X мс. Источник — гистограммы HTTP из OTel-метрик или агрегация длительностей корневых спанов.
  • SLI «Error rate»: доля трасс со статусом ERROR. Это легко считать по спанам сервиса-шлюза.
  • Диагностика алерта: из карточки алерта — ссылка на соответствующие трассы (через exemplars или по времени/меткам).

Это сокращает MTTR: вместо долгого grep по логам — сразу «разложенный» путь запроса с виновником.

Частые ошибки и как их избежать

  • Двойная отправка. Не включайте экспорт напрямую в Jaeger и одновременно через Collector из того же сервиса — будут дубликаты.
  • Несогласованное семплирование. Если один сервис отбрасывает почти все запросы, трасса «рвётся». Используйте родительское семплирование и общие настройки.
  • Высокая изменчивость атрибутов. Уникальные id в атрибутах (не в span.name) портят индексы. id держите в событиях или логах, а атрибуты — агрегируемые.
  • Отсутствие связи логов и трасс. Без trace_id в логах теряется главная выгода — корреляция. Добавьте один раз мидлвару — сэкономите часы.
  • PII в sql/сообщениях. Маскируйте в Collector или в коде до отправки.
  • Смешение форматов контекста. Используйте W3C Trace Context (заголовок traceparent) повсюду; B3 оставьте для старых систем через трансляцию в Collector.

Чек-лист внедрения и оценка эффекта

  1. Цель: «сократить MTTR на 30%» или «найти 3 главные узкие места в чекауте». Без чёткой цели телеметрия превращается в «красивые графики».
  2. Развёрнуть Jaeger + Collector (compose из статьи).
  3. Инструментировать 1–2 ключевых сервиса и шлюз; включить автоинструментацию HTTP/БД и связать логи с trace_id.
  4. Добавить 2–3 бизнес-атрибута (например, order.status, payment.provider).
  5. Настроить семплирование: ошибки и долгие — всегда, остальное — 5–20%.
  6. Добавить дашборд: 95-й перцентиль по входящим запросам, доля ошибок, топ самых «тяжёлых» маршрутов.
  7. Завести алерты по SLO.
  8. Каждую неделю — разбор 1–2 «тяжёлых» трасс, фиксы и замеры выигрыша.

Как мерить эффект:

  • MTTR: время от алерта до «понимания виновника» и до фикса — должно сократиться.
  • Количество «плавающих» инцидентов – вниз.
  • Производительность ключевых маршрутов — вверх: видны самые дорогие спаны (например, медленный SQL или внешний API), даёт быстрые победы.
  • Стоимость — под контролем: семплирование и retention.

Итог. OpenTelemetry — быстрый способ навести порядок в диагностике без привязки к одному вендору. Начните с автоинструментации и Collector с хвостовым семплированием, добавьте пару бизнес-меток и свяжите логи с trace_id. Уже это даёт измеримую пользу для SLA и затрат на поддержку.


наблюдаемостьopentelemetryтрассировка