🌐Desarrollo

Logs, métricas y trazas: cómo entender lo que ocurre dentro de tus aplicaciones

Felipe••20 min de lectura
Aprende a correlacionar logs, métricas y trazas con OpenTelemetry en Node.js. Incluye un laboratorio local con Docker Compose y Jaeger.

Una API responde lentamente. El servidor está encendido, la CPU parece normal y el log solo dice «petición recibida». ¿El tiempo se perdió esperando una base de datos, consultando otro servicio o ejecutando código propio?

Saber que algo anda mal es el comienzo. Para explicar lo que ocurrió necesitamos señales que puedan relacionarse: medidas del comportamiento general, eventos concretos y el recorrido de una operación.

Vamos a distinguir logs, métricas y trazas, y después instrumentaremos dos servicios Node.js. Seguiremos una petición desde una API hasta inventario, provocaremos un fallo controlado y buscaremos su rastro sin depender únicamente de mensajes sueltos en la consola.

Observabilidad: poder investigar lo que no habías anticipado

La observabilidad es la capacidad de entender el estado interno de un sistema mediante sus salidas. En software, esas salidas incluyen la telemetría que generan sus componentes. No se obtiene automáticamente por instalar un dashboard: depende de qué información emitimos y de si permite responder preguntas útiles. OpenTelemetry: fundamentos de observabilidad.

Una distinción práctica es esta:

  • Monitoreo: seguir condiciones conocidas, como disponibilidad, errores o saturación, y avisar cuando requieren atención.
  • Observabilidad: disponer de contexto suficiente para investigar por qué ocurre un comportamiento, incluso si no habíamos preparado una alerta específica para él.

Son capacidades complementarias. Una alerta puede avisar que aumentó la latencia de cotizaciones; la investigación puede descubrir que solo ocurre al consultar inventario desde una versión determinada de la API.

Antes de elegir herramientas, formula una pregunta concreta: «¿Dónde espera una cotización que termina con error?». Esa pregunta orientará el ejemplo.

Tres señales con diferentes niveles de detalle

SeñalQué describePregunta que ayuda a responder
LogUn evento con contexto¿Qué sucedió durante esta operación?
MétricaUna medida numérica, habitualmente agregada¿Cuántas operaciones fallan y cómo cambia su duración?
TrazaOperaciones relacionadas y su recorrido temporal¿Por qué componentes pasó esta petición?

No son tres versiones intercambiables de la misma información. Una métrica puede mostrar un problema general sin conservar el detalle de cada petición; una traza permite inspeccionar un recorrido, pero por sí sola no garantiza que estemos viendo todo el tráfico.

Logs: eventos que conservan contexto

Un log registra un hecho en un instante: comenzó una consulta, falló una dependencia o terminó una operación. Un esquema estable, con campos como fecha, nivel, servicio y evento, facilita filtrar y relacionar registros. Usar JSON ayuda al transporte y al análisis, pero cambiar arbitrariamente sus campos sigue produciendo información difícil de consultar. OpenTelemetry: logs.

«Error al procesar» aporta poco. «Falló cotizacion.preparar, en el servicio api, dentro de esta traza» permite empezar una investigación. El nivel error tampoco explica por sí solo la causa o cuántos usuarios se vieron afectados.

El objeto console de JavaScript sirve para emitir mensajes, pero necesitas decidir su estructura, destino y retención. En el laboratorio escribiremos JSON en la salida estándar y consultaremos los logs de Docker.

Métricas: ver tendencias sin inspeccionar cada petición

Un contador acumula ocurrencias, como cotizaciones terminadas. Un gauge expresa un valor actual, como memoria utilizada. Un histograma agrupa mediciones, como duraciones, y permite examinar su distribución. Son instrumentos distintos; no todo número debe modelarse como un contador. OpenTelemetry: métricas e instrumentos.

Para una API interesa relacionar volumen, errores y duración durante un intervalo. «Hubo 20 errores» significa algo diferente si ocurrieron entre 25 peticiones o entre un millón. El promedio de duración también puede ocultar una minoría de peticiones muy lentas.

