Kravchenko

Web Lab

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

Kravchenko

Web Lab

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

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

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

•

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

•

ОГРНИП: 324784700339743

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

OpenTelemetry и распределённая трассировка: как сократить время поиска проблем в 5–10 раз и удержать SLA без лишних серверов

Разработка и технологии30 декабря 2025 г.
Показываю, как запустить трассировку запросов от входа в API до базы и внешних сервисов, не превращая проект в зоопарк. Разбираем быстрый старт на Jaeger, корреляцию логов и трейсов, экономное семплирование и чек‑лист внедрения, чтобы реально снизить MTTR и расходы на поддержку.
OpenTelemetry и распределённая трассировка: как сократить время поиска проблем в 5–10 раз и удержать SLA без лишних серверов

Оглавление

  • Зачем бизнесу трассировка
  • Что такое OpenTelemetry простыми словами
  • Быстрый старт за 15 минут: Jaeger + Collector
  • Инструментирование бэкенда на Python (FastAPI)
    • Сервис_checkout: создаём заказ и зовём оплату
    • Сервис_billing: проводим платеж
  • Корреляция с логами и метриками
  • Семплирование и контроль затрат
    • Head-based семплирование в SDK
    • Tail-based семплирование в Collector
  • Типичные грабли и как их избежать
  • Безопасность и персональные данные
  • Как “продать” внедрение бизнесу
  • Чеклист внедрения на 2–4 недели
  • Что дальше

Зачем бизнесу трассировка

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

  • Время поиска причины (MTTR) сокращается в 5–10 раз. Это меньше штрафов за SLA и меньше переработок.
  • Падает число «пустых» горизонтальных масштабирований — мы лечим узкое место, а не заливаем всё железом.
  • Легче доказывать пользователям и партнёрам, что проблема на их стороне — видны задержки и ошибки их API.

Что такое OpenTelemetry простыми словами

OpenTelemetry — это открытый стандарт и набор библиотек для сбора технических сигналов: трассы (цепочки вызовов), метрики и логи. Важные термины:

  • Трейс — путь одного запроса через сервисы.
  • Спан — участок работы внутри трейса (например, обработка запроса, SQL‑запрос, вызов внешнего API). У спана есть длительность, статус (ошибка/успех) и произвольные атрибуты: order_id, user_id, имя SQL‑таблицы и т. п.
  • Пропагация контекста — передача идентификаторов трейса между сервисами через заголовки HTTP или сообщения в очереди, чтобы цепочка не «рвалась».

OpenTelemetry не навязывает конкретное хранилище. Сегодня вы можете отправлять данные в Jaeger, Tempo, Zipkin, в коммерческие APM — меняется только экспортёр/коллектор.

Быстрый старт за 15 минут: Jaeger + Collector

Так вы поднимете визуализацию и разумное семплирование без изменения кода приложений.

# docker-compose.yaml
version: "3.8"
services:
  jaeger:
    image: jaegertracing/all-in-one:1.54
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    ports:
      - "16686:16686"   # Веб‑интерфейс Jaeger
      - "4317:4317"     # OTLP gRPC (если нужно)
      - "4318:4318"     # OTLP HTTP

  otel-collector:
    image: otel/opentelemetry-collector:0.98.0
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro
    ports:
      - "4319:4318"  # OTLP HTTP вход для приложений
      - "4320:4317"  # OTLP gRPC вход (опционально)
    depends_on:
      - jaeger

Конфиг коллектора: tail‑семплирование (сохраняем все ошибки и медленные трейсы, остальные — выборочно), отправляем в Jaeger через OTLP.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch: {}
  tail_sampling:
    decision_wait: 10s
    num_traces: 10000
    expected_new_traces_per_sec: 500
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow_traces
        type: latency
        latency:
          threshold_ms: 300
      - name: important_orders
        type: string_attribute
        string_attribute:
          key: business.important
          values: ["true"]
      - name: probabilistic_rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

exporters:
  otlphttp/jaeger:
    endpoint: http://jaeger:4318
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling, batch]
      exporters: [otlphttp/jaeger]

Дальше приложения шлют данные на OTLP HTTP http://localhost:4319 — коллектор решает, что хранить, и отрисовывает в Jaeger (http://localhost:16686).

Инструментирование бэкенда на Python (FastAPI)

Покажу два небольших сервиса, чтобы видно было межсервисные вызовы и передачу контекста.

  • checkout (порт 8000): принимает заказ и зовёт оплату;
  • billing (порт 8001): проводит платеж, иногда «косячит» для наглядности.

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

python -m venv .venv && source .venv/bin/activate
pip install fastapi uvicorn httpx
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
pip install opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-httpx opentelemetry-instrumentation-logging

Сервис_checkout: создаём заказ и зовём оплату

# app_checkout.py
import os
import random
import time
import logging
from typing import Optional

import httpx
from fastapi import FastAPI, HTTPException

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

# Настройка трейсера
OTLP_ENDPOINT = os.getenv("OTLP_HTTP_ENDPOINT", "http://localhost:4319/v1/traces")
resource = Resource.create({"service.name": "checkout", "service.version": "1.0.0"})
provider = TracerProvider(resource=resource, sampler=ParentBased(TraceIdRatioBased(0.3)))
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=OTLP_ENDPOINT))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

# Логирование с корелляцией по trace_id/span_id
LoggingInstrumentor().instrument(set_logging_format=True)
logger = logging.getLogger("checkout")
logger.setLevel(logging.INFO)

app = FastAPI(title="checkout")
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor().instrument()

BILLING_URL = os.getenv("BILLING_URL", "http://localhost:8001/pay")

@app.get("/healthz")
def healthz():
    return {"status": "ok"}

@app.post("/checkout")
async def checkout(order_id: str, user_id: str, amount: float, important: Optional[bool] = False):
    # Кладём бизнес‑атрибуты в текущий спан
    current_span = trace.get_current_span()
    current_span.set_attribute("order.id", order_id)
    current_span.set_attribute("user.id", user_id)
    current_span.set_attribute("amount", amount)
    current_span.set_attribute("business.important", str(important).lower())

    # Симулируем работу в приложении
    with tracer.start_as_current_span("validate_order") as span:
        span.set_attribute("rules.count", 3)
        time.sleep(random.uniform(0.01, 0.05))

    # Вызов внешнего сервиса оплаты (контекст передастся через заголовки автоматически)
    async with httpx.AsyncClient(timeout=3.0) as client:
        try:
            resp = await client.post(BILLING_URL, json={
                "order_id": order_id,
                "user_id": user_id,
                "amount": amount,
                "important": important
            })
            resp.raise_for_status()
            data = resp.json()
        except httpx.HTTPError as e:
            logger.exception("billing call failed")
            # Помечаем текущий спан как ошибку
            current_span.record_exception(e)
            current_span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
            raise HTTPException(status_code=502, detail="billing unavailable")

    return {"order_id": order_id, "payment": data}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Сервис_billing: проводим платеж

# app_billing.py
import os
import random
import time
import logging
from fastapi import FastAPI, HTTPException

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

OTLP_ENDPOINT = os.getenv("OTLP_HTTP_ENDPOINT", "http://localhost:4319/v1/traces")
resource = Resource.create({"service.name": "billing", "service.version": "1.0.0"})
provider = TracerProvider(resource=resource, sampler=ParentBased(TraceIdRatioBased(1.0)))
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=OTLP_ENDPOINT))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

LoggingInstrumentor().instrument(set_logging_format=True)
logger = logging.getLogger("billing")
logger.setLevel(logging.INFO)

app = FastAPI(title="billing")
FastAPIInstrumentor.instrument_app(app)

@app.get("/healthz")
def healthz():
    return {"status": "ok"}

