Modern QA2026Распределённая трассировка с OpenTelemetry
Join

Course06 Observability-Driven Testing

Cutting-edge · Chapter 06

Распределённая трассировка с OpenTelemetry

Updated Jul 2026

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

В микросервисной архитектуре один пользовательский запрос может пройти через 5, 10 или 20 сервисов. Когда этот запрос медленный или завершается с ошибкой, вам нужен ответ: какой сервис является узким местом? Распределённая трассировка даёт ответ, отслеживая путь каждого запроса через границы сервисов, записывая время, статус и метаданные на каждом переходе.

Архитектура OpenTelemetry

OpenTelemetry (OTel) — это стандарт CNCF для сбора телеметрических данных. Он предоставляет вендор-нейтральные API, SDK и коллектор для трассировок, метрик и логов.

  [Service A]        [Service B]         [Service C]
  OTel SDK           OTel SDK            OTel SDK
      |                  |                    |
      v                  v                    v
  +-------------------------------------------------------+
  |              OpenTelemetry Collector                  |
  |  (receives, processes, exports telemetry data)        |
  +-------+-----------+-----------+-----------+-----------+
          |           |           |           |
          v           v           v           v
       [Jaeger]   [Grafana    [Datadog]   [Cloud
                   Tempo]                  provider]

Почему именно OpenTelemetry?

  • Вендор-нейтральный. Меняйте бэкенды (Jaeger, Datadog, Grafana Tempo) без изменения кода приложения
  • Стандарт CNCF. Поддерживается Google, Microsoft, Splunk и большинством вендоров наблюдаемости
  • Автоинструментирование. Инструментируйте HTTP, библиотеки баз данных и обмена сообщениями без изменения кода
  • Распространение контекста. Автоматически передаёт контекст трассировки через границы сервисов с помощью HTTP-заголовков

Как работают трассировки, спаны и контекст

User clicks "Submit Order"
    |
    v
[TRACE: order-submit-abc123] -------- spans the entire request flow
    |
    +---> [Service: API Gateway]
    |         LOG: "Received POST /orders from user_id=42"
    |         METRIC: http_requests_total{method="POST", path="/orders"} +1
    |
    +---> [Service: Order Service]
    |         LOG: "Creating order for user_id=42, items=3, total=$149.99"
    |         METRIC: order_creation_duration_seconds = 0.045
    |         SPAN: order-service.create_order (45ms)
    |
    +---> [Service: Payment Service]
    |         LOG: "Charging $149.99 to card ending 4242"
    |         METRIC: payment_processing_duration_seconds = 1.2
    |         SPAN: payment-service.charge (1200ms)  <-- slow!
    |
    +---> [Service: Inventory Service]
              LOG: "Reserved 3 items for order_id=ORD-789"
              METRIC: inventory_reservations_total +3
              SPAN: inventory-service.reserve (23ms)

Трассировка показывает, что сервис платежей является узким местом (1200 мс из ~1300 мс общего времени). Метрика подтверждает, что это тренд. Лог предоставляет конкретный контекст для отладки.

Инструментирование Python-сервиса

Настройка автоматического инструментирования

# otel_setup.py -- OpenTelemetry configuration for a Flask service
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

def setup_telemetry(service_name: str, service_version: str):
    """Initialize OpenTelemetry with OTLP export."""
    resource = Resource.create({
        "service.name": service_name,
        "service.version": service_version,
        "deployment.environment": "production",
    })

    # Traces
    trace_provider = TracerProvider(resource=resource)
    trace_provider.add_span_processor(
        BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
    )
    trace.set_tracer_provider(trace_provider)

    # Metrics
    metric_reader = PeriodicExportingMetricReader(
        OTLPMetricExporter(endpoint="http://otel-collector:4317"),
        export_interval_millis=10000,
    )
    metrics.set_meter_provider(MeterProvider(
        resource=resource, metric_readers=[metric_reader]
    ))

    # Auto-instrument frameworks
    FlaskInstrumentor().instrument()
    RequestsInstrumentor().instrument()
    SQLAlchemyInstrumentor().instrument()

Ручные спаны для бизнес-логики

# app.py -- Adding manual spans for business-critical operations
from opentelemetry import trace

tracer = trace.get_tracer("order-service")

