En arquitecturas modernas de Next.js 15 con App Router, el procesamiento de una única solicitud HTTP de cliente se distribuye a través de múltiples entornos de ejecución heterogéneos: el navegador del cliente, capas perimetrales (Edge Middleware), componentes de servidor de React (RSC) en Node.js, Server Actions, grupos de conexiones a bases de datos relacionales (PostgreSQL, Prisma) y microservicios externos. En un entorno polimórfico semejante, el registro aislado convencional (console.log, archivos de log inconexos en disco) resulta incapaz de diagnosticar cuellos de botella de latencia (Latency Bottlenecks), interbloqueos distribuidos y fallos en cascada.
El pilar fundamental del rastreo distribuido es la especificación W3C Trace Context. El contexto se propaga de manera determinista a través de las fronteras de red mediante dos cabeceras HTTP estándar:
traceparent: codifica la versión de la especificación, un identificador global de traza de 16 bytes (trace_id), un identificador de span padre de 8 bytes (parent_id) y flags de control (trace_flags, como el bit de muestreo). Formato: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01.tracestate: lista de pares clave-valor opacos encargada de transportar metadatos de enrutamiento y filtrado específicos de plataformas de monitorización (Datadog, Dynatrace, New Relic) sin mutar la cabecera traceparent.En Next.js 15, el punto de entrada canónico para inicializar la telemetría a nivel de runtime es el archivo instrumentation.ts, ubicado en la raíz del proyecto (o dentro de src/). La función register() exportada por dicho archivo es invocada una única vez por el runtime de Next.js durante el arranque en frío de la instancia antes de la importación y compilación de rutas. Una restricción arquitectónica crucial radica en el aislamiento de entornos: instrumentation.ts se evalúa tanto en el runtime de Node.js como en el Edge Runtime. La importación estática de dependencias pesadas (@opentelemetry/sdk-node, enlaces gRPC en C++) en el Edge Runtime desencadena errores fatales de compilación (Module not found) o excepciones por APIs no soportadas de Node.js (fs, net).
La topología de ejecución de una traza distribuida a través de la pila de Next.js 15 y los servicios externos constituye un Grafo Acíclico Dirigido (DAG) de spans anidados:
[ Navegador: Navegación de Cliente ] -> traceparent: 00-7a8b9c...-1111-01
│
▼
[ Edge Middleware: Auth y Enrutamiento Geo ] -> duración: 4ms
│
▼
[ Servidor Node.js: Render RSC Raíz (/dashboard) ] -> duración: 185ms
│
├─► [ Span Anidado: Server Action (fetchUserData) ] -> duración: 62ms
│ │
│ └─► [ Span BD: pg.query (SELECT * FROM users) ] -> duración: 18ms
│
└─► [ Span Cliente HTTP: fetch(api.billing.internal) ] -> duración: 95ms
│ (Propagación de Cabeceras W3C)
▼
[ Microservicio Remoto: Go/gRPC Facturación ] -> duración: 80msawait hacia APIs externas o Prisma en lugar de ejecutarse en paralelo mediante Promise.all().La instrumentación de observabilidad distribuida requiere una segregación modular: el archivo de entrada instrumentation.ts con discriminación de NEXT_RUNTIME, una configuración independiente instrumentation.node.ts para NodeSDK con auto-instrumentaciones y utilidades auxiliares para la creación de spans personalizados en Server Actions.
/**
* Punto de entrada raíz de instrumentación en Next.js 15.
* Invocado una sola vez al arrancar el servidor antes de cargar rutas y middleware.
*/
export async function register() {
// Verificación estricta: los módulos pesados de NodeSDK se importan EXCLUSIVAMENTE en Node.js.
// Intentar cargar estos módulos en el Edge Runtime provocaría el fallo inmediato de la compilación.
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { initializeNodeTelemetry } = await import('./instrumentation.node');
initializeNodeTelemetry();
}
}El cliente inicia una navegación o petición fetch. Se inyecta la cabecera W3C traceparent para continuar la traza.
Un span ligero verifica la sesión del usuario, procesa reglas geográficas y propaga cabeceras hacia upstream.
Inicia el span raíz de la página. NodeSDK extrae el contexto entrante mediante el propagador W3C.
La instrumentación automática de pg/Prisma genera spans secundarios para consultas SQL registrando sentencias saneadas.
BatchSpanProcessor en segundo plano transmite los spans encolados por gRPC hacia el recolector OTel o agente APM.
A continuación se detalla una configuración de NodeSDK de nivel empresarial con exportación OTLP por gRPC, muestreo probabilístico, mitigación de ruido y el helper withTracedSpan para Server Actions.
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { BatchSpanProcessor, TraceIdRatioBasedSampler, ParentBasedSampler } from '@opentelemetry/sdk-trace-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { trace, context, SpanStatusCode, Span, propagation, type Tracer } from '@opentelemetry/api';
/**
* Inicializa OpenTelemetry NodeSDK para el runtime de Node.js.
* Configura la exportación de spans a un recolector OTLP (Jaeger / Agente Datadog)
* e incorpora instrumentación automática para HTTP, Prisma, PostgreSQL y Redis.
*/
export function initializeNodeTelemetry(): void {
// Muestreo adaptativo: respeta decisiones upstream o muestrea el 20% del tráfico raíz
const sampleRatio = parseFloat(process.env.OTEL_TRACE_SAMPLE_RATIO || '0.2');
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(sampleRatio),
});
// Exportador de trazas OTLP mediante gRPC binario de alto rendimiento
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS
? JSON.parse(process.env.OTEL_EXPORTER_OTLP_HEADERS)
: {},
});
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'nextjs-enterprise-app',
[ATTR_SERVICE_VERSION]: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
'deployment.environment': process.env.NODE_ENV || 'production',
'host.name': process.env.HOSTNAME || 'k8s-pod',
}),
sampler,
spanProcessor: new BatchSpanProcessor(traceExporter, {
maxQueueSize: 2048,
scheduledDelayMillis: 2000,
exportTimeoutMillis: 5000,
maxExportBatchSize: 512,
}),
instrumentations: [
getNodeAutoInstrumentations({
// Deshabilitar spans ruidosos de fs y dns generados por la carga de chunks en Next.js
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
'@opentelemetry/instrumentation-http': {
enabled: true,
ignoreIncomingRequestHook: (req) => {
// Ignorar sondas internas de salud de Kubernetes
return req.url === '/api/healthz' || req.url === '/api/metrics';
},
},
}),
],
});
sdk.start();
console.log('[OpenTelemetry] NodeSDK iniciado correctamente con exportador OTLP gRPC.');
// Cierre limpio de buffers ante señales de terminación del contenedor
const shutdown = async () => {
try {
await sdk.shutdown();
console.log('[OpenTelemetry] SDK detenido y buffers volcados exitosamente.');
} catch (err) {
console.error('[OpenTelemetry] Error durante el apagado del SDK:', err);
}
};
process.once('SIGTERM', shutdown);
process.once('SIGINT', shutdown);
}
/**
* Retorna una instancia nombrada de tracer vinculada a la versión de la aplicación.
*/
export function getTracer(name = 'nextjs-custom-tracer'): Tracer {
return trace.getTracer(name, process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0');
}
/**
* Utilidad de producción para trazar Server Actions y operaciones en RSC.
* Gestiona el ciclo de vida del span, captura excepciones y asegura su finalización.
*/
export async function withTracedSpan<T>(
name: string,
attributes: Record<string, string | number | boolean>,
fn: (span: Span) => Promise<T>
): Promise<T> {
const tracer = getTracer();
return tracer.startActiveSpan(name, async (span) => {
for (const [key, value] of Object.entries(attributes)) {
span.setAttribute(key, value);
}
try {
const result = await fn(span);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : 'Unknown execution error',
});
throw error;
} finally {
span.end();
}
});
}
/**
* Utilidad para inyectar el W3C TraceContext activo en cabeceras HTTP salientes.
*/
export function injectTraceContext(headers: Headers = new Headers()): Headers {
const output: Record<string, string> = {};
propagation.inject(context.active(), output);
for (const [key, val] of Object.entries(output)) {
headers.set(key, val);
}
return headers;
}En Server Actions, los spans aíslan transacciones operativas y transmiten cabeceras durante la invocación de microservicios:
'use server';
import { withTracedSpan, injectTraceContext } from '@/instrumentation.node';
interface CheckoutPayload {
cartId: string;
userId: string;
totalAmount: number;
}
export async function processOrderAction(payload: CheckoutPayload) {
return withTracedSpan(
'action.checkout.processOrder',
{
'business.user_id': payload.userId,
'business.cart_id': payload.cartId,
'business.amount': payload.totalAmount,
},
async (span) => {
span.addEvent('order.validation.start');
if (payload.totalAmount <= 0) {
throw new Error('Invalid order amount');
}
// Invocación a microservicio de pagos con propagación de W3C Trace Context
const paymentHeaders = injectTraceContext(new Headers({ 'Content-Type': 'application/json' }));
const response = await fetch('https://billing.internal.enterprise/v1/charge', {
method: 'POST',
headers: paymentHeaders,
body: JSON.stringify({
userId: payload.userId,
amount: payload.totalAmount,
}),
});
if (!response.ok) {
throw new Error(`Payment processing failed with status ${response.status}`);
}
const chargeResult = await response.json();
span.setAttribute('payment.transaction_id', chargeResult.transactionId);
span.addEvent('order.completed');
return { success: true, orderId: chargeResult.orderId };
}
);
}Declarar import { NodeSDK } from '@opentelemetry/sdk-node' en la raíz de instrumentation.ts obliga a Webpack o Turbopack a compilar el paquete para el Edge Runtime. La compilación fallará de inmediato al no encontrar módulos nativos de Node (child_process, net, tls). Aísle siempre la importación mediante await import('./instrumentation.node') dentro de una comprobación if (process.env.NEXT_RUNTIME === 'nodejs').
Si se produce una excepción y no se invoca span.end(), el span permanece abierto en la memoria del búfer de forma indefinida. Estos spans se descartan o se envían con duraciones infinitas, falseando los gráficos de latencia en los paneles de APM. Envuelva siempre la lógica en un bloque try...catch...finally y ejecute span.end() en finally.
Configurar SimpleSpanProcessor en lugar de BatchSpanProcessor obliga a Node.js a realizar una llamada de red síncrona al recolector OTLP por cada span generado. Esta sobrecarga de red bloquea el Event Loop y reduce drásticamente el rendimiento (RPS). En entornos de producción debe utilizarse siempre BatchSpanProcessor con búfer en memoria.
Emplear AlwaysOnSampler en plataformas con miles de solicitudes por segundo produce gigabytes de telemetría, sobrecarga la CPU con la serialización Protobuf y genera costes astronómicos de ingesta en Datadog. Aplique ParentBasedSampler junto con TraceIdRatioBasedSampler (10-20% de ratio) o traslade el tail-sampling al OpenTelemetry Collector.
Definir el hook register() con aislamiento condicional para prevenir errores de compilación en Edge.
Configurar BatchSpanProcessor, el exportador gRPC OTLP y silenciar spans innecesarios de fs y dns.
Garantizar que las peticiones salientes con fetch incluyan cabeceras traceparent mediante propagation.inject().
Envolver operaciones críticas con withTracedSpan para registrar atributos de negocio y estados de error.
Comprobar la recepción de trazas en Jaeger o Datadog, analizando árboles de latencia y percentiles p99.