6. Instrumentación con OpenTelemetry
6. Instrumentación con OpenTelemetry
Los SDKs propios de Jaeger (jaeger-client-*) están deprecados desde 2022. La instrumentación moderna se hace con OpenTelemetry, que es vendor-agnostic: los mismos SDKs sirven para Jaeger, Tempo, Zipkin, Datadog, Honeycomb, etc.
Dos formas de instrumentar
flowchart LR
A[Auto-instrumentación] -->|"libraries comunes\nya instrumentadas"| O[Trazas]
M[Manual] -->|"código propio\ncustom spans"| O
O --> J[Jaeger]
Auto-instrumentación
Detecta automáticamente librerías populares y les agrega instrumentación sin que escribas código.
Cubre típicamente:
- HTTP servers: Express, Fastify, Koa, Flask, FastAPI, gin, http.HandleFunc
- HTTP clients: fetch, axios, undici, requests, http.NewRequest
- Bases de datos: pg, mysql2, mongodb, redis, sqlalchemy, gorm
- RPC: gRPC server/client
- Mensajería: Kafka, RabbitMQ, AWS SQS
- Serverless: AWS Lambda, GCP Functions
Instrumentación manual
Escribís spans para tu lógica de negocio. Para cosas como:
- Una función crítica que querés medir.
- Un loop que procesa N items y querés ver cada item.
- Una operación que tarda mucho y querés desglosar.
Regla: empezá con auto-instrumentación. Sumá manual donde la auto no llega.
Auto-instrumentación por lenguaje
Node.js
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({ url: 'http://jaeger:4317' }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Arrancás con node --require ./tracing.js app.js. Listo.
Python
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap --action=install
Arrancás con opentelemetry-instrument python app.py. La CLI detecta tus dependencias y agrega los wrappers.
export OTEL_SERVICE_NAME=mi-app
export OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
opentelemetry-instrument python app.py
Go
Go no tiene auto-instrumentación tipo monkey-patching: se escriben las líneas a mano (Go es estricto con eso). Pero las librerías oficiales ya vienen instrumentadas:
import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
router := gin.New()
router.Use(otelgin.Middleware("mi-app"))
Java / .NET
Tienen auto-instrumentación tipo “Java agent” — un JAR que se inyecta sin tocar código:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=mi-app \
-Dotel.exporter.otlp.endpoint=http://jaeger:4317 \
-jar mi-app.jar
Spans manuales
Para tu lógica de negocio:
Node.js
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('mi-app');
async function procesarOrden(orderId) {
return tracer.startActiveSpan('procesar_orden', async (span) => {
try {
span.setAttribute('order.id', orderId);
const validacion = await validar(orderId);
span.addEvent('validacion_completada');
const cobro = await cobrar(orderId);
span.setAttribute('order.amount', cobro.amount);
span.setStatus({ code: SpanStatusCode.OK });
return { ok: true };
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
}
Python
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def procesar_orden(order_id):
with tracer.start_as_current_span("procesar_orden") as span:
span.set_attribute("order.id", order_id)
try:
validar(order_id)
span.add_event("validacion_completada")
cobrar(order_id)
except Exception as e:
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
raise
Go
import "go.opentelemetry.io/otel"
tracer := otel.Tracer("mi-app")
func procesarOrden(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "procesar_orden")
defer span.End()
span.SetAttributes(attribute.String("order.id", orderID))
if err := validar(ctx, orderID); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
return cobrar(ctx, orderID)
}
Atributos: usá los nombres oficiales
OpenTelemetry define Semantic Conventions — nombres estándar para atributos comunes. Usalos.
| Buenos | Malos |
|---|---|
http.request.method | httpMethod, method, http_verb |
http.response.status_code | status, http_status |
db.system.name | database, db_type |
db.query.text | sql, query |
messaging.system | queue_type |
user.id | userId, user, uid |
Ventajas de los nombres oficiales:
- La UI de Jaeger los reconoce y los muestra de manera especial.
- Otras herramientas (Grafana, Datadog) los entienden si migrás.
- Las queries de búsqueda funcionan igual entre proyectos.
Resource attributes
Atributos del proceso, no del span individual:
const { Resource } = require('@opentelemetry/resources');
new NodeSDK({
resource: new Resource({
'service.name': 'order-service',
'service.version': '2.4.1',
'service.namespace': 'commerce',
'deployment.environment': 'production',
'host.name': process.env.HOSTNAME,
'k8s.pod.name': process.env.POD_NAME,
'k8s.namespace.name': process.env.NAMESPACE,
}),
});
Vía environment es más portable:
export OTEL_RESOURCE_ATTRIBUTES="service.namespace=commerce,deployment.environment=prod"
El batch processor
Por defecto, el SDK no manda spans uno por uno. Los agrupa con el BatchSpanProcessor:
new BatchSpanProcessor(exporter, {
maxQueueSize: 2048, // descarta si se llena
scheduledDelayMillis: 5000, // flush cada 5s
exportTimeoutMillis: 30000, // timeout de export
maxExportBatchSize: 512, // tamaño máximo del batch
});
| Parámetro | Default | Cuándo tocar |
|---|---|---|
maxQueueSize | 2048 | Subí si tenés volumen alto y memoria de sobra |
scheduledDelayMillis | 5000 | Bajá a 500-1000ms en desarrollo para ver trazas rápido |
maxExportBatchSize | 512 | Subí en producción a 1024-2048 para menos overhead |
Para serverless / scripts cortos: no usés batch, usá SimpleSpanProcessor para asegurar que los spans se exporten antes de terminar.
Exporters: elegir el correcto
| Exporter | Cuándo |
|---|---|
OTLPTraceExporter (gRPC) | Default. Mejor performance, binario |
OTLPHttpExporter | Cuando hay restricciones de gRPC en la red |
JaegerExporter (Thrift) | Deprecated. Solo para legacy |
ConsoleSpanExporter | Debug local — imprime spans a stdout |
Para debug local mientras desarrollás, podés usar dos exporters al mismo tiempo:
new NodeSDK({
spanProcessors: [
new BatchSpanProcessor(otlpExporter), // a Jaeger
new SimpleSpanProcessor(consoleExporter), // a stdout
],
});
Cardinalidad: el error que se paga caro
Cardinalidad = cantidad de valores únicos para un atributo.
Bueno (baja cardinalidad):
http.method = GET / POST / PUT / DELETE
http.route = /api/users/:id
db.system.name = postgresql
Malo (alta cardinalidad):
user.id = 9374-2841-... (millones)
http.url = /api/users/9374 (miles de millones)
request_body = {...}
user.id no es necesariamente un problema: si lo pones como atributo de span, es OK. El problema es ponerlo como nombre del span (name = "GET /api/users/9374"). Eso explota la cardinalidad operacional y te tira el storage.
Regla: el name del span debe ser una plantilla, no una URL concreta:
- Mal:
GET /api/users/9374 - Bien:
GET /api/users/:id
Las auto-instrumentaciones serias ya hacen esto bien. En manual hay que cuidarlo.
Errores y excepciones
try {
await operacionQuePuedeFallar();
} catch (err) {
span.recordException(err); // adjunta stack trace como evento
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message,
});
throw err;
}
recordException es una helper que crea un evento con atributos exception.type, exception.message, exception.stacktrace.
No olvidar setStatus(ERROR): por defecto los spans terminan con status UNSET, que no se distingue visualmente de OK en la UI.
¿Qué viene?
Sabés instrumentar. Pero un span solo no es un trace distribuido — falta que el contexto viaje entre servicios. En el próximo capítulo cubrimos cómo se propaga el trace_id entre HTTP, gRPC, Kafka y otros transportes.