Instrumentación de OpenTelemetry para Node.js
Guía práctica para instrumentar Node.js con OpenTelemetry: trazas, métricas y logs con auto-instrumentación, spans propios, contexto y exportadores.

OpenTelemetry es el estándar abierto para recopilar trazas, métricas y logs de las aplicaciones. Sustituye los SDK específicos de cada proveedor por una única capa de instrumentación que puede exportar a cualquier backend — Jaeger, Grafana, Datadog o New Relic. En lugar de eliminar toda la instrumentación cada vez que cambias de proveedor de observabilidad, solo tienes que cambiar la configuración de un exportador.
Para Node.js, OpenTelemetry ofrece auto-instrumentación que captura solicitudes HTTP, consultas a bases de datos y operaciones del framework sin cambiar una sola línea de código. Los spans personalizados añaden contexto específico de la aplicación: qué usuario originó la solicitud, qué ruta de feature flag se tomó, cuánto tardó la lógica de negocio en comparación con las operaciones de E/S.
Configuración de la auto-instrumentación
La auto-instrumentación aplica parches a las bibliotecas más populares (Express, Fastify, pg, Redis, HTTP) para crear spans automáticamente. Debes inicializarla antes de importar el código de tu aplicación.
// tracing.ts — load this BEFORE your application
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
getNodeAutoInstrumentations,
} from "@opentelemetry/auto-instrumentations-node";
import {
OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";
import {
OTLPMetricExporter,
} from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "payment-service",
[ATTR_SERVICE_VERSION]: "2.4.1",
environment: process.env.NODE_ENV ?? "development",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/traces",
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + "/v1/metrics",
}),
exportIntervalMillis: 15_000,
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": { enabled: false },
"@opentelemetry/instrumentation-http": {
ignoreIncomingPaths: ["/health", "/ready"],
},
}),
],
});
sdk.start();
process.on("SIGTERM", async () => {
await sdk.shutdown();
process.exit(0);
});// package.json — start with tracing loaded first
{
"scripts": {
"start": "node --require ./dist/tracing.js ./dist/index.js",
"dev": "tsx --require ./src/tracing.ts ./src/index.ts"
}
}Añadir spans personalizados
La auto-instrumentación captura las operaciones de infraestructura, pero no puede captar la lógica de negocio. Los spans personalizados añaden contexto como "procesando el pago del usuario X" o "aplicando las reglas de descuento".
import { trace, SpanStatusCode, context } from "@opentelemetry/api";
const tracer = trace.getTracer("payment-service", "2.4.1");
interface PaymentRequest {
userId: string;
amount: number;
currency: string;
method: "card" | "bank_transfer" | "wallet";
}
async function processPayment(request: PaymentRequest): Promise<string> {
return tracer.startActiveSpan("processPayment", async (span) => {
try {
// Add attributes for filtering and grouping in your backend
span.setAttributes({
"payment.user_id": request.userId,
"payment.amount": request.amount,
"payment.currency": request.currency,
"payment.method": request.method,
});
// Nested span for validation
const validated = await tracer.startActiveSpan(
"validatePayment",
async (validationSpan) => {
const result = await validatePaymentDetails(request);
validationSpan.setAttribute("payment.valid", result.valid);
validationSpan.end();
return result;
}
);
if (!validated.valid) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: validated.reason,
});
throw new Error(validated.reason);
}
// Nested span for provider call
const chargeId = await tracer.startActiveSpan(
"chargeProvider",
async (providerSpan) => {
providerSpan.setAttribute("payment.provider", "stripe");
const id = await chargePaymentProvider(request);
providerSpan.setAttribute("payment.charge_id", id);
providerSpan.end();
return id;
}
);
span.setStatus({ code: SpanStatusCode.OK });
return chargeId;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message,
});
throw error;
} finally {
span.end();
}
});
}Métricas personalizadas
Las trazas muestran solicitudes individuales. Las métricas muestran el comportamiento agregado: tasas de solicitudes, tasas de error, latencias y métricas de negocio como los ingresos por minuto.
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("payment-service", "2.4.1");
// Counter: monotonically increasing value
const paymentCounter = meter.createCounter("payments.total", {
description: "Total number of payment attempts",
unit: "1",
});
// Histogram: distribution of values (latencies, sizes)
const paymentDuration = meter.createHistogram("payments.duration", {
description: "Payment processing duration",
unit: "ms",
});
// Up/Down Counter: value that increases and decreases
const activePayments = meter.createUpDownCounter(
"payments.active",
{
description: "Currently processing payments",
unit: "1",
}
);
// Usage in request handler
async function handlePayment(request: PaymentRequest): Promise<void> {
const startTime = Date.now();
activePayments.add(1, { method: request.method });
try {
await processPayment(request);
paymentCounter.add(1, {
method: request.method,
status: "success",
});
} catch (error) {
paymentCounter.add(1, {
method: request.method,
status: "failure",
error_type: (error as Error).name,
});
} finally {
const duration = Date.now() - startTime;
paymentDuration.record(duration, { method: request.method });
activePayments.add(-1, { method: request.method });
}
}// ❌ High-cardinality attributes cause metric explosion
paymentCounter.add(1, {
user_id: request.userId, // Millions of unique values
request_id: request.id, // Unique per request
timestamp: Date.now().toString(), // Unique per call
});
// Result: millions of time series, backend crashes or bill explodes
// ✅ Low-cardinality attributes for metrics
paymentCounter.add(1, {
method: request.method, // 3-4 values: card, bank, wallet
status: "success", // 2 values: success, failure
currency: request.currency, // ~10 values: USD, EUR, GBP...
});
// Result: ~80 time series (4 × 2 × 10), manageable and usefulPropagación de contexto entre servicios
Cuando el Servicio A llama al Servicio B, el contexto de la traza debe propagarse para que los spans de ambos servicios aparezcan en la misma traza. OpenTelemetry se encarga de esto automáticamente en las llamadas HTTP mediante las cabeceras W3C Trace Context.
// Service A: makes an outgoing HTTP call
// OpenTelemetry auto-instrumentation automatically injects
// traceparent and tracestate headers:
//
// traceparent: 00-<trace-id>-<span-id>-01
// tracestate: <vendor-specific data>
//
// You don't need to do anything — the HTTP instrumentation handles it.
import express from "express";
const app = express();
app.post("/api/orders", async (req, res) => {
// This span is automatically created by Express instrumentation
// The HTTP call below automatically propagates trace context
const paymentResult = await fetch(
"http://payment-service:3001/api/charge",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: req.body.total }),
}
);
// Both the Express span and the fetch span (and the
// payment-service spans) appear in the same trace
res.json({ orderId: "ord_123", paymentStatus: "charged" });
});# Docker Compose for local development with OpenTelemetry
version: "3.8"
services:
payment-service:
build: ./payment-service
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_SERVICE_NAME: payment-service
order-service:
build: ./order-service
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_SERVICE_NAME: order-service
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-config.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # gRPC receiver
- "4318:4318" # HTTP receiver
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UIPuntos clave
- Inicializa OpenTelemetry antes de importar el código de la aplicación — la auto-instrumentación aplica parches a las bibliotecas en el momento de la importación; si la cargas después de tu aplicación, se pierden los puntos de enganche de la instrumentación.
- La auto-instrumentación cubre la infraestructura; los spans personalizados cubren la lógica de negocio — las solicitudes HTTP y las consultas a bases de datos se capturan automáticamente; añade spans personalizados para las operaciones específicas de tu dominio.
- Usa atributos de baja cardinalidad en las métricas — los IDs de usuario, los IDs de solicitud y las marcas de tiempo generan millones de series temporales; limítate a dimensiones como el método, el estado y la categoría.
- La propagación de contexto ocurre automáticamente en HTTP — las cabeceras W3C Trace Context se inyectan y extraen mediante la instrumentación HTTP; no se necesita gestión manual de cabeceras.
- Registra las excepciones y define el estado del span en caso de error —
span.recordException()captura la pila de llamadas;span.setStatus(ERROR)marca el span en rojo en las visualizaciones de trazas.


