Saltar al contenido

Diseño de pipelines de trazabilidad distribuida confiables

Construye trazabilidad distribuida de producción con OpenTelemetry: propagación de contexto, muestreo, backends, correlación y cardinalidad.

5 min de lectura
Diagrama de cascada de trazas distribuidas que muestra spans en varios microservicios con tiempos, códigos de estado y relaciones padre-hijo conectadas por propagación de contexto de traza

El trazado distribuido te muestra el recorrido completo de una petición entre servicios. Sin él, depurar en una arquitectura de microservicios es adivinación: estás mirando logs individuales de cada servicio esperando correlacionar timestamps. Con él, ves la ruta completa de la petición, dónde se gastó el tiempo y exactamente qué servicio causó el fallo.

Pero la infraestructura de trazabilidad que funciona en desarrollo suele fallar en producción. El volumen de datos de trazas a escala exige muestreo inteligente, los requisitos de almacenamiento pueden superar a tus bases de datos de aplicación, y una propagación de contexto mal configurada crea huecos que hacen las trazas inútiles justo cuando más las necesitas.

Instrumentación con OpenTelemetry

OpenTelemetry proporciona la base independiente del proveedor. Instrumenta una vez, exporta a cualquier backend.

tstypescript
// ❌ Creación manual de spans en todas partes: ruidosa, frágil
import { trace } from "@opentelemetry/api";
 
async function handleOrder(orderId: string) {
  const span = trace.getTracer("app").startSpan("handleOrder");
  try {
    span.setAttribute("order.id", orderId);
    // 50 lines of manual span management per function
    // Developers forget, spans are inconsistent
  } finally {
    span.end();
  }
}
tstypescript
// ✅ Configuración del SDK con instrumentación automática
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  getNodeAutoInstrumentations,
} from "@opentelemetry/auto-instrumentations-node";
import {
  OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-grpc";
import {
  BatchSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { Resource } from "@opentelemetry/resources";
import {
  ATTR_SERVICE_NAME,
  ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
 
const exporter = new OTLPTraceExporter({
  url: "http://otel-collector:4317",
});
 
const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: "order-service",
    [ATTR_SERVICE_VERSION]: "2.4.1",
    "deployment.environment": process.env.NODE_ENV ?? "development",
  }),
  spanProcessors: [
    new BatchSpanProcessor(exporter, {
      maxQueueSize: 2048,
      maxExportBatchSize: 512,
      scheduledDelayMillis: 5000,
    }),
  ],
  instrumentations: [
    getNodeAutoInstrumentations({
      "@opentelemetry/instrumentation-http": {
        ignoreIncomingRequestHook: (req) =>
          req.url === "/health" || req.url === "/ready",
      },
      "@opentelemetry/instrumentation-express": {
        enabled: true,
      },
      "@opentelemetry/instrumentation-pg": {
        enhancedDatabaseReporting: true,
      },
    }),
  ],
});
 
sdk.start();
 
// Apagado graceful
process.on("SIGTERM", async () => {
  await sdk.shutdown();
  process.exit(0);
});

La instrumentación automática cubre clientes y servidores HTTP, drivers de base de datos, colas de mensajes y gRPC de forma automática. Los spans manuales solo se necesitan para los límites de la lógica de negocio que la instrumentación automática no puede detectar.

Estrategias de muestreo

A escala de producción, trazar el 100% de las peticiones es poco práctico: el volumen de datos desborda el almacenamiento y el pipeline del collector. El muestreo decide qué trazas conservar.

tstypescript
// Muestreo head-based: decidir al inicio de la traza
import {
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
  AlwaysOnSampler,
} from "@opentelemetry/sdk-trace-base";
import { Sampler, SamplingResult } from "@opentelemetry/api";
 
// Sampler compuesto: distintas tasas según el tráfico
class RuleBasedSampler implements Sampler {
  shouldSample(
    context: any,
    traceId: string,
    spanName: string,
    spanKind: any,
    attributes: Record<string, unknown>
  ): SamplingResult {
    const path = attributes["http.target"] as string;
 
    // Trazar siempre los errores
    const statusCode = attributes["http.status_code"];
    if (statusCode && Number(statusCode) >= 500) {
      return { decision: 1 }; // RECORD_AND_SAMPLED
    }
 
    // Trazar siempre las peticiones lentas (se decide luego con tail sampling)
    // Health checks de alto tráfico: nunca
    if (path === "/health" || path === "/metrics") {
      return { decision: 0 }; // NOT_RECORD
    }
 
    // Rutas de pago: trazar siempre
    if (path?.startsWith("/api/payments")) {
      return { decision: 1 };
    }
 
    // Por defecto: muestreo del 10%
    const hash = traceId
      .slice(-8)
      .split("")
      .reduce((h, c) => h * 31 + c.charCodeAt(0), 0);
 
    return {
      decision: Math.abs(hash) % 100 < 10 ? 1 : 0,
    };
  }
 
  toString(): string {
    return "RuleBasedSampler";
  }
}
ymlyaml
# Muestreo tail-based en el OTel Collector
# Decide tras ver la traza completa
processors:
  tail_sampling:
    decision_wait: 30s
    num_traces: 100000
    policies:
      # Conservar siempre las trazas con error
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
 
      # Conservar siempre las trazas lentas
      - name: latency
        type: latency
        latency:
          threshold_ms: 2000
 
      # Muestrear el 5% del tráfico normal
      - name: probabilistic
        type: probabilistic
        probabilistic:
          sampling_percentage: 5
 
      # Conservar siempre las trazas de servicios críticos
      - name: critical-services
        type: string_attribute
        string_attribute:
          key: service.name
          values:
            - payment-service
            - auth-service