def process_order(order_data):
    with tracer.start_as_current_span("process_order") as span:
        span.set_attribute("order.id", order_data["id"])
        span.set_attribute("order.item_count", len(order_data["items"]))
        span.set_attribute("order.total_cents", order_data["total"])

        with tracer.start_as_current_span("validate_order"):
            validate(order_data)

        with tracer.start_as_current_span("charge_payment") as payment_span:
            result = charge(order_data["payment"])
            payment_span.set_attribute("payment.method", result.method)
            payment_span.set_attribute("payment.processor", result.processor)

        with tracer.start_as_current_span("reserve_inventory"):
            reserve(order_data["items"])

        span.set_attribute("order.status", "completed")

Тестирование на основе трассировок

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

# test_trace_assertions.py
import requests
import time

def test_order_flow_creates_expected_trace(trace_client):
    """Verify the order flow hits all expected services in the correct order."""
    # Trigger the order flow
    response = requests.post("https://api.example.com/orders", json={
        "items": [{"sku": "LAPTOP-1", "qty": 1}],
        "payment": {"method": "card", "token": "tok_test_123"},
    })
    assert response.status_code == 201
    trace_id = response.headers["X-Trace-Id"]

    # Allow time for spans to propagate to the backend
    time.sleep(5)
    trace_data = trace_client.fetch_trace(trace_id)

    # Assert all expected services are present
    service_names = [span["service_name"] for span in trace_data["spans"]]
    assert "api-gateway" in service_names
    assert "order-service" in service_names
    assert "payment-service" in service_names
    assert "inventory-service" in service_names

    # Assert ordering: payment happens before inventory reservation
    payment_span = next(s for s in trace_data["spans"]
                       if s["operation"] == "charge_payment")
    inventory_span = next(s for s in trace_data["spans"]
                         if s["operation"] == "reserve_inventory")
    assert payment_span["end_time"] <= inventory_span["start_time"]

    # Assert performance: total trace duration under 3 seconds
    root_span = next(s for s in trace_data["spans"] if s["parent_id"] is None)
    assert root_span["duration_ms"] < 3000

    # Assert no error spans
    error_spans = [s for s in trace_data["spans"] if s.get("status") == "ERROR"]
    assert len(error_spans) == 0, f"Unexpected errors in trace: {error_spans}"

Что обнаруживают тесты на основе трассировок

Утверждение Что обнаруживает
Сервис присутствует в трассировке Пропущенный вызов сервиса (регрессия интеграции)
Порядок спанов Состояния гонки, неправильная оркестрация
Общая длительность трассировки Сквозная регрессия производительности
Отсутствие ошибочных спанов Проглоченные ошибки, тихие сбои
Ожидаемые атрибуты на спанах Нарушение распространения контекста
Количество спанов Неожиданные вызовы сервисов (N+1 запросы, лишние повторы)

Конфигурация OpenTelemetry Collector

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

processors:
  batch:
    timeout: 5s
    send_batch_size: 1000

  # Tail-based sampling: keep all error traces, sample 10% of successful traces
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors-always
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 2000 }
      - name: probabilistic-sample
        type: probabilistic
        probabilistic: { sampling_percentage: 10 }

exporters:
  otlp/tempo:
    endpoint: tempo.monitoring:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, tail_sampling]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

Стратегии сэмплирования

В высоконагруженных системах сбор каждой трассировки обходится дорого. Выберите стратегию сэмплирования:

Стратегия Описание Преимущества Недостатки
Head-based (вероятностный) Решение в начале трассировки — сэмплировать или нет Простой, предсказуемая стоимость Может пропустить редкие ошибки
Tail-based Решение после завершения трассировки, на основе результата Сохраняет все ошибки и медленные трассировки Более высокое потребление памяти в коллекторе
С ограничением частоты Сэмплирование N трассировок в секунду Предсказуемая стоимость Пропускает всплески
Всегда включён для ошибок 100% сэмплирование ошибочных трассировок Никогда не пропускает сбои Не снижает объём ошибочных трассировок

Рекомендация: Используйте tail-based сэмплирование с постоянным сбором ошибок и медленных трассировок. Это даёт 100% видимость проблем при контролируемых затратах на успешные быстрые запросы.

Распределённая трассировка — это основа наблюдаемости в микросервисах. В сочетании со структурированным логированием и метриками она обеспечивает полную картину, необходимую для эффективного тестирования в продакшене.