4. Tu primer trace en Jaeger

Por: Artiko
jaegertracingopentelemetrynodeprimer-trace

4. Tu primer trace en Jaeger

Vamos a generar el primer trace visible en la UI. Usaremos Node.js + Express + OpenTelemetry porque es el stack más rápido para un hello world. Los conceptos aplican igual a Python, Go, Java, .NET, etc.

Pre-requisito: tener Jaeger corriendo en http://localhost:16686/ con OTLP en puerto 4317. Si no lo tenés, capítulo 3.


Setup del proyecto

mkdir hello-jaeger && cd hello-jaeger
npm init -y
npm install express \
  @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

Cuatro paquetes hacen el trabajo:


El archivo de instrumentación

Creá tracing.js:

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'hello-jaeger',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'development',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'http://localhost:4317',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
console.log('Tracing started');

process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((err) => console.error(err))
    .finally(() => process.exit(0));
});

Tres puntos críticos:

  1. SERVICE_NAME: este nombre aparece en Jaeger. Si pones algo genérico como “api” vas a sufrir cuando tengas 10 servicios llamados igual.
  2. URL de OTLP: apuntá al puerto 4317 (gRPC). Si Jaeger está en otro host, ajustá.
  3. auto-instrumentations: con esta línea sola, queda instrumentado HTTP, Express, MySQL, Redis, gRPC, fs, dns, net y más. Sin escribir código manual.

La aplicación

app.js:

const express = require('express');
const app = express();

app.get('/hola/:nombre', async (req, res) => {
  // Simulamos un poco de trabajo
  await new Promise((r) => setTimeout(r, 50));
  res.json({ saludo: `Hola ${req.params.nombre}` });
});

app.get('/lento', async (req, res) => {
  await new Promise((r) => setTimeout(r, 800));
  res.json({ ok: true });
});

app.listen(3000, () => console.log('App en http://localhost:3000'));

Arrancar la app con tracing

La clave es cargar tracing antes de cualquier otro require:

node --require ./tracing.js app.js

Salida esperada:

Tracing started
App en http://localhost:3000

Generar trazas

curl http://localhost:3000/hola/jaeger
# {"saludo":"Hola jaeger"}

curl http://localhost:3000/lento
# {"ok":true}

# Repetí varias veces para tener material
for i in 1 2 3 4 5; do
  curl -s http://localhost:3000/hola/$i > /dev/null
  curl -s http://localhost:3000/lento > /dev/null
done

Ver las trazas en la UI

  1. Abrí http://localhost:16686/
  2. En el dropdown Service, seleccioná hello-jaeger.
  3. Click Find Traces.

Vas a ver una lista de trazas con su duración:

GET /hola/:nombre   54ms    1 span
GET /hola/:nombre   51ms    1 span
GET /lento          820ms   1 span
GET /lento          815ms   1 span

Click en una traza de /lento y vas a ver el timeline.


Lo que está pasando bajo el capó

sequenceDiagram
    participant C as curl
    participant E as Express handler
    participant O as OTel SDK
    participant J as Jaeger Collector
    C->>E: GET /lento
    E->>O: span "GET /lento" start
    Note over E: setTimeout 800ms
    E->>O: span end (status=OK)
    O->>O: batch (espera N spans o T ms)
    O->>J: OTLP gRPC export
    J-->>O: ack
    E-->>C: 200 OK

Tres cosas pasaron sin que escribieras código:

  1. La auto-instrumentación de Express creó automáticamente un span por cada request.
  2. El SDK los agrupó en batches y los exportó vía gRPC.
  3. Jaeger los recibió, los almacenó (en memoria) y los expuso en la UI.

¿Por qué solo 1 span?

Porque tu app no llama a nada más. Si agregás una llamada HTTP a otra API o una query a una DB, vas a ver más spans dentro del mismo trace:

const { default: axios } = require('axios');

app.get('/dependencias', async (req, res) => {
  const r1 = await axios.get('https://httpbin.org/delay/1');
  const r2 = await axios.get('https://httpbin.org/uuid');
  res.json({ uuid: r2.data.uuid });
});

Con esto, cada request a /dependencias te va a generar 3 spans:

Y vas a ver cómo se anidan en el timeline.


Variables de entorno alternativas

En lugar de hardcodear cosas en tracing.js, OpenTelemetry respeta variables de entorno:

export OTEL_SERVICE_NAME=hello-jaeger
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_TRACES_EXPORTER=otlp
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=dev,service.version=1.0.0"

node --require ./tracing.js app.js

Esto es lo que vas a hacer en producción: la imagen de la app es la misma, los valores los inyecta Kubernetes/docker-compose.


Errores comunes

”Connection refused” al exportar

Jaeger no está corriendo, o no está escuchando en 4317. Verificá docker ps y los logs.

”ECONNRESET”

A veces pasa al apagar Jaeger. La app retiene spans en buffer e intenta reenviar; el SDK eventualmente los descarta.

Las trazas no aparecen aunque la app dice que las exporta


¿Qué viene?

Ya tenés trazas reales. En el próximo capítulo abrimos la caja: qué hay dentro de Jaeger, cómo fluye la información del SDK al storage, y por qué all-in-one no sirve en producción.