Cristhian Villegas
DevOps12 min read1 views

OpenTelemetry: El Estándar de Observabilidad en 2026 — Guía Completa

¿Qué es la observabilidad y por qué importa?

La observabilidad es la capacidad de entender el estado interno de un sistema a partir de sus salidas externas. A diferencia del monitoreo tradicional, que responde preguntas predefinidas ("¿está arriba el servidor?"), la observabilidad te permite hacer preguntas que nunca anticipaste: "¿por qué este request específico tardó 12 segundos solo para usuarios de Colombia?"

Los tres pilares de la observabilidad son:

  • Logs — Registros textuales de eventos discretos. Útiles para debugging puntual.
  • Métricas — Valores numéricos agregados en el tiempo (latencia p99, tasa de errores, uso de CPU).
  • Trazas (Traces) — El recorrido completo de una request a través de múltiples servicios.

El problema histórico es que cada pilar usaba herramientas distintas con formatos incompatibles. OpenTelemetry resuelve exactamente esto: un estándar unificado para los tres pilares.

Dato clave: OpenTelemetry es el segundo proyecto más activo de la CNCF (Cloud Native Computing Foundation), solo después de Kubernetes. Más de 1,000 contribuidores activos lo mantienen.

¿Qué es OpenTelemetry (OTel)?

OpenTelemetry es un framework de observabilidad open source y vendor-neutral que proporciona APIs, SDKs y herramientas para generar, recopilar y exportar datos de telemetría (trazas, métricas y logs). Nació en 2019 de la fusión de dos proyectos: OpenTracing y OpenCensus.

La propuesta de valor es clara: instrumentas tu código una sola vez con OTel y puedes enviar los datos a cualquier backend — Jaeger, Grafana Tempo, Datadog, New Relic, Prometheus, Elastic — sin cambiar tu código.

Sala de servidores con infraestructura de monitoreo

Componentes principales de OTel

  • API — Define las interfaces para instrumentación (Tracer, Meter, Logger). Es estable y segura para librerías.
  • SDK — Implementación de la API con procesamiento, sampling y exportación.
  • OTel Collector — Agente independiente que recibe, procesa y exporta telemetría.
  • Exporters — Plugins que envían datos al backend final (OTLP, Jaeger, Prometheus, etc.).
  • Auto-instrumentación — Agentes que instrumentan frameworks populares sin cambiar código.

Arquitectura del OpenTelemetry Collector

El OTel Collector es el corazón de cualquier pipeline de observabilidad con OpenTelemetry. Actúa como un proxy inteligente entre tus aplicaciones y los backends de almacenamiento.

Su arquitectura se basa en tres componentes conectados en pipeline:

  1. Receivers — Reciben datos en múltiples formatos (OTLP, Jaeger, Zipkin, Prometheus).
  2. Processors — Transforman, filtran y enriquecen los datos (batch, memory limiter, attributes).
  3. Exporters — Envían los datos al destino final (OTLP, Prometheus, Loki, etc.).
yaml
1# otel-collector-config.yaml
2receivers:
3  otlp:
4    protocols:
5      grpc:
6        endpoint: 0.0.0.0:4317
7      http:
8        endpoint: 0.0.0.0:4318
9
10processors:
11  batch:
12    timeout: 5s
13    send_batch_size: 1024
14  memory_limiter:
15    check_interval: 1s
16    limit_mib: 512
17    spike_limit_mib: 128
18  attributes:
19    actions:
20      - key: environment
21        value: production
22        action: upsert
23
24exporters:
25  otlp/tempo:
26    endpoint: tempo:4317
27    tls:
28      insecure: true
29  prometheus:
30    endpoint: 0.0.0.0:8889
31    namespace: myapp
32  loki:
33    endpoint: http://loki:3100/loki/api/v1/push
34
35service:
36  pipelines:
37    traces:
38      receivers: [otlp]
39      processors: [memory_limiter, batch, attributes]
40      exporters: [otlp/tempo]
41    metrics:
42      receivers: [otlp]
43      processors: [memory_limiter, batch]
44      exporters: [prometheus]
45    logs:
46      receivers: [otlp]
47      processors: [memory_limiter, batch]
48      exporters: [loki]
Tip: Siempre coloca el processor memory_limiter primero en la cadena. Esto evita que un pico de telemetría haga crash al Collector por falta de memoria.

Auto-instrumentación en Java (Spring Boot)

Una de las grandes ventajas de OTel es la auto-instrumentación: puedes obtener trazas completas de tu aplicación Spring Boot sin escribir una sola línea de código de instrumentación. Solo necesitas adjuntar el agente Java.

dockerfile
1# Dockerfile para Spring Boot con OTel Agent
2FROM eclipse-temurin:21-jre-alpine
3
4WORKDIR /app
5
6# Descargar el agente de OpenTelemetry
7ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /app/otel-agent.jar
8
9COPY target/my-api-0.0.1-SNAPSHOT.jar /app/app.jar
10
11ENV OTEL_SERVICE_NAME=my-spring-api
12ENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
13ENV OTEL_EXPORTER_OTLP_PROTOCOL=grpc
14ENV OTEL_TRACES_SAMPLER=parentbased_traceidratio
15ENV OTEL_TRACES_SAMPLER_ARG=0.5
16ENV OTEL_METRICS_EXPORTER=otlp
17ENV OTEL_LOGS_EXPORTER=otlp
18ENV OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.2.0
19
20EXPOSE 8080
21
22ENTRYPOINT ["java", "-javaagent:/app/otel-agent.jar", "-jar", "/app/app.jar"]

Con esto, el agente automáticamente captura:

  • Todas las requests HTTP entrantes (Spring MVC / WebFlux)
  • Llamadas a bases de datos (JDBC, R2DBC)
  • Requests HTTP salientes (RestTemplate, WebClient, HttpClient)
  • Mensajería (Kafka, RabbitMQ, SQS)
  • Caché (Redis, Caffeine)
Cuidado: El sampler parentbased_traceidratio con arg 0.5 solo muestrea el 50% de las trazas raíz. En producción esto reduce costos significativamente, pero asegúrate de que trazas de errores siempre se capturen configurando un sampler personalizado.

Instrumentación manual en TypeScript/Node.js

Para aplicaciones Node.js, la auto-instrumentación cubre Express, Fastify, bases de datos y más. Pero muchas veces necesitas instrumentación manual para capturar lógica de negocio específica.

typescript
1// src/tracing.ts — Configuración de OpenTelemetry para Node.js
2import { NodeSDK } from '@opentelemetry/sdk-node';
3import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
4import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
5import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
6import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
7import { Resource } from '@opentelemetry/resources';
8import {
9  ATTR_SERVICE_NAME,
10  ATTR_SERVICE_VERSION,
11} from '@opentelemetry/semantic-conventions';
12
13const sdk = new NodeSDK({
14  resource: new Resource({
15    [ATTR_SERVICE_NAME]: 'order-service',
16    [ATTR_SERVICE_VERSION]: '2.1.0',
17    'deployment.environment': process.env.NODE_ENV || 'development',
18  }),
19  traceExporter: new OTLPTraceExporter({
20    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
21  }),
22  metricReader: new PeriodicExportingMetricReader({
23    exporter: new OTLPMetricExporter({
24      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
25    }),
26    exportIntervalMillis: 15000,
27  }),
28  instrumentations: [
29    getNodeAutoInstrumentations({
30      '@opentelemetry/instrumentation-fs': { enabled: false },
31    }),
32  ],
33});
34
35sdk.start();
36console.log('OpenTelemetry SDK iniciado');
37
38process.on('SIGTERM', () => {
39  sdk.shutdown().then(() => console.log('OTel SDK apagado'));
40});

Ahora veamos cómo agregar spans personalizados para lógica de negocio:

typescript
1// src/services/order.service.ts
2import { trace, SpanStatusCode, metrics } from '@opentelemetry/api';
3
4const tracer = trace.getTracer('order-service', '2.1.0');
5const meter = metrics.getMeter('order-service', '2.1.0');
6
7// Métricas personalizadas
8const orderCounter = meter.createCounter('orders.created', {
9  description: 'Total de órdenes creadas',
10});
11const orderDuration = meter.createHistogram('orders.processing_duration_ms', {
12  description: 'Duración de procesamiento de orden en ms',
13  unit: 'ms',
14});
15
16export async function createOrder(userId: string, items: CartItem[]) {
17  return tracer.startActiveSpan('createOrder', async (span) => {
18    const start = Date.now();
19
20    try {
21      span.setAttribute('user.id', userId);
22      span.setAttribute('order.item_count', items.length);
23
24      // Validar inventario
25      const available = await tracer.startActiveSpan('validateInventory', async (child) => {
26        const result = await inventoryService.checkAvailability(items);
27        child.setAttribute('inventory.all_available', result.allAvailable);
28        child.end();
29        return result;
30      });
31
32      if (!available.allAvailable) {
33        span.setStatus({ code: SpanStatusCode.ERROR, message: 'Inventario insuficiente' });
34        throw new InsufficientInventoryError(available.missing);
35      }
36
37      // Procesar pago
38      const payment = await tracer.startActiveSpan('processPayment', async (child) => {
39        const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
40        child.setAttribute('payment.amount', total);
41        child.setAttribute('payment.currency', 'MXN');
42        const result = await paymentService.charge(userId, total);
43        child.end();
44        return result;
45      });
46
47      span.setAttribute('order.payment_id', payment.id);
48      orderCounter.add(1, { status: 'success', region: 'mx' });
49
50      return { orderId: payment.orderId, status: 'confirmed' };
51
52    } catch (error) {
53      span.setStatus({ code: SpanStatusCode.ERROR });
54      span.recordException(error as Error);
55      orderCounter.add(1, { status: 'error', region: 'mx' });
56      throw error;
57
58    } finally {
59      orderDuration.record(Date.now() - start);
60      span.end();
61    }
62  });
63}

Propagación de contexto (W3C Trace Context)

La propagación de contexto es lo que permite que una traza cruce fronteras entre servicios. Cuando el Servicio A llama al Servicio B, necesita pasar el trace_id y span_id para que B cree spans hijos de A.

OpenTelemetry usa el estándar W3C Trace Context por defecto, que define dos headers HTTP:

  • traceparent — Contiene version, trace-id, parent-id y trace-flags
  • tracestate — Metadata adicional vendor-specific

Ejemplo de un header traceparent:

bash
1traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
2#             v  trace-id (32 hex)              parent-id (16 hex) flags

Pantalla de monitoreo con gráficas de observabilidad

Baggage API

Además del contexto de traza, OTel proporciona la Baggage API para propagar datos de negocio entre servicios sin acoplarlos. Por ejemplo, propagar el tenant_id o user_tier para que todos los servicios downstream lo usen en sus métricas.

Precaución: El baggage se envía como headers HTTP en cada request. No almacenes datos sensibles (tokens, PII) ni datos grandes. Úsalo para metadatos ligeros como IDs de tenant o flags de feature.

OTel en Kubernetes: DaemonSet vs Sidecar

Cuando despliegas el OTel Collector en Kubernetes, tienes dos patrones principales:

Patrón DaemonSet

Un Collector por nodo. Todos los pods del nodo envían su telemetría al Collector local. Es más eficiente en recursos porque compartes un solo Collector entre muchos pods.

Patrón Sidecar

Un Collector por pod. Cada pod tiene su propio Collector como container sidecar. Ofrece mejor aislamiento pero consume más recursos.

yaml
1# kubernetes/otel-collector-daemonset.yaml
2apiVersion: apps/v1
3kind: DaemonSet
4metadata:
5  name: otel-collector
6  namespace: observability
7spec:
8  selector:
9    matchLabels:
10      app: otel-collector
11  template:
12    metadata:
13      labels:
14        app: otel-collector
15    spec:
16      containers:
17        - name: collector
18          image: otel/opentelemetry-collector-contrib:0.97.0
19          ports:
20            - containerPort: 4317  # gRPC OTLP
21              hostPort: 4317
22            - containerPort: 4318  # HTTP OTLP
23              hostPort: 4318
24            - containerPort: 8889  # Prometheus metrics
25          volumeMounts:
26            - name: config
27              mountPath: /etc/otelcol-contrib
28          resources:
29            requests:
30              cpu: 200m
31              memory: 256Mi
32            limits:
33              cpu: 500m
34              memory: 512Mi
35          livenessProbe:
36            httpGet:
37              path: /
38              port: 13133
39          readinessProbe:
40            httpGet:
41              path: /
42              port: 13133
43      volumes:
44        - name: config
45          configMap:
46            name: otel-collector-config
47---
48apiVersion: v1
49kind: Service
50metadata:
51  name: otel-collector
52  namespace: observability
53spec:
54  type: ClusterIP
55  selector:
56    app: otel-collector
57  ports:
58    - name: otlp-grpc
59      port: 4317
60    - name: otlp-http
61      port: 4318
62    - name: metrics
63      port: 8889
Recomendación: Para la mayoría de los equipos, el patrón DaemonSet es la mejor opción inicial. Usa Sidecar solo cuando necesites configuraciones diferentes por servicio o aislamiento estricto de multi-tenancy.

Comparación con soluciones propietarias

¿Por qué elegir OTel sobre soluciones como Datadog, New Relic o Dynatrace? Aquí una comparación honesta:

