Jaeger es una plataforma de rastreo distribuido: recibe las trazas que generan tus aplicaciones y te permite seguir una petición a través de todos los servicios que atraviesa, con el tiempo que pasa en cada uno. Desde la versión 2, Jaeger está construido sobre OpenTelemetry Collector y recibe los datos directamente por OTLP, el protocolo estándar de OpenTelemetry. En este tutorial desplegarás Jaeger v2 con Docker Compose en Ubuntu 24.04, con almacenamiento persistente en disco, e instrumentarás una pequeña aplicación Python para ver sus trazas en la interfaz web.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
- Un usuario no root con privilegios
sudo. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker (consulta la guía "Cómo instalar Docker en Ubuntu 24.04, Debian 12 y Rocky 9").
- Acceso SSH desde tu equipo, para abrir la interfaz de Jaeger mediante un túnel.
Conceptos básicos
Antes de empezar conviene tener claros tres términos:
- Span: una operación con nombre, inicio y duración, por ejemplo "GET /pedidos" o "SELECT en la tabla users". Puede llevar atributos (método HTTP, código de estado, errores).
- Traza: el conjunto de spans de una misma petición, enlazados por un identificador común y organizados en árbol.
- OTLP: el protocolo con el que los SDK de OpenTelemetry envían las trazas. Jaeger lo acepta por gRPC en el puerto
4317y por HTTP en el4318.
Jaeger v2 sustituye a los antiguos componentes separados de la versión 1 (agent, collector, query). Un único binario hace de receptor OTLP, almacenamiento e interfaz web. El antiguo jaeger-agent ya no existe: las aplicaciones envían las trazas por OTLP directamente a Jaeger o a un OpenTelemetry Collector.
Paso 1: Preparar el directorio del proyecto
Crea un directorio para Jaeger con un subdirectorio data donde se guardarán las trazas:
mkdir -p ~/jaeger/data
cd ~/jaeger
La imagen oficial de Jaeger se ejecuta con el usuario sin privilegios de UID 10001, así que ese usuario debe poder escribir en data:
sudo chown -R 10001:10001 ~/jaeger/data
Paso 2: Configurar Jaeger con almacenamiento Badger
Sin configuración, la imagen de Jaeger guarda las trazas en memoria y se pierden al reiniciar el contenedor. Para un servidor de un solo nodo, Badger es la opción más sencilla: una base de datos embebida que escribe en disco sin servicios adicionales. Para volúmenes grandes o varios nodos, Jaeger admite Elasticsearch, OpenSearch o Cassandra.
La configuración de Jaeger v2 sigue el formato de OpenTelemetry Collector: receptores, procesadores, exportadores y extensiones, unidos en un pipeline. Crea el archivo:
nano config.yaml
service:
extensions: [jaeger_storage, jaeger_query]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter]
extensions:
jaeger_query:
storage:
traces: badger_store
http:
endpoint: 0.0.0.0:16686
jaeger_storage:
backends:
badger_store:
badger:
directories:
keys: /badger/keys
values: /badger/values
ephemeral: false
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
jaeger_storage_exporter:
trace_storage: badger_store
Qué hace cada bloque:
receivers.otlp: acepta trazas OTLP por gRPC y HTTP. Escucha en0.0.0.0dentro del contenedor; la exposición real hacia fuera la controla Docker en el paso siguiente.processors.batch: agrupa los spans antes de escribirlos, lo que reduce la carga sobre el almacenamiento.extensions.jaeger_storage: define un backend llamadobadger_storeque escribe en/badgerdentro del contenedor.extensions.jaeger_query: sirve la interfaz web y la API de consulta en el puerto16686, leyendo de ese mismo backend.exporters.jaeger_storage_exporter: escribe enbadger_storelas trazas que llegan por el pipeline.
Paso 3: Arrancar Jaeger con Docker Compose
Crea el archivo compose.yaml:
nano compose.yaml
services:
jaeger:
image: jaegertracing/jaeger:latest
container_name: jaeger
restart: unless-stopped
command: ["--config", "/jaeger/config.yaml"]
volumes:
- ./config.yaml:/jaeger/config.yaml:ro
- ./data:/badger
ports:
- "127.0.0.1:16686:16686"
- "127.0.0.1:4317:4317"
- "127.0.0.1:4318:4318"
Todos los puertos se publican solo en 127.0.0.1. Docker gestiona sus propias reglas de iptables y los puertos publicados en todas las interfaces se saltan UFW, así que de este modo ni la interfaz (que no tiene autenticación) ni los receptores OTLP quedan expuestos a Internet.
Consejo
latestapunta a la última versión 2.x. En producción, fija una versión concreta (por ejemplojaegertracing/jaeger:2.x.y, consulta las etiquetas en Docker Hub) para que una actualización no llegue sin que la hayas probado.
Arranca el contenedor:
docker compose up -d
Comprueba que está en marcha y revisa los logs:
docker compose ps
docker compose logs jaeger | tail -n 20
En los logs no debe haber líneas de nivel error, y deberías ver que se inician los receptores OTLP en 0.0.0.0:4317 y 0.0.0.0:4318 y el servidor HTTP de consulta en 0.0.0.0:16686. Comprueba además que Badger ha creado sus archivos:
sudo ls ~/jaeger/data
keys values
Por último, verifica que la API de consulta responde:
curl -s http://127.0.0.1:16686/api/services
{"data":[],"total":0,"limit":0,"offset":0,"errors":null}
La lista de servicios aún está vacía porque ninguna aplicación ha enviado trazas.
Paso 4: Instrumentar una aplicación Python con OpenTelemetry
Para generar trazas reales vas a crear una pequeña aplicación Flask y a instrumentarla con la autoinstrumentación de OpenTelemetry, que crea spans para cada petición HTTP sin tocar el código. Añadirás además un span manual para una operación interna.
Instala el soporte de entornos virtuales de Python:
sudo apt update
sudo apt install python3-venv
Crea el proyecto y un entorno virtual:
mkdir -p ~/trace-demo
cd ~/trace-demo
python3 -m venv venv
source venv/bin/activate
Instala Flask, la distribución de OpenTelemetry y el exportador OTLP:
pip install flask opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap detecta las librerías instaladas (en este caso Flask) e instala sus paquetes de instrumentación:
opentelemetry-bootstrap -a install
Crea la aplicación:
nano app.py
import random
import time
from flask import Flask
from opentelemetry import trace
app = Flask(__name__)
tracer = trace.get_tracer(__name__)
def calcular_precio(producto_id):
# Span manual para una operación interna
with tracer.start_as_current_span("calcular_precio") as span:
span.set_attribute("producto.id", producto_id)
time.sleep(random.uniform(0.02, 0.2))
return round(random.uniform(5, 50), 2)
@app.route("/producto/<int:producto_id>")
def producto(producto_id):
precio = calcular_precio(producto_id)
return {"id": producto_id, "precio": precio}
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
Ejecuta la aplicación con opentelemetry-instrument. Las variables de entorno indican el nombre del servicio, que se exporten solo trazas y que se envíen por OTLP/HTTP al puerto 4318 de Jaeger:
export OTEL_SERVICE_NAME=tienda-demo
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
opentelemetry-instrument python app.py
* Serving Flask app 'app'
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
Deja la aplicación en marcha. En otra sesión SSH, genera unas cuantas peticiones:
for i in $(seq 1 20); do curl -s "http://127.0.0.1:5000/producto/$i"; echo; done
Tras unos segundos (el procesador batch y el SDK envían los spans por lotes), comprueba que Jaeger ha registrado el servicio:
curl -s http://127.0.0.1:16686/api/services
{"data":["tienda-demo"],"total":1,"limit":0,"offset":0,"errors":null}
Paso 5: Explorar las trazas en la interfaz web
La interfaz de Jaeger solo escucha en localhost. Desde tu equipo local, abre un túnel SSH hacia el puerto 16686 del servidor, sustituyendo your_user y your_server_ip:
ssh -N -L 16686:127.0.0.1:16686 your_user@your_server_ip
Con el túnel abierto, visita http://localhost:16686 en el navegador:
- En Service, elige
tienda-demoy pulsa Find Traces. - El gráfico superior muestra la duración de cada traza en el tiempo. Los puntos más altos son las peticiones más lentas.
- Abre una traza. Verás un span raíz
GET /producto/<int:producto_id>creado por la instrumentación de Flask y, dentro de él, el spancalcular_preciocon el atributoproducto.id. - Pulsa sobre cada span para ver sus atributos: método HTTP, ruta, código de estado y la duración exacta.
Con varias aplicaciones instrumentadas que se llaman entre sí, la misma vista muestra el recorrido completo de una petición, y la pestaña System Architecture dibuja el grafo de dependencias entre servicios.
Paso 6: Controlar el volumen con muestreo
En producción no suele ser necesario guardar el 100 % de las trazas. El muestreo se configura en el SDK de cada aplicación, también con variables de entorno. Esta configuración guarda el 10 % de las trazas nuevas y respeta la decisión del servicio que inició la petición, para no romper trazas a medias:
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
Reinicia la aplicación con estas variables y lanza de nuevo las 20 peticiones: en Jaeger aparecerán aproximadamente dos trazas nuevas en lugar de veinte.
Enviar trazas desde otros servidores
Si tus aplicaciones se ejecutan en otros servidores, tienen que alcanzar los puertos OTLP de Jaeger. Las opciones más seguras son:
- Publicar
4317y4318en la IP privada del servidor (por ejemplo"10.0.0.5:4318:4318"encompose.yaml) y usar esa red privada entre servidores. - Ejecutar un OpenTelemetry Collector en cada servidor que reenvíe las trazas a Jaeger por una conexión cifrada.
Evita publicar los puertos OTLP en la IP pública: no tienen autenticación y cualquiera podría enviar datos a tu Jaeger.
Solución de problemas
El contenedor se reinicia en bucle con un error de permisos en /badger: el directorio data no pertenece al UID del contenedor. Ejecuta sudo chown -R 10001:10001 ~/jaeger/data y docker compose up -d.
El contenedor no arranca y el log indica un error en la configuración: Jaeger valida config.yaml al iniciar y rechaza claves desconocidas o mal indentadas. Revisa la línea que indica docker compose logs jaeger y recuerda que YAML no admite tabuladores.
tienda-demo no aparece en Jaeger: comprueba que las variables OTEL_* están exportadas en la misma sesión donde ejecutas opentelemetry-instrument, que el endpoint es http://127.0.0.1:4318 sin ruta adicional y que Jaeger está en marcha (docker compose ps en ~/jaeger).
channel 2: open failed al abrir el túnel SSH: Jaeger no está escuchando en 127.0.0.1:16686 del servidor. Comprueba los puertos publicados con docker compose ps.
Conclusión
Has desplegado Jaeger v2 con Docker Compose en Ubuntu 24.04, con las trazas persistidas en disco mediante Badger y los puertos limitados a localhost, y has instrumentado una aplicación Python con OpenTelemetry para ver sus spans automáticos y manuales. Como OpenTelemetry es un estándar, el mismo Jaeger sirve para aplicaciones en Go, Java, Node.js o .NET cambiando solo el SDK.
Como siguientes pasos puedes:
- Instrumentar el resto de tus servicios con el SDK de OpenTelemetry de su lenguaje y enviar las trazas al mismo Jaeger.
- Migrar el almacenamiento a OpenSearch o Elasticsearch cuando el volumen de trazas crezca o necesites alta disponibilidad.
- Correlacionar trazas con métricas y logs añadiendo Prometheus y Grafana a tu stack de observabilidad.