En nuestro ejemplo contaremos resultados ok y error, y mediremos la duración de cada cotización. Estas métricas se agregan en el proceso: no son un registro durable de transacciones y se reinician con él.

Trazas: reconstruir una operación entre componentes

Una traza reúne spans relacionados. Un span representa una operación con inicio, fin y contexto; puede incluir atributos, eventos y estado. Por ejemplo, «atender HTTP», «preparar cotización» o «consultar inventario». OpenTelemetry: trazas y spans.

El trace ID identifica la traza; el span ID, una operación dentro de ella. Cada span hijo conserva la referencia a su padre. No debes reutilizar un único span ID para todas las operaciones.

En una vista temporal, un span padre puede contener a varios hijos y estos pueden solaparse. Sumar todas sus duraciones duplicaría tiempo. Conviene mirar qué estaba esperando el componente y qué trabajo ocurrió en paralelo, no interpretar cada barra como tiempo adicional independiente.

Cómo viaja el contexto de una traza

Dentro de un proceso, el contexto activo permite saber a qué operación pertenece el trabajo asíncrono. Entre procesos hay que inyectarlo en el transporte y extraerlo al recibirlo. Sin esa propagación, dos servicios pueden registrar la misma llamada como trazas independientes. OpenTelemetry: propagación de contexto.

En HTTP, W3C Trace Context define la cabecera traceparent. En su versión 00 contiene versión, trace ID de 32 caracteres hexadecimales, parent ID de 16 y flags. El parent ID identifica el span del emisor; el receptor crea su propio span hijo. El flag sampled comunica una decisión de muestreo, no garantiza que todos los componentes almacenen la traza. W3C Trace Context, sección 3.2.

No transportamos todos los logs dentro de esa cabecera. Enviamos el contexto necesario para relacionar las operaciones. Tampoco es una prueba de identidad: una cabecera recibida del cliente no autoriza acceso a recursos.

Un trace ID no sustituye una clave de idempotencia. La primera referencia una ejecución observada; la segunda permite reconocer una misma intención de negocio para evitar efectos duplicados durante reintentos.

Qué aporta OpenTelemetry y dónde termina su responsabilidad

OpenTelemetry, u OTel, proporciona APIs, SDKs, instrumentaciones y protocolos para generar, recoger y exportar telemetría. Permite instrumentar una aplicación con convenciones comunes y enviarla a destinos compatibles. No es, por sí solo, una plataforma completa de almacenamiento, dashboards y alertas. Qué es OpenTelemetry.

ComponenteResponsabilidad
API e instrumentacionesCrear señales desde el código y las bibliotecas compatibles
SDK y exportersProcesarlas y enviarlas o exponerlas para su recogida
Collector, si se utilizaRecibir, procesar y reenviar telemetría
Backend y herramientas de consultaConservarla, buscarla y presentarla según sus capacidades

El OpenTelemetry Collector puede centralizar procesamiento, filtrado y exportación. No es obligatorio interponerlo para este laboratorio: enviaremos trazas directamente a Jaeger. Esa simplificación es adecuada para aprender; una instalación mayor puede beneficiarse de desacoplar los servicios de sus destinos mediante un Collector. Documentación del Collector.

La instrumentación automática conoce operaciones técnicas de bibliotecas compatibles: peticiones HTTP, por ejemplo. La manual añade el significado del negocio: preparar una cotización. Se complementan; una librería HTTP no puede deducir qué significa «cotizar» para tu aplicación.

Laboratorio: una petición, dos servicios y tres señales

Usaremos Node.js, Docker Compose y Jaeger. La API expone GET /cotizacion; inventario responde GET /stock. Este último espera 120 ms de forma deliberada y puede fallar mediante un parámetro de prueba. No hay una base de datos real ni una medición de rendimiento de producción detrás de esa espera.

sequenceDiagram
    participant C as Cliente
    participant A as API
    participant I as Inventario
    C->>A: GET /cotizacion
    Note over A: Span cotizacion.preparar
    A->>I: GET /stock + traceparent
    Note over I: Span inventario.consultar
    I-->>A: Disponible o fallo simulado
    A-->>C: HTTP 200 o 503 + X-Trace-Id

Cada señal tendrá un destino explícito:

  • Trazas: Jaeger recibe OTLP sobre HTTP y permite consultarlas en su interfaz.
  • Logs: JSON en stdout, consultable con docker compose logs. No configuramos el SDK de logs ni enviamos estos mensajes a Jaeger.
  • Métricas: un exporter expone /metrics en formato Prometheus. No instalamos un servidor Prometheus: consultar ese endpoint no crea almacenamiento histórico ni alertas.

Jaeger, con la configuración local usada aquí, almacena las trazas en memoria; se pierden al reiniciarlo. Sus instrucciones oficiales describen este modo para empezar, separado de una configuración persistente. Jaeger 2.22: primeros pasos.

1. Preparar las dependencias

Crea una carpeta independiente del proyecto que quieras observar. Necesitas Docker con Compose, Node.js 22.15 o superior y pnpm 11 para preparar el lockfile. El contenedor fija una versión de Node.js 22 para repetir el entorno.

mkdir observabilidad-node
cd observabilidad-node

Guarda este package.json. Las versiones están fijadas para mantener compatibles los ejemplos:

{
  "name": "uncodigo-observabilidad-lab",
  "private": true,
  "dependencies": {
    "@opentelemetry/api": "1.10.0",
    "@opentelemetry/sdk-node": "0.300.0",
    "@opentelemetry/resources": "3.0.0",
    "@opentelemetry/instrumentation-http": "0.300.0",
    "@opentelemetry/exporter-trace-otlp-http": "0.300.0",
    "@opentelemetry/exporter-prometheus": "0.300.0"
  }
}

Añade pnpm-workspace.yaml. La autorización de protobufjs corresponde a un script de instalación de esta dependencia transitiva; queda explícita en este laboratorio:

packages:
  - '.'

allowBuilds:
  protobufjs: true

Instala y conserva el pnpm-lock.yaml generado:

pnpm install

Usaremos archivos .cjs para que el orden de carga CommonJS sea explícito. Un proyecto ESM requiere revisar su mecanismo de precarga; cambiar las extensiones no es suficiente.

2. Inicializar OpenTelemetry antes de cargar la aplicación

Crea instrumentation.cjs:

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');

const sdk = new NodeSDK({
  autoDetectResources: false,
  resource: resourceFromAttributes({
    'service.name': process.env.OTEL_SERVICE_NAME,
  }),
  traceExporter: new OTLPTraceExporter(),
  metricReaders: process.env.METRICS_PORT
    ? [new PrometheusExporter({
        host: '0.0.0.0',
        port: Number(process.env.METRICS_PORT),
      })]
    : [],
  instrumentations: [new HttpInstrumentation({
    ignoreIncomingRequestHook: (req) =>
      req.url === '/healthz' || req.url === '/metrics',
  })],
});

sdk.start();

module.exports = { sdk };

service.name permite distinguir las señales de api e inventario. traceExporter enviará las trazas al endpoint definido en Compose; metricReaders configura el exporter de métricas cuando existe METRICS_PORT.

La instrumentación HTTP debe cargarse antes de que la aplicación use la biblioteca instrumentada. Por eso arrancaremos Node con --require ./instrumentation.cjs. Es un requisito de orden de inicialización, no una importación que pueda colocarse al final del servidor. OpenTelemetry JavaScript: inicio en Node.js.

Elegimos node:http tanto en el servidor como en el cliente. Esta configuración no implica cobertura automática de cualquier cliente: fetch de Node.js utiliza Undici y requiere una instrumentación compatible. Node.js: fetch.

El exporter de métricas escucha en 0.0.0.0 dentro del contenedor para aceptar el reenvío de Docker. Compose limitará el puerto publicado a 127.0.0.1 en tu máquina.

3. Escribir logs correlacionados y cerrar el SDK

Crea common.cjs:

const { trace } = require('@opentelemetry/api');
const { sdk } = require('./instrumentation.cjs');

function log(evento, nivel = 'info') {
  const actual = trace.getActiveSpan()?.spanContext();

  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level: nivel,
    service: process.env.OTEL_SERVICE_NAME,
    event: evento,
    trace_id: actual?.traceId,
    span_id: actual?.spanId,
  }));
}

function shutdownWith(server) {
  process.once('SIGTERM', () => {
    const limite = setTimeout(() => process.exit(1), 5000);
    limite.unref();

    server.close(async () => {
      try {
        await sdk.shutdown();
        clearTimeout(limite);
        process.exitCode = 0;
      } catch {
        process.exitCode = 1;
      }
    });
  });
}

module.exports = { log, shutdownWith };

getActiveSpan() obtiene el span activo en esa ejecución. Dentro de los callbacks instrumentados, sus IDs se incorporan al log sin mantener una variable global compartida entre peticiones. Fuera de una traza activa, esos campos pueden estar ausentes.

La función de cierre deja terminar las conexiones HTTP y solicita al SDK cerrar sus componentes, con un límite de espera. Finalizar el proceso de forma abrupta puede perder telemetría todavía pendiente de exportación.

4. Instrumentar inventario

Crea inventario.cjs:

const http = require('node:http');
const { setTimeout: esperar } = require('node:timers/promises');
const { trace, SpanStatusCode } = require('@opentelemetry/api');
const { log, shutdownWith } = require('./common.cjs');

const tracer = trace.getTracer('demo-inventario');

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://inventario');

  if (req.method === 'GET' && url.pathname === '/healthz') {
    res.writeHead(200).end('ok');
    return;
  }

  if (req.method !== 'GET' || url.pathname !== '/stock') {
    res.writeHead(404).end();
    return;
  }

  await tracer.startActiveSpan('inventario.consultar', async (span) => {
    try {
      log('inventario.inicio');
      await esperar(120); // Latencia simulada, no una consulta real.

      if (url.searchParams.get('fallo') === '1') {
        throw new Error('Fallo simulado de inventario');
      }

      log('inventario.ok');
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ disponible: true }));
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      log('inventario.error', 'error');
      res.writeHead(503).end();
    } finally {
      span.end();
    }
  });
});

server.listen(3001, '0.0.0.0');
shutdownWith(server);

La instrumentación automática crea el span HTTP del servidor. Dentro de él, startActiveSpan abre la operación manual inventario.consultar y la vuelve activa durante el callback.

recordException agrega información de la excepción al span; setStatus marca explícitamente el error de la operación. Son acciones distintas. span.end() en finally evita dejarla abierta si falla. Los mensajes de este laboratorio son controlados; en una aplicación real hay que revisar qué información sensible puede contener una excepción. OpenTelemetry JavaScript: instrumentación manual.

5. Instrumentar la API y medir sus resultados

Crea api.cjs:

const http = require('node:http');
const { performance } = require('node:perf_hooks');
const { trace, metrics, SpanStatusCode } = require('@opentelemetry/api');
const { log, shutdownWith } = require('./common.cjs');

const tracer = trace.getTracer('demo-api');
const meter = metrics.getMeter('demo-api');
const total = meter.createCounter('demo.cotizaciones');
const duracion = meter.createHistogram('demo.cotizacion.duration', {
  unit: 's',
  advice: { explicitBucketBoundaries: [0.05, 0.1, 0.25, 0.5, 1, 2, 5] },
});

function consultarInventario(fallo) {
  const url = new URL('/stock', process.env.INVENTARIO_URL);
  url.searchParams.set('fallo', fallo ? '1' : '0');

  return new Promise((resolve, reject) => {
    const request = http.get(url, { timeout: 2000 }, (response) => {
      let body = '';
      response.setEncoding('utf8');
      response.on('error', reject);
      response.on('data', (chunk) => {
        body += chunk;

        if (body.length > 8192) {
          request.destroy(new Error('Respuesta demasiado grande'));
        }
      });

      response.on('end', () => {
        if (response.statusCode !== 200) {
          reject(new Error('Inventario no disponible'));
          return;
        }

        try {
          const data = JSON.parse(body);

          if (typeof data.disponible !== 'boolean') {
            throw new Error('Respuesta de inventario inválida');
          }

          resolve({ disponible: data.disponible });
        } catch {
          reject(new Error('Respuesta de inventario inválida'));
        }
      });
    });

    request.on('timeout', () => request.destroy(new Error('Timeout de inventario')));
    request.on('error', reject);
  });
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://api');

  if (req.method === 'GET' && url.pathname === '/healthz') {
    res.writeHead(200).end('ok');
    return;
  }

  if (req.method !== 'GET' || url.pathname !== '/cotizacion') {
    res.writeHead(404).end();
    return;
  }

  await tracer.startActiveSpan('cotizacion.preparar', async (span) => {
    const inicio = performance.now();
    let resultado = 'ok';

    res.setHeader('Content-Type', 'application/json');
    res.setHeader('X-Trace-Id', span.spanContext().traceId);

    try {
      log('cotizacion.inicio');
      const stock = await consultarInventario(url.searchParams.get('fallo') === '1');

      log('cotizacion.ok');
      res.writeHead(200).end(JSON.stringify(stock));
    } catch (error) {
      resultado = 'error';
      span.recordException(error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      log('cotizacion.error', 'error');
      res.writeHead(503).end(JSON.stringify({ error: 'No se pudo consultar el stock' }));
    } finally {
      total.add(1, { resultado });
      duracion.record((performance.now() - inicio) / 1000, { resultado });
      span.end();
    }
  });
});

server.listen(3000, '0.0.0.0');
shutdownWith(server);

La llamada http.get crea automáticamente un span cliente y propaga el contexto hacia inventario. Nuestra operación cotizacion.preparar contiene esa llamada.

El cliente limita la inactividad de la petición a dos segundos y comprueba el estado y la forma de la respuesta. Ese timeout de socket no es un plazo absoluto para toda la operación. Se evita devolver al consumidor la excepción interna; el cuerpo de error es estable. Node.js: opciones de las peticiones HTTP.

El contador y el histograma registran tanto éxito como error en finally. La duración se expresa en segundos, y los límites del histograma usan esa misma unidad. El atributo resultado tiene dos valores posibles: no genera una serie nueva por usuario o por petición.

X-Trace-Id es una cabecera diagnóstica propia del ejemplo para localizar la petición en Jaeger. No es la cabecera de propagación estándar ni sustituye traceparent.

6. Ejecutar el entorno local

Crea Dockerfile:

FROM node:22.23.4-bookworm-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN npm install --global pnpm@11.14.0 && pnpm install --frozen-lockfile
COPY *.cjs ./
USER node

Y .dockerignore, para no copiar dependencias de tu equipo dentro de la imagen:

node_modules
.git
*.log

Finalmente, crea compose.yaml:

name: uncodigo-observabilidad-lab

x-node: &node
  build: .
  init: true
  healthcheck:
    test: ["CMD", "node", "-e", "fetch('http://localhost:'+process.env.PORT+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
    interval: 3s
    timeout: 2s
    retries: 15

services:
  jaeger:
    image: cr.jaegertracing.io/jaegertracing/jaeger:2.22.0
    ports:
      - "127.0.0.1:16686:16686"

  inventario:
    <<: *node
    command: ["node", "--require", "./instrumentation.cjs", "inventario.cjs"]
    environment:
      PORT: "3001"
      OTEL_SERVICE_NAME: inventario
      OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://jaeger:4318/v1/traces
      OTEL_TRACES_SAMPLER: always_on
    depends_on:
      jaeger:
        condition: service_started

  api:
    <<: *node
    command: ["node", "--require", "./instrumentation.cjs", "api.cjs"]
    environment:
      PORT: "3000"
      OTEL_SERVICE_NAME: api
      OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://jaeger:4318/v1/traces
      OTEL_TRACES_SAMPLER: always_on
      INVENTARIO_URL: http://inventario:3001
      METRICS_PORT: "9464"
    ports:
      - "127.0.0.1:3000:3000"
      - "127.0.0.1:9464:9464"
    depends_on:
      inventario:
        condition: service_healthy

El endpoint OTLP incluye /v1/traces porque configuramos específicamente el exportador HTTP de trazas. La UI de Jaeger usa otro puerto. always_on conserva todas las trazas del laboratorio para facilitar la inspección; no es una política general para producción.

Arranca los servicios:

docker compose up --build -d --wait

Los puertos de la API, métricas y Jaeger quedan publicados solo en loopback; inventario se consulta por la red interna de Compose. Este entorno no incorpora autenticación y está pensado para ejecutarse localmente.

Seguir una petición de principio a fin

Realiza dos peticiones correctas y una con fallo deliberado:

curl -i http://127.0.0.1:3000/cotizacion

curl -i http://127.0.0.1:3000/cotizacion

curl -i 'http://127.0.0.1:3000/cotizacion?fallo=1'

Las dos primeras devuelven 200 y {"disponible":true}. La tercera devuelve 503 con un mensaje controlado. Copia el X-Trace-Id de una respuesta.

Abre http://127.0.0.1:16686, selecciona el servicio api y busca trazas recientes. También puedes abrir http://127.0.0.1:16686/trace/ seguido del ID copiado. La exportación se realiza por lotes, de modo que puede tardar unos segundos en aparecer.

Para una petición completa encontrarás esta jerarquía:

api: GET                         ← servidor HTTP
└─ cotizacion.preparar           ← operación manual
   └─ api: GET                   ← cliente HTTP hacia inventario
      └─ inventario: GET         ← servidor HTTP
         └─ inventario.consultar ← operación manual

Los cinco spans tienen el mismo trace ID y distintos span IDs. La relación padre-hijo atraviesa el límite entre contenedores: no basta con que sus fechas sean parecidas.

Jaeger muestra los cinco spans de una petición correcta entre los servicios API e inventario.

Captura real del laboratorio: la jerarquía de la izquierda conecta los cinco spans; las barras muestran sus intervalos, que se solapan. Los tiempos incluyen la espera artificial de inventario. Pulsa la imagen para verla a tamaño completo.

Consulta los logs:

docker compose logs --no-log-prefix api inventario

En una ejecución real del laboratorio, estos fueron dos registros de inicio de la misma petición, reformateados para facilitar su lectura:

{
  "timestamp": "2026-11-01T18:13:53.390Z",
  "level": "info",
  "service": "api",
  "event": "cotizacion.inicio",
  "trace_id": "0685e72c428d37cb4cee8411d94c79ae",
  "span_id": "6a3ad5ffff7ebc98"
}
{
  "timestamp": "2026-11-01T18:13:53.394Z",
  "level": "info",
  "service": "inventario",
  "event": "inventario.inicio",
  "trace_id": "0685e72c428d37cb4cee8411d94c79ae",
  "span_id": "7906f14cb9594804"
}

Tus IDs y fechas serán diferentes. Lo que debe coincidir entre componentes es el trace_id; el span_id identifica su operación manual correspondiente. Este JSON no aparecerá automáticamente en Jaeger: aquí hacemos la correlación entre dos destinos mediante los identificadores.

Inspecciona también la petición fallida. Las operaciones manuales quedan con estado de error y un evento de excepción, mientras el cliente recibe una respuesta sin detalles internos. Así puedes identificar dónde comenzó el fallo y cómo lo recibió la API.

Traza fallida en Jaeger con respuestas HTTP 503 y el evento de excepción del span inventario.consultar desplegado.

En esta petición se activó fallo=1. El span inventario.consultar muestra otel.status_code=ERROR y el evento exception, cuyo mensaje es «Fallo simulado de inventario». El detalle corresponde a telemetría interna del laboratorio, no al cuerpo de la respuesta HTTP. Pulsa la imagen para ampliar.

Leer las métricas sin confundirlas con un historial

Consulta el exporter:

curl -s http://127.0.0.1:9464/metrics

Después de las tres peticiones anteriores, partiendo de procesos nuevos, se verificaron estas líneas:

demo_cotizaciones_total{resultado="ok",otel_scope_name="demo-api"} 2
demo_cotizaciones_total{resultado="error",otel_scope_name="demo-api"} 1

El exporter adapta los nombres al formato de Prometheus. También encontrarás demo_cotizacion_duration_count, _sum y _bucket: número de observaciones, suma de duraciones y recuentos acumulados por límite. le="0.25", por ejemplo, corresponde a observaciones de hasta 0,25 segundos.

No interpretes los tiempos de este ejercicio como un benchmark: inventario incluye una espera artificial y el resultado depende de tu entorno. La verificación comprueba que se emiten las mediciones y se pueden relacionar las operaciones.

Si repites las llamadas, los contadores aumentan. Si recreas la API, se reinician. Para obtener series históricas necesitas un sistema que recoja y almacene las métricas, como un servidor Prometheus; este exporter solo las expone. OpenTelemetry JavaScript: exportadores.

Para detener y eliminar los contenedores del laboratorio:

docker compose down

Errores que vuelven la telemetría costosa o poco útil

Registrar datos sin una pregunta concreta

Emitir cada variable aumenta ruido y costo, pero no necesariamente explica el comportamiento. Define eventos estables y spans que representen operaciones útiles. Revisa qué conviene registrar en una API y qué datos excluir.

Tokens, cookies, contraseñas y cuerpos completos pueden acabar en logs, atributos HTTP o mensajes de excepción. La instrumentación automática también necesita revisión: OpenTelemetry no sabe por sí solo qué considera sensible tu aplicación. Minimiza los datos desde su origen y controla acceso y retención en sus destinos. OpenTelemetry: tratamiento de datos sensibles.

Crear una serie de métricas por cada usuario

La cardinalidad describe cuántas combinaciones de atributos produce una métrica. Usar un trace ID, un correo o una URL con identificadores como etiqueta puede multiplicar esas combinaciones rápidamente.

En el ejemplo, resultado solo admite ok y error. Para identificar una ejecución individual ya tenemos sus logs y su traza. Mantén nombres de operaciones estables y utiliza plantillas de rutas cuando corresponda, en lugar de crear una etiqueta distinta para cada recurso.

Perder contexto o duplicar instrumentaciones

Cargar el SDK demasiado tarde, utilizar un cliente no cubierto o ejecutar trabajo fuera del contexto esperado puede romper la continuidad. Añadir otra instrumentación que ya aporta el framework también puede duplicar spans.

Comprueba una petición real de extremo a extremo, incluidos errores y concurrencia. En este laboratorio se verificaron cinco spans relacionados entre dos servicios, logs correlacionados y diez peticiones concurrentes con trace IDs distintos. Que el proceso arranque sin errores no demostraría esas propiedades.

Suponer que una traza representa todo el tráfico

El muestreo selecciona qué trazas conservar. Una estrategia puede decidir al inicio o después de recibir información del recorrido; ambas tienen costos y limitaciones. No deduzcas el porcentaje total de errores contando únicamente las trazas visibles si desconoces esa selección. OpenTelemetry: muestreo.

El always_on de este ejercicio ayuda a comprobar el flujo con poco tráfico. En producción, acuerda una política según volumen, presupuesto y necesidades de diagnóstico. Las métricas que incrementamos en la aplicación no dependen de que un span haya sido seleccionado para exportarse.

Confundir instrumentación con una plataforma operativa

Faltan decisiones de almacenamiento, retención, consultas, alertas, control de acceso y comportamiento cuando el destino no está disponible. Un Collector puede ayudar a procesar y enviar señales, pero no convierte por sí solo este Compose en una plataforma de producción.

La siguiente pregunta debe poder responderse

Empieza por un recorrido importante y comprueba que puedes responder tres preguntas: cuántas operaciones fallan, dónde pasan el tiempo y qué ocurrió en una ejecución concreta.

Si puedes pasar de una métrica a una traza y de esa traza a los eventos de sus componentes, la instrumentación ya está ayudando a investigar. Amplíala cuando aparezca una pregunta que tus señales actuales no puedan responder; acumular telemetría sin propósito no mejora automáticamente la comprensión del sistema.

Referencias

#observabilidad#OpenTelemetry#Node.js#logs#métricas#trazas distribuidas#Jaeger

Artículos relacionados