OpenTelemetry en Next.js: traces que sobreviven el edge y el servidor sin perder el contexto
¿Por qué observabilidad en Next.js sigue siendo un tema a medias en 2025? Llevamos años con OpenTelemetry como estándar de facto en el backend — bien documentado en Spring Boot, en Node puro, en Go — y sin embargo el App Router de Next.js tiene una frontera que silenciosamente rompe el contexto de trace sin levantar ningún error visible. Hace tiempo que me pregunto si la comunidad lo subestima porque el problema no tira excepciones: simplemente desaparece.
Mi tesis es directa: OpenTelemetry en Next.js funciona, pero requiere configuración explícita del propagador. El default silenciosamente rompe el trace en la frontera edge/node. Si venís del lado Spring Boot donde el contexto se propaga casi automático, esto te va a sorprender.
El problema real: qué pasa en la frontera edge/node con el contexto de trace
El App Router de Next.js corre en dos entornos distintos que comparten muy poco:
- Edge Runtime: Middleware, algunas Route Handlers. Entorno recortado, basado en Web APIs, sin soporte completo de Node.js. Corre en V8 isolates.
- Node.js Runtime: Server Components, Server Actions, API Routes. El Node de siempre, con acceso al filesystem,
process, etc.
Cuando una request entra por el Middleware (edge) y después llega a un Server Component (node), hay una transición de entorno. Si el propagador de OpenTelemetry no está configurado explícitamente para leer y escribir los headers traceparent y tracestate en ambos lados, el trace se corta ahí. El span del Middleware cierra sin hijos. El Server Component arranca un nuevo trace sin parent. En Jaeger o en cualquier collector, ves dos trazas huérfanas donde debería haber una sola cadena.
Lo que hace difícil diagnosticar esto: no hay error. No hay warning. El código corre perfectamente. Solo cuando mirás el collector notás que los trace IDs no coinciden.
Cómo configurar OpenTelemetry en Next.js App Router: el instrumentation hook
Next.js expone un punto de entrada específico para esto, documentado en la guía oficial: el archivo instrumentation.ts en la raíz del proyecto (o dentro de src/ si usás esa estructura). Este hook se ejecuta una sola vez al iniciar el servidor.
// instrumentation.ts — se ejecuta una vez al arrancar el servidor Node.js
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { W3CTraceContextPropagator } from '@opentelemetry/core'
import { CompositePropagator, W3CBaggagePropagator } from '@opentelemetry/core'
import { Resource } from '@opentelemetry/resources'
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
export async function register() {
// Importación dinámica: solo inicializamos en el runtime de Node.js
// El edge runtime no soporta el SDK completo de Node
if (process.env.NEXT_RUNTIME === 'nodejs') {
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'mi-app-nextjs',
}),
traceExporter: new OTLPTraceExporter({
// Apuntá a tu collector local o a Railway, Fly, etc.
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
}),
// CRÍTICO: el propagador W3C TraceContext es el que lee/escribe
// los headers traceparent y tracestate entre edge y node.
// Sin esto, cada entorno arranca un trace nuevo sin parent.
textMapPropagator: new CompositePropagator({
propagators: [
new W3CTraceContextPropagator(),
new W3CBaggagePropagator(),
],
}),
})
sdk.start()
}
}El condicional process.env.NEXT_RUNTIME === 'nodejs' no es opcional. Si intentás inicializar el NodeSDK completo en el edge runtime, el build falla porque ese entorno no tiene acceso a las APIs de Node que el SDK necesita. La documentación oficial menciona esto, pero lo entierra un poco.
El lado del edge: cómo propagar el contexto sin el SDK completo
El edge runtime no puede correr el NodeSDK. Lo que sí puede hacer es leer y escribir headers usando las primitivas del W3C TraceContext. Si usás Middleware para autenticación o routing, este es el patrón para propagar el contexto hacia el servidor:
// middleware.ts — edge runtime, solo propagación de headers
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Si ya existe un traceparent entrante (por ejemplo, desde un API gateway),
// lo reenviamos tal cual hacia el servidor.
// Si no existe, el servidor Node arrancará un trace nuevo — eso es correcto.
const traceparent = request.headers.get('traceparent')
if (traceparent) {
response.headers.set('traceparent', traceparent)
}
const tracestate = request.headers.get('tracestate')
if (tracestate) {
response.headers.set('tracestate', tracestate)
}
return response
}
export const config = {
// Aplicar solo a rutas que necesiten propagación
matcher: ['/api/:path*', '/((?!_next/static|_next/image|favicon.ico).*)'],
}Esto no genera spans en el edge (para eso necesitarías el SDK completo, que no está disponible), pero mantiene la cadena de contexto viva para que el Node.js Runtime pueda continuar el trace desde el mismo trace ID.
Los gotchas que nadie documenta bien
1. El instrumentation.ts necesita que experimental.instrumentationHook esté habilitado en versiones anteriores a Next.js 15.
En Next.js 15+ está habilitado por defecto. Si estás en 14, necesitás esto en next.config.ts:
// next.config.ts
const nextConfig = {
experimental: {
instrumentationHook: true, // necesario en Next.js 14 y anteriores
},
}
export default nextConfigSin esto, el archivo instrumentation.ts existe en el disco pero nunca se ejecuta. El servidor arranca sin telemetría y sin aviso.
2. Server Actions no propagan el contexto automáticamente.
Una Server Action es, desde la perspectiva de OpenTelemetry, una request HTTP nueva. Si no instrumentás explícitamente la Action con un span manual, va a aparecer como un trace separado en el collector.
// app/actions/crear-recurso.ts
'use server'
import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('mi-app-nextjs')
export async function crearRecurso(formData: FormData) {
// Creamos un span hijo explícito para la Server Action
return await tracer.startActiveSpan('server-action.crearRecurso', async (span) => {
try {
const nombre = formData.get('nombre') as string
span.setAttribute('recurso.nombre', nombre)
// tu lógica acá
const resultado = await guardarEnDB(nombre)
span.setStatus({ code: 0 }) // SpanStatusCode.OK
return resultado
} catch (error) {
span.recordException(error as Error)
span.setStatus({ code: 2, message: (error as Error).message }) // SpanStatusCode.ERROR
throw error
} finally {
span.end()
}
})
}3. El nombre del exporter importa para el collector.
OTLPTraceExporter por HTTP usa el puerto 4318. Si estás usando gRPC (Jaeger directamente), necesitás @opentelemetry/exporter-trace-otlp-grpc y el puerto 4317. Mezclar exporters y puertos es una fuente clásica de "todo configurado y no llega nada al collector".
4. El sdk.start() no espera confirmación del collector.
Si el collector no está disponible al arrancar, el SDK no falla con error — simplemente descarta los spans. Hay un SDK shutdown que podés hookear para hacer flush antes de que el proceso cierre:
// Dentro del register(), después de sdk.start()
process.on('SIGTERM', () => {
sdk.shutdown().finally(() => process.exit(0))
})Checklist de decisión: antes de instrumentar tu Next.js App Router
Antes de arrancar, estas son las preguntas que determinan cuánto esfuerzo necesitás:
| Pregunta | Si la respuesta es... | Implicación |
|---|---|---|
| ¿Tenés Middleware que corre en el edge? | Sí | Necesitás propagación manual de headers en middleware.ts |
| ¿Usás Server Actions con lógica de negocio? | Sí | Necesitás spans manuales en cada Action relevante |
| ¿Estás en Next.js 14 o anterior? | Sí | Necesitás habilitar instrumentationHook explícitamente |
| ¿Usás un collector en Railway/Fly/Docker? | Sí | Verificá el puerto: 4318 para HTTP, 4317 para gRPC |
| ¿Necesitás traces completos end-to-end? | Sí | El W3CTraceContextPropagator es obligatorio, no opcional |
| ¿Solo querés traces del servidor Node? | Sí | Con instrumentation.ts básico alcanza, sin manual spans |
El SDK de OpenTelemetry para JavaScript documenta todos los exporters y propagadores disponibles. El punto que la documentación oficial de Next.js no enfatiza suficiente es que el propagador por defecto del SDK no es el W3C TraceContext — y eso es exactamente lo que rompe la cadena en la frontera edge/node.
Qué no podés concluir sin datos productivos propios
Siendo honesto sobre los límites de esta guía:
- Overhead de latencia: No hay números verificables sobre cuánto agrega la instrumentación en cold starts de Vercel o en edge functions. Podría ser cero, podría ser relevante. Necesitás medirlo en el ambiente donde desplegás.
- Volume de spans: En un sistema con mucho tráfico, la cantidad de spans que genera el
NodeSDKautomáticamente (Next.js instrumenta sus propias operaciones internas) puede ser mayor de lo esperado. El sampling es un tema aparte. - Compatibilidad con Vercel Edge Network: La propagación de headers funciona en teoría en cualquier entorno que respete el protocolo HTTP. Que funcione exactamente igual en Vercel Edge, Cloudflare Workers o un Node.js propio en Railway es algo que depende de cómo cada plataforma maneja los headers internos.
Estos son límites reales. Una guía que no los nombra no es honesta.
FAQ: OpenTelemetry en Next.js App Router
¿Puedo usar OpenTelemetry en el edge runtime de Next.js?
Parcialmente. El NodeSDK completo no corre en el edge runtime porque depende de APIs de Node.js que no están disponibles en ese entorno. Lo que sí podés hacer es propagar manualmente los headers traceparent y tracestate en el Middleware para que el Node.js Runtime pueda continuar el trace con el mismo trace ID. Para generar spans reales en el edge, necesitarías una librería de telemetría diseñada específicamente para entornos sin Node (algo que al momento de escribir esto todavía no tiene una solución madura y oficial en el ecosistema OTel JS).
¿Qué propagador tengo que usar para que los traces sobrevivan entre Middleware y Server Components?
W3CTraceContextPropagator. Este es el propagador que lee y escribe los headers traceparent y tracestate del estándar W3C TraceContext, que es el formato que el ecosistema moderno usa para propagar contexto entre servicios. Sin configurarlo explícitamente en el NodeSDK, el SDK no sabe cómo leer el contexto que viene del Middleware y arranca un trace nuevo sin parent.
¿OpenTelemetry en Next.js es compatible con Jaeger, Grafana Tempo y similares?
Sí, siempre que uses el exporter correcto y apuntes al puerto correcto del collector. OTLPTraceExporter (HTTP, puerto 4318) o OTLPTraceExporter con gRPC (puerto 4317) funciona con cualquier collector que implemente el protocolo OTLP: Jaeger, Grafana Tempo, Zipkin (con el exporter específico), Honeycomb, DataDog, etc. La configuración del exporter es independiente del collector que uses.
¿Las Server Actions generan spans automáticamente?
No. Next.js instrumenta automáticamente algunas operaciones internas (fetch, rendering de páginas, algunas operaciones de cache), pero las Server Actions son código de aplicación. Si querés trazar la lógica dentro de una Server Action, necesitás crear spans manualmente usando el tracer.startActiveSpan() de la API de OpenTelemetry.
¿Cómo sé si el contexto de trace se está propagando correctamente?
La forma más directa es mirar el collector. Si ves dos traces separados para una request que debería ser una sola cadena (por ejemplo, una request que pasa por el Middleware y llega a un Server Component), el contexto se está rompiendo. En Jaeger podés buscar por traceparent en los atributos o simplemente verificar que el trace ID sea el mismo en todos los spans de una request.
¿Necesito instrumentation.ts si mi app no usa el edge runtime?
Si todo corre en Node.js runtime (sin Middleware ni edge routes), instrumentation.ts es suficiente y la complejidad se reduce bastante. El problema de propagación es específico de la frontera edge/node. Si no cruzás esa frontera, la instrumentación automática de Next.js junto con el NodeSDK configurado con W3CTraceContextPropagator debería darte traces funcionales con poco esfuerzo adicional.
Mi postura: instrumentá temprano, no cuando algo se rompa
Cuando trabajé observabilidad desde el lado del backend en Java con Spring Boot, la ventaja era que el framework te daba mucho sin configurar. Next.js es diferente: la arquitectura del App Router con su frontera edge/node crea una discontinuidad que no existe en un servidor clásico, y OpenTelemetry no la resuelve automáticamente.
Lo incómodo es que el silencio del sistema cuando el contexto se rompe hace que mucha gente asuma que la instrumentación está funcionando hasta que necesita debuggear algo serio en producción y descubre que los traces están fragmentados.
Mi recomendación concreta: si estás usando App Router con Middleware, configurá el W3CTraceContextPropagator desde el principio y verificá en el collector que los spans de una request formen una cadena coherente antes de necesitarlo. Es mucho más barato hacer esa verificación en desarrollo que reconstruirla bajo presión.
El próximo paso práctico: levantá un Jaeger local con Docker (docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest), configurá el OTLPTraceExporter apuntando a http://localhost:4318/v1/traces, y verificá manualmente que una request que pase por el Middleware y llegue a un Server Component aparezca como un solo trace con spans encadenados. Si aparecen dos traces, el propagador está mal configurado. Si aparece uno, estás bien.
Esa verificación visual es la prueba de fuego que no puede reemplazar ninguna guía.
Fuentes originales:
- Next.js OpenTelemetry documentation: https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry
- OpenTelemetry JS SDK: https://opentelemetry.io/docs/languages/js/
Artículos Relacionados
My Homelab AI Dev Platform: qué problema real señala y dónde están los límites
La comunidad de homelabbers está armando plataformas de desarrollo con IA local y la discusión está buena. Yo tengo algunas observaciones que van más allá del entusiasmo inicial — y un checklist para que vos decidás si vale la pena el experimento.
The Birth and Death of JavaScript (2014): qué sigue siendo verdad y qué ya no
Una charla de 2014 predijo que JavaScript moriría reemplazado por ASM.js. Una década después, JS sigue vivo pero la tensión que señaló es más real que nunca. Esto es lo que conviene extraer, lo que hay que ignorar y cómo convertirlo en una decisión técnica concreta.
Métodos formales y el futuro de la programación: qué vale la pena probar y dónde está el techo
Formal methods aparece cada tanto en el radar técnico como la solución que la industria ignoró. Mi lectura: el problema que señala es real, pero la receta que circula omite costos que cambian la ecuación.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.