Skip to main content
Back to course
Распределенная трассировка: от основ до production
7 / 1839%

Context Propagation и W3C Trace Context

70 минут

Context Propagation и W3C Trace Context

Проблема: Broken Trace

Представьте: вы добавили трассировку в API Gateway и User Service. Запускаете запрос, открываете Jaeger — и видите два разных trace:

Trace 1: API Gateway [200ms]
├─ POST /api/checkout [200ms]
└─ HTTP GET http://user-service/users/123 [150ms]
 
Trace 2: User Service [150ms] ← отдельный trace!
└─ GET /users/123 [145ms]

Почему? Trace context не передался между сервисами.

Этот урок объясняет:

  • Как работает W3C Trace Context стандарт
  • Какие HTTP headers передают context
  • Что делать, если автоинструментация не работает
  • Manual propagation для очередей (RabbitMQ, Kafka) и gRPC

Что такое Trace Context?

Trace Context — это метаданные, которые связывают spans из разных сервисов в единый trace:

┌────────────────┐
│ Trace Context  │
├────────────────┤
│ trace_id       │ ← Уникальный ID всего trace
│ span_id        │ ← ID текущего span
│ parent_span_id │ ← ID родительского span
│ trace_flags    │ ← Флаги (sampled, debug)
└────────────────┘

Пример:

Service A создаёт trace:
  trace_id: a1b2c3d4e5f6
  span_id: 1111
  parent_span_id: null (root span)
 
Service A вызывает Service B и передаёт:
  trace_id: a1b2c3d4e5f6 ← тот же trace_id
  span_id: 2222 ← новый span_id
  parent_span_id: 1111 ← указывает на span в Service A

Благодаря этому Jaeger знает, что span 2222 — дочерний для span 1111.


W3C Trace Context Стандарт

W3C Trace Context — это стандарт для передачи trace context через HTTP headers.

Два обязательных headers:

1. traceparent — основные данные

traceparent: 00-<trace-id>-<parent-id>-<trace-flags>

Формат:

  • 00 — версия стандарта (всегда 00 на данный момент)
  • <trace-id> — 32 hex символа (16 байт)
  • <parent-id> — 16 hex символов (8 байт)
  • <trace-flags> — 2 hex символа (флаги sampled/debug)

Пример:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
                 ^                                ^                ^
                 trace-id                         parent-id        sampled

2. tracestate — vendor-specific данные (опционально)

tracestate: vendorname1=value1,vendorname2=value2

Используется для передачи дополнительных данных между сервисами (например, sampling rate, tenant ID).


W3C Baggage: передача бизнес-контекста

Baggage — это W3C стандарт для передачи произвольных key-value пар через всю цепочку сервисов.

В чем разница: Baggage vs Span Attributes

АспектSpan AttributesBaggage
ScopeТолько текущий spanВесь trace (все сервисы)
Propagation❌ Не передается✅ Автоматически везде
Use caseТехнические детали spanБизнес-контекст
Size limitПрактически без лимита~8KB total (осторожно!)
PerformanceМинимальный overheadOverhead на каждый hop

Формат Baggage Header

baggage: key1=value1,key2=value2,key3=value3;metadata

Пример:

baggage: userId=42,tenantId=acme,featureFlag=newUI,experimentId=exp123

Use Case 1: Feature Flags

Проблема: Нужно передать feature flag через всю цепочку микросервисов.

Node.js implementation:

const { propagation, context, trace } = require("@opentelemetry/api");
 
app.get("/api/profile", async (req, res) => {
  // ⭐ Set baggage
  const bag = propagation.getBaggage(context.active()) || {};
  const newBaggage = propagation.createBaggage({
    ...bag,
    "feature.newUI": { value: "true" },
    "experiment.id": { value: "exp-123" },
  });
 
  const ctx = propagation.setBaggage(context.active(), newBaggage);
 
  // ⭐ Baggage автоматически передается в outgoing requests!
  await context.with(ctx, async () => {
    const response = await fetch("http://user-service/users/42");
    // baggage header добавлен автоматически!
    return response.json();
  });
 
  res.json({ message: "OK" });
});

В User Service (Python):

from opentelemetry import propagation, context
 
@app.route('/users/<user_id>')
def get_user(user_id):
    # ⭐ Read baggage
    baggage = propagation.get_baggage("feature.newUI")
 
    if baggage == "true":
        # Use NEW UI logic
        return render_new_ui(user_id)
    else:
        # Use OLD UI logic
        return render_old_ui(user_id)

Use Case 2: Multi-Tenant Context

Проблема: SaaS приложение с множеством клиентов (tenants). Нужно передать tenant_id через все сервисы для:

  • Фильтрации данных
  • Cost attribution
  • Compliance (GDPR isolation)
// В API Gateway - устанавливаем tenant_id из JWT token
const tenantId = extractTenantFromJWT(req.headers.authorization);
 
const bag = propagation.createBaggage({
  "tenant.id": { value: tenantId },
  "tenant.tier": { value: "enterprise" }, // free/pro/enterprise
});
 
const ctx = propagation.setBaggage(context.active(), bag);
 
await context.with(ctx, async () => {
  // Все downstream сервисы получат tenant.id!
  await processRequest(req);
});

В Database Service:

const tenantId = propagation
  .getBaggage(context.active())
  ?.getEntry("tenant.id")?.value;
 
// Автоматическая фильтрация по tenant!
const users = await db.query("SELECT * FROM users WHERE tenant_id = $1", [
  tenantId,
]);

В Logging:

const tenantId = propagation
  .getBaggage(context.active())
  ?.getEntry("tenant.id")?.value;
 
logger.info("Processing request", {
  tenantId, // Автоматически в каждом логе!
  userId,
  operation: "getUserProfile",
});

Use Case 3: User Context (userId, sessionId)

// Set в Authentication Service
const bag = propagation.createBaggage({
  "user.id": { value: user.id.toString() },
  "user.email": { value: user.email },
  "session.id": { value: req.sessionID },
});
 
// Read в ANY downstream service
const userId = propagation
  .getBaggage(context.active())
  ?.getEntry("user.id")?.value;
 
span.setAttribute("user.id", userId); // Добавляем в span тоже!

Best Practices для Baggage

✅ Используйте Baggage для:

  1. Feature flags / A/B experiments

    "feature.experimentId": "exp-123"
    "feature.variant": "B"
  2. Tenant/Organization ID

    "tenant.id": "acme-corp"
    "tenant.tier": "enterprise"
  3. User segments

    "user.segment": "premium"
    "user.region": "eu-west"
  4. Request correlation metadata

    "request.sourceApp": "mobile-ios"
    "request.apiVersion": "v2"

❌ НЕ используйте Baggage для:

  1. Large payloads - limit ~8KB total!

    // ❌ BAD
    "user.profile": JSON.stringify(hugeObject) // Too big!
  2. Sensitive data (PII)

    // ❌ BAD - Baggage передается в КАЖДОМ header!
    "user.creditCard": "4111-1111-1111-1111" // Security risk!
    "user.password": "..." // Never!
  3. Frequently changing values

    // ❌ BAD - overhead на каждый request
    "timestamp": Date.now().toString() // Changes every time
  4. Data that не нужна downstream

    // ❌ BAD - only needed in current service
    "internal.cacheKey": "..." // Use span attribute instead

Security Warning: Baggage Overhead

⚠️ Baggage передается в КАЖДОМ HTTP request через header!

Impact:

Best Practice: Держите Baggage < 1KB total

// ✅ GOOD - small keys, small values
const bag = propagation.createBaggage({
  "tenant.id": { value: "acme" }, // 4 + 4 = 8 bytes
  "user.id": { value: "42" }, // 4 + 2 = 6 bytes
  "feature.flag": { value: "1" }, // 8 + 1 = 9 bytes
});
// Total: ~23 bytes ✅
 
// ❌ BAD - too many keys or large values
const bag = propagation.createBaggage({
  "user.fullProfile": { value: JSON.stringify(user) }, // 5KB!
  "analytics.data": { value: "..." }, // 2KB!
  // ... 10 more keys
});
// Total: 8KB+ ❌ Too much!

Monitoring Baggage Size

// Полезная функция для monitoring
function logBaggageSize() {
  const bag = propagation.getBaggage(context.active());
  const entries = bag ? Array.from(bag.getAllEntries()) : [];
 
  const totalSize = entries.reduce((sum, [key, entry]) => {
    return sum + key.length + entry.value.length;
  }, 0);
 
  if (totalSize > 1024) {
    // > 1KB
    logger.warn("Baggage size exceeded 1KB", {
      size: totalSize,
      keys: entries.map(([k]) => k),
    });
  }
}

