Context Propagation и W3C Trace Context
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 sampled2. tracestate — vendor-specific данные (опционально)
tracestate: vendorname1=value1,vendorname2=value2Используется для передачи дополнительных данных между сервисами (например, sampling rate, tenant ID).
W3C Baggage: передача бизнес-контекста
Baggage — это W3C стандарт для передачи произвольных key-value пар через всю цепочку сервисов.
В чем разница: Baggage vs Span Attributes
| Аспект | Span Attributes | Baggage |
|---|---|---|
| Scope | Только текущий span | Весь trace (все сервисы) |
| Propagation | ❌ Не передается | ✅ Автоматически везде |
| Use case | Технические детали span | Бизнес-контекст |
| Size limit | Практически без лимита | ~8KB total (осторожно!) |
| Performance | Минимальный overhead | Overhead на каждый hop |
Формат Baggage Header
baggage: key1=value1,key2=value2,key3=value3;metadataПример:
baggage: userId=42,tenantId=acme,featureFlag=newUI,experimentId=exp123Use 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 для:
-
Feature flags / A/B experiments
"feature.experimentId": "exp-123" "feature.variant": "B" -
Tenant/Organization ID
"tenant.id": "acme-corp" "tenant.tier": "enterprise" -
User segments
"user.segment": "premium" "user.region": "eu-west" -
Request correlation metadata
"request.sourceApp": "mobile-ios" "request.apiVersion": "v2"
❌ НЕ используйте Baggage для:
-
Large payloads - limit ~8KB total!
// ❌ BAD "user.profile": JSON.stringify(hugeObject) // Too big! -
Sensitive data (PII)
// ❌ BAD - Baggage передается в КАЖДОМ header! "user.creditCard": "4111-1111-1111-1111" // Security risk! "user.password": "..." // Never! -
Frequently changing values
// ❌ BAD - overhead на каждый request "timestamp": Date.now().toString() // Changes every time -
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 напрямую! Нужно:
- Добавить baggage как span attributes:
const bag = propagation.getBaggage(context.active());
if (bag) {
for (const [key, entry] of bag.getAllEntries()) {
span.setAttribute(`baggage.${key}`, entry.value);
}
}- Теперь в 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:
- Перехватывает исходящий HTTP-запрос
- Извлекает trace context из активного span
- Добавляет headers
traceparentиtracestate - Отправляет запрос
HTTP Server Instrumentation:
- Получает входящий HTTP-запрос
- Извлекает headers
traceparentиtracestate - Создаёт новый span с
parent_span_idиз header - Продолжает 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не совпадает
Решение:
- Проверьте HTTP headers на сервере:
console.log("traceparent:", req.headers.traceparent);-
Если header пустой — клиент не inject'ит context:
- Проверьте, что HTTP-клиент автоинструментирован
- Используйте manual injection (см. примеры выше)
-
Проверьте версию 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:
- API Gateway публикует сообщение в очередь
- Worker обрабатывает сообщение
- Убедитесь, что 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!