DEV Community

Cover image for OpenTelemetry: el estándar que unifica trazas, métricas y logs
lu1tr0n
lu1tr0n

Posted on Originally published at elsolitario.org

OpenTelemetry: el estándar que unifica trazas, métricas y logs

Un usuario reporta que el checkout tardó ocho segundos. El backend tiene catorce microservicios y ningún log individual explica dónde se perdió el tiempo. Ese es el problema que OpenTelemetry resuelve: un estándar abierto que conecta trazas, métricas y logs de todos esos servicios bajo un mismo identificador, sin atarte a un proveedor de monitoreo.

El proyecto nació en 2019 de la fusión de OpenTracing y OpenCensus y hoy es uno de los proyectos con más actividad dentro de la Cloud Native Computing Foundation. La diferencia con las herramientas de monitoreo tradicionales es que el código no queda atado a un proveedor: la instrumentación es la misma, sin importar si el backend termina siendo Jaeger, Grafana Tempo o un SaaS de pago.

TL;DR

  • Entendés la diferencia entre trazas, spans, métricas y logs, y cómo OpenTelemetry los correlaciona.- Instalás y configurás el SDK de Node.js con un exporter de consola y uno OTLP real.- Levantás un Collector con Jaeger en Docker y visualizás tu primera traza completa.- Escribís un span manual para una operación de negocio que la instrumentación automática no cubre.- Sabés configurar un sampler para no capturar el 100% de las trazas en producción.- Identificás errores comunes: contexto no propagado, atributos de alta cardinalidad y sampling mal configurado.- Comparás cuatro backends de trazas (Jaeger, Tempo, Zipkin, SaaS) y sabés cuál conviene según tu equipo.

Qué es OpenTelemetry y por qué importa

OpenTelemetry es un conjunto de APIs, SDKs y herramientas para generar, recolectar y exportar datos de telemetría: trazas distribuidas, métricas y logs. No es un backend de visualización ni un servicio de monitoreo: es la capa de instrumentación que se coloca en el código, y después decidís a dónde mandar esos datos.

Antes de OpenTelemetry, instrumentar una app significaba elegir un SDK específico de un proveedor y quedar atado a él. Migrar de proveedor implicaba reescribir la instrumentación completa. OpenTelemetry separa las dos cosas: el código de instrumentación es neutral, y el exporter (la pieza que manda los datos a un backend) es intercambiable con un cambio de configuración, no de código.

El caso de uso central es la traza distribuida: un identificador único (trace-id) que viaja entre servicios, desde que un usuario hace un click hasta que la respuesta vuelve. Cada operación dentro de esa traza (una consulta SQL, una llamada HTTP, un cálculo) se registra como un span, con su propio tiempo de inicio, duración y atributos. Encadenados, los spans forman un árbol que muestra exactamente dónde se fue el tiempo.

Esto importa particularmente en arquitecturas de microservicios, donde un solo request de usuario puede atravesar diez o veinte servicios distintos. Sin un identificador común, cada servicio genera logs aislados, y correlacionar un error entre ellos requiere buscar por timestamp aproximado, algo que falla en cuanto hay concurrencia.

Cómo funciona OpenTelemetry por dentro

La arquitectura de OpenTelemetry tiene tres capas: la API (las interfaces que usás en el código para crear spans y métricas), el SDK (la implementación que procesa y exporta esos datos) y el Collector, un proceso separado que recibe telemetría de múltiples servicios, la procesa (batching, filtrado, enriquecimiento) y la reenvía a uno o más backends.
El Collector desacopla la app del backend de monitoreo elegido.
Separar el Collector de la app tiene una ventaja concreta: si mañana cambiás de Jaeger a Grafana Tempo, o agregás Prometheus para métricas, el cambio se hace en la configuración del Collector, no en el código de cada microservicio. La app solo necesita saber la URL del Collector, no del backend final.

flowchart TD
A["App instrumentada"] --> B["OpenTelemetry SDK"]
B --> C["OTel Collector"]
C --> D[("Jaeger / Tempo")]
C --> E[("Prometheus")]
subgraph Observabilidad
D
E
end
Enter fullscreen mode Exit fullscreen mode

El protocolo que conecta SDK, Collector y backend se llama OTLP (OpenTelemetry Protocol), y corre sobre gRPC o HTTP con payloads en Protocol Buffers. Es el mismo protocolo sin importar el lenguaje: un SDK de Python y uno de Go mandan datos con el mismo formato de wire, lo que permite mezclar servicios en distintos lenguajes dentro de una sola traza.

Esa interoperabilidad depende de un estándar más chico pero clave: W3C Trace Context, que define cómo viaja el identificador de traza entre servicios. Cuando el servicio A llama al servicio B por HTTP, agrega una cabecera traceparent con el trace-id y el span-id actual. El servicio B la lee, crea sus propios spans como hijos de ese span-id, y así toda la cadena de llamadas queda conectada bajo el mismo trace-id sin que ningún servicio necesite conocer la topología completa del sistema.

Las tres señales que captura OpenTelemetry no son intercambiables: las trazas muestran el camino de un request específico con su latencia por etapa; las métricas agregan números en el tiempo (requests por segundo, percentil 99 de latencia, uso de memoria); los logs son eventos puntuales con contexto arbitrario. OpenTelemetry los correlaciona: un log puede llevar el mismo trace-id que la traza que lo generó, lo que permite saltar de una métrica anómala a la traza específica y de ahí al log exacto que explica el error.

Ejemplos prácticos: instrumentar un servicio Node.js

El primer paso es levantar la instrumentación más simple posible, sin backend externo, para confirmar que el SDK arranca y genera spans. Esto usa el ConsoleSpanExporter, que imprime cada span como JSON en la terminal:

// tracer.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');

const sdk = new NodeSDK({
  traceExporter: new ConsoleSpanExporter(),
  serviceName: 'checkout-service',
});

sdk.start();
Enter fullscreen mode Exit fullscreen mode

Al requerir tracer.js antes que el resto de la app (node -r ./tracer.js server.js), cada request HTTP que reciba el servidor genera un span automáticamente, sin tocar el código de las rutas. En la terminal aparece un bloque JSON por cada span, con campos como traceId, spanId y duration.

El segundo ejemplo reemplaza la consola por un Collector real vía OTLP, y agrega instrumentación automática para librerías comunes (Express, HTTP, MySQL, Redis) con getNodeAutoInstrumentations:

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
  serviceName: 'checkout-service',
});

sdk.start();
Enter fullscreen mode Exit fullscreen mode

Con esto, cada llamada HTTP entrante y saliente, y cada consulta a MySQL o Redis, genera spans sin una sola línea adicional en las rutas de la app. Para operaciones de negocio que la instrumentación automática no cubre, como una llamada a un gateway de pago, se agrega un span manual:

const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('checkout-service');

async function chargeCard(orderId) {
  return tracer.startActiveSpan('charge-card', async (span) => {
    span.setAttribute('order.id', orderId);
    try {
      const result = await paymentGateway.charge(orderId);
      span.setStatus({ code: 1 });
      return result;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: 2, message: err.message });
      throw err;
    } finally {
      span.end();
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Ese span manual queda como hijo del span HTTP que lo originó, automáticamente: OpenTelemetry propaga el contexto activo dentro del mismo proceso sin que chargeCard necesite recibir el trace-id como parámetro.

Cómo empezar: de cero a una traza completa

Los pasos para tener trazas funcionando en un servicio Node.js existente son los siguientes:

  • Instalar las dependencias: npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http.- Levantar un Collector local. La forma más rápida es con Docker: docker run -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one:latest, que expone el puerto OTLP HTTP (4318) y la UI de Jaeger (16686).- Crear el archivo tracer.js con el SDK apuntando a http://localhost:4318/v1/traces, como en el ejemplo anterior.- Arrancar la app precargando el tracer: node -r ./tracer.js server.js.- Generar tráfico real con curl y abrir http://localhost:16686 para buscar el servicio checkout-service en el dropdown.

💡 Tip: Empezá siempre con ConsoleSpanExporter antes de sumar un Collector. Confirmar que los spans se generan en tu propia terminal aísla los problemas de instrumentación de los problemas de infraestructura.

Cuando el request atraviesa varios servicios, cada uno agrega sus propios spans bajo el mismo trace-id, propagado vía la cabecera traceparent:

sequenceDiagram
participant U as Usuario
participant G as API Gateway
participant P as Servicio Pagos
participant D as Base de Datos
U->>G: POST /checkout
G->>P: cobrar orden (traceparent)
P->>D: INSERT pago
D-->>P: OK
P-->>G: pago confirmado
G-->>U: 200 OK
Note over G,P: mismo trace-id en las 3 llamadas
Enter fullscreen mode Exit fullscreen mode

Para confirmar que el Collector está recibiendo datos sin depender de la UI, Jaeger expone un endpoint de estado: curl http://localhost:16686/api/services devuelve la lista de servicios que reportaron al menos una traza. Si checkout-service aparece en esa lista, la instrumentación está funcionando de punta a punta.

Casos de uso reales

El caso más común es el debugging de latencia en microservicios: cuando un endpoint es lento de forma intermitente, la traza muestra exactamente qué span consumió el tiempo, sin necesidad de reproducir el problema localmente. Es la diferencia entre revisar quince logs distintos con timestamps aproximados y abrir una sola traza con el árbol completo de spans.

Otro caso frecuente es detectar dependencias N+1: una traza que muestra cien spans casi idénticos de consulta a base de datos, todos hijos del mismo span padre, es la señal visual de un problema que en logs planos pasa desapercibido.
Cien spans casi idénticos delatan una consulta N+1 en la base de datos.
En sistemas con colas de mensajes (Kafka, RabbitMQ, SQS), OpenTelemetry también propaga contexto de forma asíncrona: el productor agrega el trace-id a los headers del mensaje, y el consumidor lo retoma como un span hijo, aunque pasen minutos entre un evento y otro. Esto conecta trazas que de otra forma se cortarían en el momento en que el mensaje entra a la cola.

Para migraciones de proveedor, la neutralidad del SDK es la ventaja práctica más citada: equipos que migran de un SDK propietario a OpenTelemetry mantienen la instrumentación intacta mientras cambian de backend, porque el punto de acoplamiento pasa a ser la configuración del Collector, no el código de cada servicio.

Errores comunes y buenas prácticas

El error más frecuente es no configurar un sampler y dejar la captura en el 100% de las trazas por defecto. En un servicio con tráfico alto, eso satura el Collector y dispara el costo de almacenamiento del backend, sobre todo en SaaS que cobran por volumen de spans ingeridos.

⚠️ Ojo: El sampler por defecto de OpenTelemetry captura todas las trazas. En producción conviene un TraceIdRatioBasedSampler que capture, por ejemplo, el 10% de las trazas normales y el 100% de las que terminan en error.

Otro error común es no propagar el contexto en llamadas asíncronas manuales: si un handler dispara un setTimeout o publica en una cola sin copiar el contexto activo, el span resultante queda huérfano, sin trace-id, y aparece como una traza aislada en vez de conectarse a la que lo originó.

También es habitual sobrecargar los spans con atributos de alta cardinalidad, como IDs de usuario en el nombre del span en vez de como atributo. Eso rompe la agregación en el backend: cada usuario termina generando un span con nombre único, y las vistas de operación más lenta o percentil 99 por endpoint dejan de ser útiles.

Por último, instrumentar solo el borde del sistema (el API Gateway) y no los servicios internos da una traza incompleta: se ve cuánto tardó el request total, pero no cuál de los servicios internos fue el responsable. La instrumentación automática de librerías cubre la mayoría de los casos sin esfuerzo manual adicional.

Comparativa: dónde guardar las trazas

OpenTelemetry define cómo se genera y transporta la telemetría, pero no dónde se almacena ni cómo se visualiza. Esa decisión es independiente y se puede cambiar sin tocar la instrumentación:
BackendCuándo usarloVentajaLimitaciónJaegerSelf-hosted, equipos que quieren control totalOpen source, proyecto CNCF, UI maduraRequiere operar el storage backend (Cassandra, Elasticsearch)Grafana TempoEquipos que ya usan Grafana, Prometheus o LokiStorage en object storage tipo S3, barato a escalaBúsqueda por atributos limitada sin GrafanaZipkinSetups simples, un solo servicio o pocosLiviano, fácil de levantarMenos activo que Jaeger, features más básicasSaaS (Datadog, New Relic, Honeycomb)Equipos sin capacidad de operar infraestructura propiaCero mantenimiento, alertas y dashboards listosCosto recurrente por volumen de datos ingeridos
La instrumentación con OpenTelemetry es la misma en los cuatro casos: solo cambia la URL del exporter en la configuración del Collector.

Profundizando: sampling, contexto y semantic conventions

El sampling decide qué porcentaje de trazas se conserva. Hay dos estrategias principales: head-based, donde la decisión se toma al iniciar la traza, antes de saber si terminará en error, y tail-based, donde el Collector espera a que la traza termine y decide en base al resultado completo, por ejemplo conservando siempre las que tuvieron un error o una latencia por encima de un umbral. El sampling tail-based necesita un Collector centralizado que agregue todos los spans de una traza antes de decidir.

💭 Clave: El contexto de una traza viaja como una cabecera HTTP normal (traceparent). No hace falta un protocolo especial: cualquier proxy o gateway que no toque las cabeceras deja pasar la traza intacta sin configuración adicional.

La jerarquía de spans dentro de una traza forma un árbol, no una lista plana. Cada span conoce a su padre directo, y el backend reconstruye el árbol completo a partir de esas relaciones:

flowchart TD
S1["Span raiz: POST /checkout"] --> S2["Span: validar-stock"]
S1 --> S3["Span: cobrar-tarjeta"]
S3 --> S4["Span: consulta-db"]
S1 --> S5["Span: enviar-email"]
Enter fullscreen mode Exit fullscreen mode

Otro concepto avanzado son las semantic conventions: un catálogo de nombres estándar para atributos comunes (http.method, db.system, rpc.service), definido por el proyecto para que dos SDKs distintos, en dos lenguajes distintos, nombren el mismo dato de la misma forma. Sin esa convención, cada equipo termina inventando su propio esquema de atributos, y las queries de un dashboard dejan de funcionar en cuanto un servicio nuevo usa un nombre distinto para lo mismo.

Por último, el Collector no es solo un proxy: soporta processors que transforman datos en tránsito, como redactar atributos con información sensible (números de tarjeta, emails) antes de que lleguen al backend, o agregar atributos comunes a todos los spans que pasan por él, como región o versión de deploy, sin tocar el código de cada servicio.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: levantá el Collector local con docker run -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one:latest e instrumentá un único endpoint de un proyecto que ya tengas corriendo para ver tu primera traza real en la UI de Jaeger.

Preguntas frecuentes

¿OpenTelemetry reemplaza a Jaeger o Prometheus?

No. OpenTelemetry genera y transporta la telemetría; Jaeger, Prometheus, Tempo o un SaaS son los backends que la almacenan y visualizan. Son piezas complementarias, no competidoras.

¿Cuánto overhead agrega instrumentar una app?

Depende del volumen de spans y del sampler configurado. Para medirlo en tu caso específico, comparás el percentil 99 de latencia del endpoint con y sin el SDK activo, usando la misma carga de tráfico en ambas corridas.

¿Funciona con lenguajes distintos a JavaScript?

Sí. Hay SDKs oficiales para Python, Go, Java, .NET, Ruby, PHP y más, todos compatibles con el mismo protocolo OTLP, lo que permite mezclar servicios en distintos lenguajes dentro de una sola traza.

¿Qué diferencia hay entre una traza y un log?

La traza muestra el camino completo de un request específico con tiempos por etapa; el log es un evento puntual con contexto arbitrario. OpenTelemetry los correlaciona compartiendo el mismo trace-id, pero son señales distintas con propósitos distintos.

¿Necesito un Collector o puedo exportar directo a un backend?

Es posible exportar directo desde el SDK a algunos backends, pero el Collector agrega una capa de desacople: permite cambiar de backend, aplicar sampling centralizado y procesar datos en tránsito sin tocar el código de cada servicio.

¿OpenTelemetry es gratis?

El proyecto y sus SDKs son open source y gratuitos, bajo la Apache License 2.0. El costo, si existe, viene del backend elegido para almacenar y visualizar los datos, no de la instrumentación en sí.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Top comments (0)