AspectoOpenTelemetryDatadog / New Relic
CostoGratuito (open source)$15-35/host/mes + ingesta
Vendor lock-inNinguno — cambia backend sin cambiar códigoAlto — SDKs propietarios
Setup inicialMayor complejidad — debes gestionar Collector y backendsRápido — SaaS listo para usar
DashboardsTú los construyes (Grafana)Predefinidos y potentes
AlertasConfiguras con Alertmanager o GrafanaIntegradas con ML
SoporteComunidad + docsSoporte empresarial 24/7
EscalaSin límites (tú gestionas infra)Costos crecen linealmente
Nota: Muchas empresas usan un enfoque híbrido: instrumentan con OTel (estándar abierto) pero envían los datos a un backend SaaS como Grafana Cloud o Datadog. Así obtienen lo mejor de ambos mundos: estándar abierto + dashboards potentes.

Métricas personalizadas con OTel

OTel soporta tres tipos de instrumentos de métricas:

  • Counter — Valor que solo incrementa (requests totales, errores totales)
  • Histogram — Distribución de valores (latencia, tamaño de payload)
  • Gauge — Valor puntual que sube y baja (conexiones activas, temperatura)
java
1// MetricsConfig.java — Métricas personalizadas en Spring Boot
2import io.opentelemetry.api.GlobalOpenTelemetry;
3import io.opentelemetry.api.metrics.LongCounter;
4import io.opentelemetry.api.metrics.DoubleHistogram;
5import io.opentelemetry.api.metrics.Meter;
6import io.opentelemetry.api.common.Attributes;
7import io.opentelemetry.api.common.AttributeKey;
8import org.springframework.stereotype.Component;
9
10@Component
11public class OrderMetrics {
12
13    private final LongCounter ordersCreated;
14    private final DoubleHistogram orderProcessingTime;
15    private final LongCounter paymentFailures;
16
17    public OrderMetrics() {
18        Meter meter = GlobalOpenTelemetry.getMeter("com.myapp.orders", "1.0.0");
19
20        this.ordersCreated = meter.counterBuilder("app.orders.created")
21            .setDescription("Número total de órdenes creadas")
22            .setUnit("{orders}")
23            .build();
24
25        this.orderProcessingTime = meter.histogramBuilder("app.orders.processing_time")
26            .setDescription("Tiempo de procesamiento de orden")
27            .setUnit("ms")
28            .build();
29
30        this.paymentFailures = meter.counterBuilder("app.payments.failures")
31            .setDescription("Número total de fallos de pago")
32            .setUnit("{failures}")
33            .build();
34    }
35
36    public void recordOrderCreated(String region, String tier) {
37        ordersCreated.add(1, Attributes.of(
38            AttributeKey.stringKey("region"), region,
39            AttributeKey.stringKey("customer.tier"), tier
40        ));
41    }
42
43    public void recordProcessingTime(long durationMs) {
44        orderProcessingTime.record(durationMs);
45    }
46
47    public void recordPaymentFailure(String reason) {
48        paymentFailures.add(1, Attributes.of(
49            AttributeKey.stringKey("failure.reason"), reason
50        ));
51    }
52}

Mejores prácticas para producción

Después de implementar OTel en múltiples proyectos, estas son las prácticas que marcan la diferencia:

  1. Usa sampling inteligente — No captures el 100% de las trazas. Usa parentbased_traceidratio con 10-50% para tráfico normal, y 100% para errores.
  2. Nombra tus spans con convenciones semánticas — Usa los Semantic Conventions de OTel: http.server.request, db.query, etc.
  3. Agrega resource attributes — Siempre incluye service.name, service.version, deployment.environment.
  4. Configura el memory_limiter — Evita que el Collector se caiga por picos de telemetría.
  5. Usa el batch processor — Agrupa spans antes de exportar para reducir overhead de red.
  6. Correlaciona logs con trazas — Inyecta trace_id y span_id en tus logs estructurados.
  7. Monitorea al Collector mismo — El Collector expone métricas propias en /metrics. Dashboardea dropped spans, queue size, etc.
  8. Separa el Collector en tiers — Un Collector por nodo (agent) que reenvía a un Collector central (gateway) para procesamiento pesado.
Tip final: Comienza con auto-instrumentación y trazas. Una vez que tengas visibilidad básica, agrega métricas personalizadas para KPIs de negocio y después migra tus logs a OTel. No intentes implementar los tres pilares al mismo tiempo.
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles