Trazabilidad distribuida: siguiendo peticiones entre servicios
Cuando una petición atraviesa cinco servicios, los logs aislados no sirven: las trazas distribuidas los unen con trace IDs, spans y contexto.

Un usuario hace clic en "Realizar pedido" y la petición fluye a través de una API gateway, el servicio de pedidos, el servicio de inventario, el servicio de pagos y el servicio de notificaciones. El pedido se agota. ¿Dónde está el cuello de botella? Sin trazabilidad distribuida, tienes cinco archivos de logs separados sin forma de correlacionarlos. Con tracing, cada servicio anota su trabajo con trace y span IDs, creando una línea de tiempo unificada de toda la petición.
Trazas, spans y contexto
Un trace representa todo el ciclo de vida de una petición a través del sistema. Cada trace contiene varios spans — cada span representa una unidad de trabajo dentro de un único servicio.
import { trace, SpanKind, context, propagation } from "@opentelemetry/api";
const tracer = trace.getTracer("order-service");
// Each function call creates a span within the trace
async function createOrder(userId: string, items: OrderItem[]): Promise<Order> {
return tracer.startActiveSpan("createOrder", async (span) => {
span.setAttribute("user.id", userId);
span.setAttribute("order.item_count", items.length);
try {
// Each of these creates child spans
const inventory = await checkInventory(items);
const payment = await processPayment(userId, calculateTotal(items));
const order = await saveOrder(userId, items, payment.id);
span.setAttribute("order.id", order.id);
span.setStatus({ code: 0 }); // OK
return order;
} catch (error) {
span.setStatus({
code: 2, // ERROR
message: error instanceof Error ? error.message : "Unknown error",
});
throw error;
} finally {
span.end();
}
});
}Propagación de contexto
Cuando el servicio A llama al servicio B, el contexto del trace debe propagarse para que los spans de B estén vinculados al trace de A.
// ❌ No context propagation — spans from different services are disconnected
async function callInventoryService(items: OrderItem[]) {
const response = await fetch("http://inventory-service/api/check", {
method: "POST",
body: JSON.stringify({ items }),
});
return response.json();
}
// ✅ Inject trace context into outgoing request headers
async function callInventoryService(items: OrderItem[]) {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
// OpenTelemetry injects W3C Trace Context headers automatically
// traceparent: 00-<trace-id>-<span-id>-01
propagation.inject(context.active(), headers);
const response = await fetch("http://inventory-service/api/check", {
method: "POST",
headers,
body: JSON.stringify({ items }),
});
return response.json();
}
// Receiving service extracts the context
function tracingMiddleware(req: Request, res: Response, next: NextFunction) {
const parentContext = propagation.extract(context.active(), req.headers);
context.with(parentContext, () => {
const span = tracer.startSpan("handleRequest", {
kind: SpanKind.SERVER,
attributes: {
"http.method": req.method,
"http.url": req.url,
},
});
res.on("finish", () => {
span.setAttribute("http.status_code", res.statusCode);
span.end();
});
next();
});
}Configurando OpenTelemetry
OpenTelemetry proporciona un SDK independiente del proveedor para instrumentación. Configúralo una vez y exporta a cualquier backend (Jaeger, Zipkin, Datadog, Grafana Tempo).
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
const sdk = new NodeSDK({
resource: new Resource({
"service.name": "order-service",
"service.version": "1.2.0",
"deployment.environment": process.env.NODE_ENV ?? "development",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
}),
instrumentations: [
getNodeAutoInstrumentations({
// Auto-instrument HTTP, Express, database clients, etc.
"@opentelemetry/instrumentation-http": { enabled: true },
"@opentelemetry/instrumentation-express": { enabled: true },
"@opentelemetry/instrumentation-pg": { enabled: true },
}),
],
});
sdk.start();
// Graceful shutdown — flush pending spans on exit
process.on("SIGTERM", () => {
sdk.shutdown().then(() => process.exit(0));
});Añadiendo atributos personalizados
La instrumentación automática captura llamadas HTTP y consultas a la base de datos. Los atributos personalizados añaden contexto de negocio que hace que los traces sean útiles.
// ❌ Trace with only HTTP metadata — "a POST to /api/orders took 2s"
// Doesn't tell you WHY it was slow
// ✅ Custom attributes add business context
async function processPayment(userId: string, amount: Money): Promise<PaymentResult> {
return tracer.startActiveSpan("processPayment", async (span) => {
span.setAttribute("payment.amount", amount.value);
span.setAttribute("payment.currency", amount.currency);
span.setAttribute("payment.provider", "stripe");
const result = await stripe.charges.create({
amount: amount.value,
currency: amount.currency,
});
span.setAttribute("payment.status", result.status);
span.setAttribute("payment.id", result.id);
if (result.status === "failed") {
span.setAttribute("payment.failure_reason", result.failure_message ?? "unknown");
span.addEvent("payment_failed", {
reason: result.failure_message ?? "unknown",
decline_code: result.decline_code ?? "none",
});
}
span.end();
return result;
});
}Estrategia de muestreo
Trazar cada petición en un sistema con mucho tráfico genera enormes cantidades de datos. El muestreo determina qué peticiones se trazan.
import { TraceIdRatioBasedSampler, ParentBasedSampler } from "@opentelemetry/sdk-trace-base";
// Sample 10% of requests, but always trace requests where the parent was traced
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1), // 10% of root spans
// If an incoming request already has a trace context, always trace it
// This ensures complete traces across services
});
// For debugging: always trace requests with a specific header
import { Sampler, SamplingResult, SamplingDecision } from "@opentelemetry/sdk-trace-base";
class DebugHeaderSampler implements Sampler {
shouldSample(parentContext: unknown, traceId: string): SamplingResult {
// Always sample if debug header is present
// Otherwise fall back to ratio sampling
return {
decision: SamplingDecision.RECORD_AND_SAMPLED,
};
}
toString(): string {
return "DebugHeaderSampler";
}
}Puntos clave
- Los traces conectan spans entre servicios — una petición de usuario genera spans en cada servicio que toca
- La propagación de contexto es esencial — inyecta cabeceras de trace en las peticiones salientes y extráelas de las entrantes
- OpenTelemetry es independiente del proveedor — instrumenta una vez, exporta a cualquier backend de tracing
- Los atributos personalizados añaden contexto de negocio — los metadatos HTTP por sí solos no explican por qué una petición fue lenta
- Muestrea estratégicamente — traza el 100% en desarrollo, el 1-10% en producción, y completa siempre los traces parentales
- La instrumentación automática cubre lo básico — spans de HTTP, bases de datos y frameworks se capturan sin código manual