El muestreo tail-based en el collector es más potente que el muestreo head-based en el SDK porque puede ver la traza completa antes de decidir. Las trazas con error, las trazas lentas y las trazas de rutas críticas siempre se conservan, mientras que las peticiones exitosas de rutina se muestrean probabilísticamente.

Propagación de contexto entre servicios

El contexto de traza debe propagarse correctamente a través de cada límite de comunicación. Un solo servicio que pierda el contexto rompe la traza.

tstypescript
// Propagación de contexto en sistemas de mensajes asíncronos
import {
  propagation,
  context,
  trace,
  SpanKind,
} from "@opentelemetry/api";
 
// Productor: inyectar contexto de traza en los headers del mensaje
function publishEvent(
  queue: string,
  payload: Record<string, unknown>
): void {
  const tracer = trace.getTracer("publisher");
  const span = tracer.startSpan("publish", {
    kind: SpanKind.PRODUCER,
    attributes: {
      "messaging.system": "rabbitmq",
      "messaging.destination": queue,
    },
  });
 
  // Inyectar el contexto actual en los headers del mensaje
  const headers: Record<string, string> = {};
  propagation.inject(
    trace.setSpan(context.active(), span),
    headers
  );
 
  channel.publish(queue, {
    body: Buffer.from(JSON.stringify(payload)),
    properties: { headers },
  });
 
  span.end();
}
 
// Consumidor: extraer contexto de traza de los headers del mensaje
function consumeEvent(message: Message): void {
  // Extraer el contexto padre de los headers del mensaje
  const parentContext = propagation.extract(
    context.active(),
    message.properties.headers
  );
 
  const tracer = trace.getTracer("consumer");
 
  // Crear span de consumidor vinculado al productor
  context.with(parentContext, () => {
    const span = tracer.startSpan("process", {
      kind: SpanKind.CONSUMER,
      attributes: {
        "messaging.system": "rabbitmq",
        "messaging.operation": "process",
      },
    });
 
    try {
      processMessage(message);
      span.setStatus({ code: 0 }); // OK
    } catch (error) {
      span.setStatus({
        code: 2, // ERROR
        message:
          error instanceof Error
            ? error.message
            : "Unknown error",
      });
      span.recordException(error as Error);
      throw error;
    } finally {
      span.end();
    }
  });
}

Correlacionar trazas con logs y métricas

Las trazas se vuelven realmente potentes cuando se correlacionan con logs y métricas. Un trace ID en cada entrada de log te permite saltar de una traza a los logs exactos de esa petición.

tstypescript
// Logging estructurado con correlación de trazas
import { context, trace } from "@opentelemetry/api";
import pino from "pino";
 
function createLogger(serviceName: string) {
  const baseLogger = pino({
    level: process.env.LOG_LEVEL ?? "info",
  });
 
  return {
    info(message: string, data?: Record<string, unknown>) {
      baseLogger.info({
        ...data,
        ...getTraceContext(),
        service: serviceName,
        msg: message,
      });
    },
 
    error(
      message: string,
      error?: Error,
      data?: Record<string, unknown>
    ) {
      baseLogger.error({
        ...data,
        ...getTraceContext(),
        service: serviceName,
        msg: message,
        error: error
          ? {
              message: error.message,
              stack: error.stack,
              name: error.name,
            }
          : undefined,
      });
    },
  };
}
 
function getTraceContext(): Record<string, string> {
  const span = trace.getSpan(context.active());
  if (!span) return {};
 
  const spanContext = span.spanContext();
  return {
    traceId: spanContext.traceId,
    spanId: spanContext.spanId,
    traceFlags: String(spanContext.traceFlags),
  };
}
 
// Cada entrada de log incluye trace_id y span_id
// {"level":"info","traceId":"abc123...","spanId":"def456...",
//  "service":"order-service","msg":"Order created",
//  "orderId":"ord-789"}

Puntos clave

La instrumentación automática con OpenTelemetry cubre spans de HTTP, bases de datos, colas de mensajes y gRPC automáticamente: la creación manual de spans solo debe añadirse para los límites de la lógica de negocio que la instrumentación automática no puede detectar. El muestreo tail-based en el OTel Collector conserva todas las trazas con error, las trazas lentas y las trazas de rutas críticas, mientras muestrea probabilísticamente el tráfico normal, evitando los puntos ciegos de las decisiones de muestreo head-based. La propagación de contexto debe cruzar cada límite de comunicación, incluidas las colas de mensajes: inyectar el contexto de traza en los headers de los mensajes y extraerlo en los consumidores mantiene la cadena de trazas a través de flujos de trabajo asíncronos. Correlacionar trazas con logs mediante el trace ID en entradas de log estructuradas permite saltar desde un span lento directamente a las líneas de log relevantes, combinando el "qué pasó" de las trazas con el "por qué" de los logs. Los batch span processors con tamaños de cola e intervalos de exportación ajustados evitan que el trazado impacte el rendimiento de la aplicación: los spans se almacenan en buffer y se exportan de forma asíncrona en lugar de enviarse en línea con el procesamiento de la petición.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX