4. Tu primer trace en Jaeger
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 puerto4317. 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:
sdk-node: el SDK que coordina todo.auto-instrumentations-node: detecta libraries (Express, http, fs) e instrumenta automáticamente.exporter-trace-otlp-grpc: exporta vía OTLP a Jaeger.semantic-conventions: nombres estándar para atributos.
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:
SERVICE_NAME: este nombre aparece en Jaeger. Si pones algo genérico como “api” vas a sufrir cuando tengas 10 servicios llamados igual.- URL de OTLP: apuntá al puerto
4317(gRPC). Si Jaeger está en otro host, ajustá. 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
- Abrí http://localhost:16686/
- En el dropdown Service, seleccioná
hello-jaeger. - 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:
- La auto-instrumentación de Express creó automáticamente un span por cada request.
- El SDK los agrupó en batches y los exportó vía gRPC.
- 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:
- 1 server:
GET /dependencias(parent) - 2 client:
GET https://httpbin.org/...(hijos)
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
- Esperá unos segundos: el batch processor agrupa spans y los manda cada N segundos o cuando llena buffer.
- Verificá que estás usando OTLP gRPC (puerto
4317) y no Thrift legacy. - En la UI: ¿estás filtrando “Last Hour”? Si tu reloj está mal sincronizado, las trazas pueden caer fuera del rango.
¿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.