Debugging Baggage

Просмотр baggage в Jaeger:

Baggage НЕ отображается в Jaeger UI напрямую! Нужно:

  1. Добавить baggage как span attributes:
const bag = propagation.getBaggage(context.active());
if (bag) {
  for (const [key, entry] of bag.getAllEntries()) {
    span.setAttribute(`baggage.${key}`, entry.value);
  }
}
  1. Теперь в Jaeger видны baggage values в span tags!

Практический пример: End-to-end Baggage Flow

// ===== Service A: API Gateway =====
app.post("/api/orders", async (req, res) => {
  // Set baggage from request
  const bag = propagation.createBaggage({
    "tenant.id": { value: req.tenantId },
    "user.id": { value: req.userId.toString() },
    "feature.expressCheckout": { value: "true" },
  });
 
  const ctx = propagation.setBaggage(context.active(), bag);
 
  await context.with(ctx, async () => {
    // Call Service B
    await fetch("http://order-service/orders", {
      method: "POST",
      body: JSON.stringify(req.body),
      // Baggage автоматически добавлен в headers!
    });
  });
 
  res.json({ success: true });
});
 
// ===== Service B: Order Service =====
app.post("/orders", async (req, res) => {
  // Read baggage - автоматически извлечен из headers!
  const tenantId = propagation
    .getBaggage(context.active())
    ?.getEntry("tenant.id")?.value;
  const useExpressCheckout =
    propagation
      .getBaggage(context.active())
      ?.getEntry("feature.expressCheckout")?.value === "true";
 
  // Use baggage для business logic
  if (useExpressCheckout) {
    await expressCheckoutFlow(req.body, tenantId);
  } else {
    await regularCheckoutFlow(req.body, tenantId);
  }
 
  // Call Service C - baggage автоматически передается дальше!
  await fetch("http://payment-service/charge");
});
 
// ===== Service C: Payment Service =====
app.post("/charge", async (req, res) => {
  // Baggage ВСЕ ЕЩЕ доступен!
  const tenantId = propagation
    .getBaggage(context.active())
    ?.getEntry("tenant.id")?.value;
 
  // Filter payments by tenant
  await processPayment({ ...req.body, tenantId });
});

Что происходит под капотом:

Request A → B:
  Headers:
    traceparent: 00-abc-111-01
    baggage: tenant.id=acme,user.id=42,feature.expressCheckout=true
 
Request B → C:
  Headers:
    traceparent: 00-abc-222-01
    baggage: tenant.id=acme,user.id=42,feature.expressCheckout=true

                           Automatically propagated!

Когда использовать Baggage vs Span Attributes

Используйте Span Attributes когда:

  • Данные нужны только для ЭТОГО span
  • Данные технические (http.status_code, db.statement)
  • Большой объем данных (нет лимита)

Используйте Baggage когда:

  • Данные нужны в НЕСКОЛЬКИХ сервисах downstream
  • Бизнес-контекст (tenant_id, feature flags, user segment)
  • Небольшой объем (<1KB total)
  • Нужна доступность в логах/metrics/всех spans

Гибридный подход (recommended):

const tenantId = propagation
  .getBaggage(context.active())
  ?.getEntry("tenant.id")?.value;
 
// Добавляем baggage в span attribute тоже!
span.setAttribute("tenant.id", tenantId);
// Теперь видно И в Jaeger, И доступно downstream!

Как работает автоинструментация

HTTP Client Instrumentation:

  1. Перехватывает исходящий HTTP-запрос
  2. Извлекает trace context из активного span
  3. Добавляет headers traceparent и tracestate
  4. Отправляет запрос

HTTP Server Instrumentation:

  1. Получает входящий HTTP-запрос
  2. Извлекает headers traceparent и tracestate
  3. Создаёт новый span с parent_span_id из header
  4. Продолжает trace

Проверка headers в реальном запросе

Добавьте логирование в ваш User Service:

// Node.js (Express middleware)
app.use((req, res, next) => {
  console.log("=== Incoming Request ===");
  console.log("traceparent:", req.headers.traceparent);
  console.log("tracestate:", req.headers.tracestate);
  next();
});

Вывод:

=== Incoming Request ===
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: undefined

Если traceparent пустой — инструментация HTTP-клиента не работает!


Manual Propagation (когда автоинструментация не работает)

Сценарий 1: Custom HTTP Client

Если вы используете нестандартный HTTP-клиент (например, нативный fetch или кастомную библиотеку):

Язык программирования:

Сценарий 2: Message Queues (RabbitMQ, Kafka)

Проблема: HTTP headers не работают в очередях. Нужно передавать context в message metadata.

RabbitMQ Example (Node.js)

const { trace, context, propagation } = require("@opentelemetry/api");
const amqp = require("amqplib");
 
// Producer: Send message with trace context
async function publishMessage(queue, message) {
  return tracer.startActiveSpan("publishToQueue", async (span) => {
    const connection = await amqp.connect("amqp://localhost");
    const channel = await connection.createChannel();
 
    // Inject context в message properties
    const headers = {};
    propagation.inject(context.active(), headers);
 
    span.setAttribute("messaging.system", "rabbitmq");
    span.setAttribute("messaging.destination", queue);
 
    await channel.assertQueue(queue);
    await channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
      headers, // ← trace context в headers сообщения
    });
 
    span.end();
    await connection.close();
  });
}
 
// Consumer: Extract context from message
async function consumeMessages(queue) {
  const connection = await amqp.connect("amqp://localhost");
  const channel = await connection.createChannel();
 
  await channel.assertQueue(queue);
  channel.consume(queue, (msg) => {
    // Extract context из message headers
    const extractedContext = propagation.extract(
      context.active(),
      msg.properties.headers || {}
    );
 
    // Создаём span в extracted context
    context.with(extractedContext, () => {
      tracer.startActiveSpan("processQueueMessage", (span) => {
        const message = JSON.parse(msg.content.toString());
 
        span.setAttribute("messaging.system", "rabbitmq");
        span.setAttribute("messaging.source", queue);
 
        // Process message
        console.log("Processing:", message);
 
        channel.ack(msg);
        span.end();
      });
    });
  });
}

Результат в Jaeger:

Trace ID: a1b2c3d4...
 
├─ API Gateway: publishToQueue [50ms]
│  └─ RabbitMQ publish [5ms]

└─ Worker: processQueueMessage [200ms] ← связан через context!
   └─ Business logic [195ms]

Kafka Example (Python)

from opentelemetry import trace, context
from opentelemetry.propagate import inject, extract
from kafka import KafkaProducer, KafkaConsumer
import json
 
tracer = trace.get_tracer(__name__)
 
# Producer
def publish_to_kafka(topic, message):
    with tracer.start_as_current_span("publishToKafka") as span:
        producer = KafkaProducer(bootstrap_servers='localhost:9092')
 
        # Inject context в message headers
        headers = {}
        inject(headers)
 
        # Convert headers to Kafka format: list of (key, value) tuples
        kafka_headers = [(k, v.encode()) for k, v in headers.items()]
 
        span.set_attribute("messaging.system", "kafka")
        span.set_attribute("messaging.destination", topic)
 
        producer.send(
            topic,
            value=json.dumps(message).encode(),
            headers=kafka_headers  # ← trace context
        )
 
        producer.flush()
        producer.close()
 
# Consumer
def consume_from_kafka(topic):
    consumer = KafkaConsumer(
        topic,
        bootstrap_servers='localhost:9092',
        group_id='my-group'
    )
 
    for msg in consumer:
        # Extract context from headers
        headers_dict = {k: v.decode() for k, v in msg.headers}
        ctx = extract(headers_dict)
 
        # Create span in extracted context
        with tracer.start_as_current_span("processKafkaMessage", context=ctx) as span:
            message = json.loads(msg.value.decode())
 
            span.set_attribute("messaging.system", "kafka")
            span.set_attribute("messaging.source", topic)
 
            print(f"Processing: {message}")

Сценарий 3: gRPC

gRPC автоинструментация обычно работает out-of-the-box, но иногда нужно manual propagation.

// Go gRPC Client
import (
    "google.golang.org/grpc"
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
 
func main() {
    conn, err := grpc.Dial(
        "localhost:50051",
        grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),  // ← автоинструментация
        grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    )
    defer conn.Close()
 
    client := pb.NewUserServiceClient(conn)
    response, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: 123})
}

Debugging Broken Traces

Проблема 1: Span'ы из разных сервисов не связаны

Симптомы:

  • В Jaeger два trace вместо одного
  • parent_span_id не совпадает

Решение:

  1. Проверьте HTTP headers на сервере:
console.log("traceparent:", req.headers.traceparent);
  1. Если header пустой — клиент не inject'ит context:

    • Проверьте, что HTTP-клиент автоинструментирован
    • Используйте manual injection (см. примеры выше)
  2. Проверьте версию OpenTelemetry SDK — старые версии могут не поддерживать W3C Trace Context


Проблема 2: Context теряется в async операциях

Симптомы:

  • В синхронном коде trace работает
  • В setTimeout, Promise, async/await — trace ломается

Причина: Context не передаётся автоматически в async callbacks.

Решение (Node.js):

OpenTelemetry использует async_hooks для автоматической передачи context. Убедитесь, что SDK инициализирован до импорта других библиотек:

// ✅ Правильно
require("./tracing"); // ← ПЕРВЫЙ импорт
const express = require("express");
 
// ❌ Неправильно
const express = require("express");
require("./tracing"); // ← слишком поздно

Проблема 3: Multiple propagators

Если в вашей системе используются разные стандарты (W3C, Jaeger, B3), настройте composite propagator:

const { CompositePropagator } = require("@opentelemetry/core");
const { W3CTraceContextPropagator } = require("@opentelemetry/core");
const { JaegerPropagator } = require("@opentelemetry/propagator-jaeger");
const { B3Propagator } = require("@opentelemetry/propagator-b3");
 
const sdk = new NodeSDK({
  textMapPropagator: new CompositePropagator({
    propagators: [
      new W3CTraceContextPropagator(), // ← W3C (preferred)
      new JaegerPropagator(), // ← Jaeger fallback
      new B3Propagator(), // ← Zipkin B3
    ],
  }),
  // ...
});

Визуализация: Complete Trace Flow

┌──────────────────────────────────────────────────────────────────┐
│                         Client Request                           │
└────────────────────────┬─────────────────────────────────────────┘


              ┌─────────────────────┐
              │   API Gateway       │
              │  trace_id: a1b2c3   │
              │  span_id: 0001      │ ← Root span
              └──────────┬──────────┘
                         │ HTTP Request
                         │ traceparent: 00-a1b2c3-0001-01

              ┌─────────────────────┐
              │  Auth Service       │
              │  trace_id: a1b2c3   │ ← Same trace!
              │  span_id: 0002      │
              │  parent_id: 0001    │ ← Points to Gateway
              └──────────┬──────────┘
                         │ RabbitMQ Message
                         │ headers: {traceparent: 00-a1b2c3-0002-01}

              ┌─────────────────────┐
              │  Worker Service     │
              │  trace_id: a1b2c3   │ ← Same trace!
              │  span_id: 0003      │
              │  parent_id: 0002    │ ← Points to Auth
              └─────────────────────┘

Практическое задание

Задача: Создайте систему с RabbitMQ:

  1. API Gateway публикует сообщение в очередь
  2. Worker обрабатывает сообщение
  3. Убедитесь, что spans связаны в единый trace

Требования:

  • Manual propagation для RabbitMQ
  • Custom span в Worker: processQueueMessage
  • Trace должен показывать полный путь: Gateway → RabbitMQ → Worker

Ожидаемый результат в Jaeger:

├─ API Gateway: POST /api/job [100ms]
│  └─ publishToQueue [20ms]
│     └─ RabbitMQ send [5ms]

└─ Worker: processQueueMessage [500ms]
   └─ Business logic [480ms]

Best Practices

1. Всегда используйте W3C Trace Context:

  • Стандартизировано
  • Поддерживается всеми современными tracing системами
  • Совместимо с облачными провайдерами

2. Проверяйте context propagation на границах:

  • HTTP → HTTP
  • HTTP → Message Queue
  • Message Queue → HTTP
  • gRPC → gRPC

3. Логируйте traceparent header для debugging:

logger.info("Processing request", {
  traceparent: req.headers.traceparent,
  traceId: span.spanContext().traceId,
});

4. Используйте composite propagator для совместимости


Следующий урок

В следующем уроке мы изучим стратегии сэмплирования — как решить, какие trace отправлять в backend, а какие игнорировать, чтобы не перегрузить систему и не потерять важные данные.

Теперь вы понимаете магию автоинструментации и можете починить любой broken trace!