Patrón Distributed Tracing
Cómo propagar trace context a través de service boundaries con OpenTelemetry. Cubre span creation, context propagation, sampling, y trace analysis.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
El distributed tracing sigue un single request mientras fluye through múltiples services, creando un tree de spans donde cada span representa una unit de work. A diferencia de los correlation IDs (que solo taggean logs), el distributed tracing captura timing, causality, y context para cada step. OpenTelemetry es la standard implementation, usando W3C trace context headers (traceparent/tracestate) para propagar trace IDs across HTTP, gRPC, y message queue boundaries. El resultado es un visual timeline mostrando exactamente dónde se gasta tiempo y dónde ocurren errors across el entire request path.
When to Use
- Arquitecturas de microservices donde los requests span múltiples services
- Performance debugging — identificar qué service en un chain es slow
- Error investigation — tracear el exact path de un failed request
- Service dependency mapping — entender qué services llaman a qué
- Latency optimization — encontrar el critical path y longest spans
When NOT to Use
- Aplicaciones monolíticas — un single process no tiene distributed boundary
- Scripts simples o batch jobs sin inter-service calls
- Aplicaciones con muy bajo tráfico donde sampling provee insufficient data
- Cuando los correlation IDs en logs son sufficient para tus debugging needs
Solution
OpenTelemetry setup (Python)
# Python — OpenTelemetry setup con OTLP exporter
from opentelemetry import trace
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.resources import Resource
# Configure tracer
resource = Resource.create({
"service.name": "order-service",
"service.version": "1.0.0",
"deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4317")
processor = BatchSpanProcessor(exporter)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("order-service")
Manual span creation
# Python — manual span creation
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def process_order(order_id, customer_id):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("customer.id", customer_id)
span.set_attribute("order.status", "processing")
# Child span: validate order
validate_order(order_id)
# Child span: charge payment
payment_result = charge_payment(order_id)
# Child span: update inventory
update_inventory(order_id)
span.set_attribute("order.status", "completed")
span.set_attribute("payment.status", payment_result.status)
return payment_result
def validate_order(order_id):
with tracer.start_as_current_span("validate_order") as span:
span.set_attribute("order.id", order_id)
# Validation logic
order = fetch_order(order_id)
if not order:
span.set_status(trace.Status(trace.StatusCode.ERROR, "Order not found"))
raise ValueError("Order not found")
span.set_attribute("order.items_count", len(order.items))
def charge_payment(order_id):
with tracer.start_as_current_span("charge_payment") as span:
span.set_attribute("order.id", order_id)
# Payment logic
result = payment_gateway.charge(order_id)
span.set_attribute("payment.amount", result.amount)
span.set_attribute("payment.status", result.status)
return result
Automatic instrumentation (FastAPI)
# Python — automatic instrumentation para FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
from fastapi import FastAPI
app = FastAPI()
# Auto-instrument HTTP server, HTTP client, y database
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor().instrument()
AsyncPGInstrumentor().instrument()
# Todos los HTTP requests, downstream calls, y DB queries se tracean automáticamente
@app.post("/api/orders")
async def create_order(request: OrderRequest):
# Esto crea un span para el HTTP request
# Downstream HTTP calls crean child spans
# DB queries crean child spans
order = await order_service.create(request)
return order
HTTP context propagation
# Python — propagar trace context en HTTP headers
import httpx
from opentelemetry import trace, propagate
from opentelemetry.propagators.textmap import default_setter
async def call_payment_service(order_data: dict):
headers = {}
# Inject current trace context en headers
propagate.inject(headers)
# headers ahora contiene:
# traceparent: 00-<trace-id>-<span-id>-<flags>
# tracestate: ...
async with httpx.AsyncClient() as client:
response = await client.post(
"https://payment-service/api/charge",
json=order_data,
headers=headers,
)
return response.json()
Extract context en receiving side
# Python — extract trace context del incoming request
from opentelemetry import trace, propagate
from fastapi import Request, FastAPI
app = FastAPI()
@app.middleware("http")
async def tracing_middleware(request: Request, call_next):
# Extract trace context del incoming headers
ctx = propagate.extract(request.headers)
# Start un new span linked al parent del caller
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
f"{request.method} {request.url.path}",
context=ctx,
) as span:
span.set_attribute("http.method", request.method)
span.set_attribute("http.url", str(request.url))
span.set_attribute("http.scheme", request.url.scheme)
span.set_attribute("http.host", request.url.host)
response = await call_next(request)
span.set_attribute("http.status_code", response.status_code)
if response.status_code >= 400:
span.set_status(trace.Status(trace.StatusCode.ERROR))
return response
Node.js con OpenTelemetry
// JavaScript — OpenTelemetry setup para Node.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const sdk = new NodeSDK({
resource: resourceFromAttributes({
'service.name': 'order-service',
'service.version': '1.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PgInstrumentation(),
],
});
sdk.start();
// Manual spans
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service');
async function processOrder(orderId) {
return tracer.startActiveSpan('process_order', async (span) => {
span.setAttribute('order.id', orderId);
try {
const validated = await validateOrder(orderId);
const payment = await chargePayment(orderId);
span.setAttribute('payment.status', payment.status);
span.setAttribute('order.status', 'completed');
return payment;
} catch (err) {
span.recordException(err);
span.setStatus({ code: 2, message: err.message });
throw err;
} finally {
span.end();
}
});
}
Message queue context propagation
# Python — propagar trace context through RabbitMQ
import pika
import json
from opentelemetry import trace, propagate
def publish_order_event(order_data):
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
headers = {}
propagate.inject(headers)
channel.basic_publish(
exchange='orders',
routing_key='order.created',
body=json.dumps(order_data),
properties=pika.BasicProperties(
headers=headers,
content_type='application/json',
),
)
connection.close()
# Consumer side
def consume_order_events():
def callback(ch, method, properties, body):
# Extract trace context del message headers
ctx = propagate.extract(properties.headers or {})
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process_order_event", context=ctx) as span:
order_data = json.loads(body)
span.set_attribute("order.id", order_data['order_id'])
span.set_attribute("event.type", "order.created")
# Process el event — child spans se linked al original trace
process_order(order_data)
channel.basic_consume(queue='orders', on_message_callback=callback)
channel.start_consuming()
Span events y links
# Python — span events para logging within spans
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def process_refund(order_id, reason):
with tracer.start_as_current_span("process_refund") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("refund.reason", reason)
# Add events (como structured log entries within un span)
span.add_event("refund_initiated", {
"order.id": order_id,
"amount": 99.99,
})
try:
result = refund_processor.process(order_id)
span.add_event("refund_completed", {"status": result.status})
return result
except Exception as e:
span.add_event("refund_failed", {"error": str(e)})
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
raise
Sampling strategies
# Python — configure sampling
from opentelemetry.sdk.trace.sampling import (
TraceIdRatioBased,
ParentBased,
ALWAYS_ON,
)
# Head-based sampling — samplea al trace start
sampler = ParentBased(
root=TraceIdRatioBased(rate=0.1), # Samplear 10% de new traces
)
provider = TracerProvider(resource=resource, sampler=sampler)
# Tail-based sampling — samplea después de trace completion
# Configurado en OTel Collector, no en application code
# Permite sampling basado en trace characteristics (errors, slow traces)
# OTel Collector — tail-based sampling
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling]
exporters: [jaeger]
processors:
tail_sampling:
decision_wait: 30s
policies:
# Samplear todos los error traces
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
# Samplear 10% de successful traces
- name: baseline
type: probabilistic
probabilistic:
sampling_percentage: 10
# Samplear todos los traces más lentos que 2 seconds
- name: slow
type: latency
latency:
threshold_ms: 2000
Java Spring Boot instrumentation
// Java — OpenTelemetry agent para automatic instrumentation
// Run con: java -javaagent:opentelemetry-javaagent.jar -jar app.jar
// Manual spans con OpenTelemetry API
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.GlobalOpenTelemetry;
@RestController
public class OrderController {
private final Tracer tracer = GlobalOpenTelemetry.getTracer("order-service");
@PostMapping("/api/orders")
public Order createOrder(@RequestBody OrderRequest request) {
Span span = tracer.spanBuilder("create_order").startSpan();
try (var scope = span.makeCurrent()) {
span.setAttribute("order.customer_id", request.getCustomerId());
span.setAttribute("order.total", request.getTotal());
Order order = orderService.create(request);
span.setAttribute("order.id", order.getId());
span.setAttribute("order.status", "created");
return order;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
OTel Collector configuration
# 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
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
zipkin:
endpoint: http://zipkin:9411/api/v2/spans
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [resource, batch]
exporters: [jaeger]
Variants
Trace context con gRPC
# Python — gRPC interceptors para trace propagation
from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer, GrpcInstrumentorClient
# Auto-instrument gRPC server y client
GrpcInstrumentorServer().instrument()
GrpcInstrumentorClient().instrument()
# Todos los gRPC calls propagan automáticamente trace context
# via grpc metadata: traceparent-bin, tracestate-bin
Custom span attributes para business context
# Python — enrich spans con business attributes
def process_order(order):
with tracer.start_as_current_span("process_order") as span:
# Technical attributes
span.set_attribute("order.id", order.id)
# Business attributes — queryable en Jaeger/Tempo
span.set_attribute("order.product_category", order.category)
span.set_attribute("order.region", order.region)
span.set_attribute("order.payment_method", order.payment_method)
span.set_attribute("order.item_count", len(order.items))
span.set_attribute("order.total_usd", order.total)
# Estos attributes se pueden usar para filtering en trace analysis
# e.g., "show all traces where order.region = us-east AND order.total > 100"
Best Practices
-
For a deeper guide, see Distributed Tracing: OpenTelemetry, Jaeger, Zipkin.
-
Usá automatic instrumentation first — cubre HTTP, DB, y messaging sin code changes
-
Agregá manual spans para business logic — automatic instrumentation no sabe sobre tu domain
-
Seteá meaningful span names —
process_ordernospan_123ofunction_1 -
Agregá business attributes — product category, region, payment method. Estos son queryable en trace analysis.
-
Propagá context en todos lados — HTTP, gRPC, message queues. Missing propagation rompe el trace tree.
-
Usá tail-based sampling — keepá todos los errors y slow traces, sampleá el resto
-
Mantené spans short-lived — spans largos consumen memory. Breakelos en child spans.
-
Recordá exceptions — usá
span.record_exception()yspan.set_status(ERROR)para failures
Common Mistakes
- No propagar context: olvidar inject/extract trace context en service boundaries. El trace tree se rompe, mostrando disconnected spans.
- Span names demasiado generic:
handle_requestpara cada endpoint. Usá specific names comocreate_order,process_payment. - Demasiados spans: crear un span para cada function call. Esto crea noise y overhead. Focate en meaningful operations.
- No record errors: las exceptions se catchean pero no se recordan en el span. Usá
record_exception()para capturar stack traces. - 100% sampling en production: tracear cada request crea enormous volume. Usá 1-10% sampling con tail-based selection de errors.
FAQ
¿Qué es un span?
Un span representa una single operation dentro de un trace. Tiene un start time, duration, name, attributes, y parent span. Un trace es un tree de spans mostrando el full path de un request.
¿En qué se diferencia distributed tracing de correlation IDs?
Los correlation IDs taggean logs con un request ID. El distributed tracing captura timing, parent-child relationships, y attributes para cada operation. Tracing te muestra el timeline y causality, no solo qué logs belong together.
¿Qué es OpenTelemetry?
OpenTelemetry es el CNCF standard para telemetry (traces, metrics, logs). Provee APIs, SDKs, y auto-instrumentation para 12+ languages. Reemplazó OpenTracing y OpenCensus.
¿Qué sampling rate debería usar?
Para production: 1-10% head-based sampling, o tail-based sampling que keepá todos los errors y slow traces mientras samplea el resto. Para development: 100% sampling.
¿Qué es el W3C trace context?
El standard para propagar trace context across service boundaries usando traceparent y tracestate HTTP headers. OpenTelemetry usa esto por default para HTTP y gRPC.
Recursos Relacionados
Correlation ID: Trace Requests Across Distributed Services
How to propagate correlation IDs across service boundaries for end-to-end request tracing. Covers HTTP headers, message queues, and logging integration.
PatternStructured Logging: Emit JSON Logs with Consistent Fields
How to emit structured JSON logs with consistent fields for searchability. Covers Python structlog, Winston, Serilog, log levels, and log aggregation.
PatternMetrics Aggregation: Collect, Tag
How to collect, tag, and aggregate business metrics for observability. Covers Prometheus, OpenTelemetry, custom metrics, histograms, and dashboarding.