@app.post("/pay")
def pay(payload: dict):
    order_id = payload.get("order_id")
    amount = float(payload.get("amount", 0))
    important = bool(payload.get("important", False))

    span = trace.get_current_span()
    span.set_attribute("order.id", order_id)
    span.set_attribute("amount", amount)

    # Имитация внешнего провайдера с задержками и ошибками
    with tracer.start_as_current_span("acquirer") as s:
        delay = random.uniform(0.02, 0.6 if important else 0.25)
        time.sleep(delay)
        s.set_attribute("simulated.delay_ms", int(delay * 1000))
        # 5% ошибок обычным, 1% — важным заказам
        failure_rate = 0.05 if not important else 0.01
        if random.random() < failure_rate:
            err = RuntimeError("acquirer timeout")
            s.record_exception(err)
            s.set_status(trace.Status(trace.StatusCode.ERROR, str(err)))
            logger.error("payment failed for order %s", order_id)
            raise HTTPException(status_code=504, detail="acquirer timeout")

    logger.info("payment ok for order %s", order_id)
    return {"status": "ok", "order_id": order_id, "amount": amount}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8001)

Запуск:

# 1) Поднимаем Jaeger и Collector
docker compose up -d

# 2) Запускаем сервисы (в разных терминалах)
export OTLP_HTTP_ENDPOINT=http://localhost:4319/v1/traces
python app_billing.py
python app_checkout.py

# 3) Делаем несколько запросов (часть будет медленной/с ошибками)
curl -X POST "http://localhost:8000/checkout?order_id=42&user_id=7&amount=199.9&important=true"
curl -X POST "http://localhost:8000/checkout?order_id=43&user_id=1&amount=49.9"

# 4) Смотрим трейсы
open http://localhost:16686

В Jaeger увидите единый трейс: входящий запрос в checkout → внутренние шаги → HTTP‑вызов в billing → спан «acquirer». Ошибки и медленные трейсы гарантированно сохраняются коллектором.

Корреляция с логами и метриками

  • Логи. Мы включили опцию LoggingInstrumentor: в формат логов добавятся trace_id и span_id. Теперь в вашей системе логирования (ELK, Loki, Cloud Logging) можно кликнуть из лога в соответствующий трейс и наоборот.
  • Метрики. OpenTelemetry умеет собирать метрики (p95 задержки, количество ошибок) и связывать их с трейсами. Если вы уже используете Prometheus, не обязательно всё переносить — начните с трассировки, потом добавьте метрики через экспортер.

Практический совет: дублируйте в атрибутах спанов бизнес‑ключи — например, order.id, partner.id, план тарифа. Это позволит быстро фильтровать проблемные потоки и спорить с партнёрами на цифрах.

Семплирование и контроль затрат

Трейсы «весят» заметно больше метрик, и писать каждый — дорого. Баланс простой: сохраняем всё проблемное, остальное — выборочно.

Head-based семплирование в SDK

Мы настроили ParentBased + TraceIdRatioBased(0.3) в checkout — это означает, что корневой запрос попадёт в хранилище с вероятностью 30%, и все его дочерние спаны тоже сохранятся. Плюс, если downstream‑сервис (billing) получил контекст, он уважит решение родителя и не «потеряет» трейс.

Плюсы: просто и дёшево, не нужен отдельный софт. Минусы: можно промахнуться мимо редкой ошибки или медленного случая — решение о сохранении принимается в момент начала трейса.

Tail-based семплирование в Collector

Коллектор ждёт завершения трейса и решает, хранить его или нет, уже зная исход: был ли статус ERROR, превысил ли задержку порог. Конфиг выше делает именно это:

  • Сохраняем все трейсы с ошибкой;
  • Сохраняем медленные (>300 мс);
  • Сохраняем важные (атрибут business.important=true);
  • Остальные — 10% случайно.

Итог: затраты под контролем, качество данных для расследований высокое.

Типичные грабли и как их избежать

  • Нет пропагации контекста во внешние вызовы. Включайте инструментаторы HTTP‑клиента (httpx, requests), gRPC и драйверов очередей. Иначе трейс «рвётся» на границе сервиса.
  • Спаны короткие и бесполезные. Добавляйте атрибуты: бизнес‑идентификаторы, тип операции, имя таблицы/индекса для SQL, код партнёра. Но не храните чувствительные данные (см. ниже).
  • Слишком подробная выборка — инфраструктура «трескается». Сначала tail‑семплирование ошибок и медленных, затем — выборочное head‑семплирование.
  • Коллектор недоступен — растут задержки. Используйте BatchSpanProcessor (он по умолчанию), отдельный буфер и таймауты. В критичных сервисах — неблокирующий экспорт и резервный эндпойнт.
  • Миксуются версии заголовков. Оставьте один стандарт — W3C Trace Context (по умолчанию в OpenTelemetry) и убедитесь, что прокси/шлюз не «чистят» заголовки traceparent и tracestate.

Безопасность и персональные данные

  • Не пишите в атрибуты номера карт, телефоны, e‑mail, адреса. Вместо этого — внутренние идентификаторы. Если нужно показать кусочек — маскируйте.
  • Введите «белый список» ключей атрибутов, которые разрешено отправлять. Остальные — вырезайте на уровне SDK или коллектора (процессор attributes/regexp).
  • Для окружений с данными клиентов включите шифрование транспорта (HTTPS к коллектору) и контроль доступа к UI (SAML/OAuth, VPN или приватная сеть).
  • Разделяйте среды: dev/stage/prod — отдельные проекты/неймспейсы, чтобы тестовые данные не попадали в прод и наоборот.

Как “продать” внедрение бизнесу

  • Деньги. Сокращение MTTR на 60–80% даёт прямую экономию рабочего времени инженерной поддержки и меньше откатов. Плюс, уменьшается потребность «масштабировать всё подряд».
  • SLA и брендинг. Быстрые расследования — меньше инцидентов «дольше 1 часа», меньше пост‑мортемов. Репутация команды растёт.
  • Прозрачность для партнёров. Видно, где тормозит интеграция — ваши графики с трейсами заканчивают споры быстрее любых слов.

Простой расчёт: если у вас 3 инцидента в месяц на 2 часа каждый с вовлечением 3 инженеров по 2 500 ₽/час — это ~45 000 ₽/мес прямых затрат. Снижение вдвое окупает внедрение за 1–2 месяца даже при платном хранилище.

Чеклист внедрения на 2–4 недели

  • Неделя 1: поднять Jaeger + Collector, включить автодетект фреймворков (FastAPI/Django/Spring/Express) на 1–2 сервисах.
  • Неделя 2: добавить атрибуты бизнес‑контекста, включить инструментаторы HTTP‑клиента, баз данных и очередей. Настроить tail‑семплирование ошибок/медленных.
  • Неделя 3: связать логирование с trace_id, добавить дешборды «самые медленные операции», «ошибки по партнёрам». Обучить команду разбирать трейсы.
  • Неделя 4: расширить покрытие на критичные сервисы, вынести коллектор и хранилище в инфраструктуру, прописать регламенты разборов инцидентов через трейсы.

Что дальше

  • Фронтенд: подключить браузерную часть (RUM) и видеть путь пользователя от клика до ответа бэкенда.
  • SQL: добавить атрибуты имени запроса и размера результата, включить нормализацию, чтобы не светить параметры.
  • Очереди/шины: провести контекст через сообщения (Kafka/RabbitMQ) — OpenTelemetry это умеет.
  • Переезд в устойчивое хранилище: Grafana Tempo, ClickHouse‑бэкенд или ваш APM‑провайдер.

Итог: OpenTelemetry даёт наблюдаемость уровня «рентген» без привязки к одному вендору. Начните с малого — один сервис, одна болевая точка — и уже через неделю инциденты станут короче и прогнозируемее.


observabilityopentelemetrytracing