6. Instrumentación con OpenTelemetry

Por: Artiko
jaegeropentelemetryinstrumentacionauto-instrumentation

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:

Instrumentación manual

Escribís spans para tu lógica de negocio. Para cosas como:

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.

BuenosMalos
http.request.methodhttpMethod, method, http_verb
http.response.status_codestatus, http_status
db.system.namedatabase, db_type
db.query.textsql, query
messaging.systemqueue_type
user.iduserId, user, uid

Ventajas de los nombres oficiales:


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ámetroDefaultCuándo tocar
maxQueueSize2048Subí si tenés volumen alto y memoria de sobra
scheduledDelayMillis5000Bajá a 500-1000ms en desarrollo para ver trazas rápido
maxExportBatchSize512Subí 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

ExporterCuándo
OTLPTraceExporter (gRPC)Default. Mejor performance, binario
OTLPHttpExporterCuando hay restricciones de gRPC en la red
JaegerExporter (Thrift)Deprecated. Solo para legacy
ConsoleSpanExporterDebug 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:

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.