
Когда продукт разрастается, «где тормозит?» превращается в игру «угадай микросервис». Логи расползаются по разным хранилищам, метрики показывают только симптомы, а время расследования инцидента растёт. Распределённая трассировка отвечает на конкретные вопросы:
Результат — меньше «ручных» расследований, быстрее фиксы, меньше инцидентов-«повторов». И главное — измеримые метрики бизнеса: скорость заказов, доля успешных платежей, время ответа критичных операций.
OpenTelemetry (OTel) — открытый стандарт и набор библиотек для телеметрии (трассы, метрики, логи). Он не привязывает к конкретному вендору: можно отправлять данные в Jaeger, Tempo, ClickHouse-стек, Grafana Cloud, Honeycomb, любой совместимый бэкенд.
Ключевые элементы:
Плюс OTel — единый формат: легко менять хранилище, не трогая код.
Ниже — минимальный, но полезный стенд. 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
Установим зависимости:
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'))
Что получаем сразу:
enhancedDatabaseReporting добавляет атрибуты: имя БД, схему, операцию (SELECT/INSERT), длительность. Текст запроса мы в Collector заменяем на «SQL hidden», чтобы не утащить чувствительные данные.traceparent — ваш партнёр или другой ваш сервис увидит общую трассу (если тоже понимает W3C Trace Context).order.status, cart.items_count, payment.provider. Но избегайте высокой изменчивости (миллионы значений) — это бьёт по хранению и скорости поиска.trace_id в логи через pino-http. Настройте парсинг этого поля в вашей системе логов — и один клик будет вести в Jaeger (часто это делается ссылкой-шаблоном в интерфейсе).http_server_duration_seconds из автоинструментации OTel. Тогда из алерта на «медиана выросла» можно сразу открыть «плохие» трассы.Без семплинга счёт за хранение вырастет быстро. Правило:
Это уже реализовано в otel-collector.yaml: ошибки и долгие — 100%, остальное — 10%.
Сигналы для подстройки:
/checkout).Это сокращает MTTR: вместо долгого grep по логам — сразу «разложенный» путь запроса с виновником.
order.status, payment.provider).Как мерить эффект:
Итог. OpenTelemetry — быстрый способ навести порядок в диагностике без привязки к одному вендору. Начните с автоинструментации и Collector с хвостовым семплированием, добавьте пару бизнес-меток и свяжите логи с trace_id. Уже это даёт измеримую пользу для SLA и затрат на поддержку.