Next.js
Observabilidad, instrumentation y logging
Explica cómo conectar logs estructurados, métricas y traces mediante instrumentation.ts, request IDs, OpenTelemetry y contexto seguro de producción.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo conectar logs estructurados, métricas y traces mediante instrumentation.ts, request IDs, OpenTelemetry y contexto seguro de producción.
Observabilidad permite explicar qué ocurrió en una request, qué versión estaba desplegada, qué dependencia consumió tiempo y qué usuarios fueron afectados. No consiste en acumular console.log: conecta logs estructurados, métricas y traces mediante identificadores, contexto de release y políticas de privacidad.
user action
↓
request / navigation / Action
↓
Proxy → route → data layer → database/provider
↓
response / stream / client hydration
observability
├─ logs: eventos y contexto
├─ metrics: comportamiento agregado
└─ traces: recorrido y duración entre componentesLas tres señales responden preguntas diferentes:
Sin observabilidad, un reporte como “el dashboard tarda” obliga a adivinar:
¿CDN miss?
¿cold start?
¿Proxy?
¿consulta sin índice?
¿API externa?
¿streaming tardío?
¿bundle cliente?
¿hydration?Una buena instrumentación conserva evidencia suficiente para aislar la capa sin registrar información sensible.
Next.js reconoce un archivo instrumentation.ts o .js en la raíz de la aplicación, o dentro de src cuando se utiliza esa carpeta:
instrumentation.ts
app/
next.config.tsPuede exportar register, que se ejecuta una vez cuando una nueva instancia del servidor Next.js comienza:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./src/observability/node");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./src/observability/edge");
}
}Úsalo para inicializar:
No cargues automáticamente un SDK exclusivo de Node en Edge. El import condicional evita incluir módulos incompatibles en el runtime equivocado.
register() puede ejecutarse en múltiples instancias, regiones o reinicios:
instance A starts → register
instance B starts → register
cold start C → registerNo lo uses para:
Es inicialización del proceso, no una garantía global de una sola ejecución.
La convención actual permite exportar onRequestError para observar errores de servidor asociados con requests:
import type { Instrumentation } from "next";
export const onRequestError: Instrumentation.onRequestError = async (
error,
request,
context,
) => {
await reportServerError({
error,
method: request.method,
path: request.path,
routerKind: context.routerKind,
routePath: context.routePath,
routeType: context.routeType,
renderSource: context.renderSource,
});
};La firma y los campos disponibles dependen de la versión exacta; tipar mediante Instrumentation.onRequestError permite detectar cambios.
Esta función complementa error.tsx: una boundary ofrece recuperación visual; onRequestError registra el fallo servidor.
Cuando un SDK proporciona su propio handler, envuélvelo o encadénalo conscientemente. Exportar otra función con el mismo nombre reemplaza la anterior:
const providerHandler = createProviderRequestErrorHandler();
export const onRequestError: Instrumentation.onRequestError = async (...args) => {
await providerHandler(...args);
await writeAuditSafeError(...args);
};Controla errores del propio reporter para que una caída del sistema de observabilidad no derribe la request.
OpenTelemetry modela traces, spans, métricas y propagación de contexto con un estándar abierto.
Next.js incluye instrumentación automática para partes del framework cuando existe un SDK compatible. Una opción sencilla es utilizar un paquete de integración oficial o configurar el SDK manualmente:
// src/observability/node.ts
import { registerOTel } from "@vercel/otel";
registerOTel({
serviceName: "nicoo-next-app",
});En una infraestructura propia puedes configurar exporters OTLP, sampling y resource attributes.
trace: carga de /dashboard
├─ Proxy check 3 ms
├─ verifySession 12 ms
├─ getDashboard 180 ms
│ ├─ PostgreSQL metrics 45 ms
│ └─ billing API 120 ms
└─ render shell 22 msUn trace agrupa el flujo completo. Cada span representa una operación y contiene:
No crees un span por cada elemento de una lista; el volumen y coste crecerían sin aportar diagnóstico.
Cuando una operación de dominio no queda instrumentada automáticamente:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("orders");
export async function createOrder(input: CreateOrderInput) {
return tracer.startActiveSpan("orders.create", async (span) => {
try {
span.setAttribute("orders.item_count", input.items.length);
const result = await orderRepository.create(input);
span.setStatus({ code: 1 });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: 2 });
throw error;
} finally {
span.end();
}
});
}No añadas email, nombre, token ni body completo como atributo. Los traces pueden exportarse a proveedores externos.
Un request ID permite unir logs que no están en el mismo trace o sistema:
Proxy generates req_123
→ request header x-request-id
→ Route Handler / Server Component
→ DB/service logs
→ response header
→ support ticketProxy:
export function proxy(request: NextRequest) {
const requestId = crypto.randomUUID();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-request-id", requestId);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
response.headers.set("x-request-id", requestId);
return response;
}Sobrescribe un header enviado directamente por el cliente salvo que una infraestructura confiable ya lo haya normalizado.
En Node.js puedes conservar contexto por request sin pasarlo por cada función:
import { AsyncLocalStorage } from "node:async_hooks";
type RequestContext = {
requestId: string;
release: string;
};
export const requestContext = new AsyncLocalStorage<RequestContext>();Usa una integración probada con el lifecycle del framework. Un global mutable normal mezclaría requests concurrentes.
En Edge u otros runtimes, el mecanismo disponible puede ser diferente; OpenTelemetry context suele ser una abstracción más portable.
En vez de:
console.log("Order failed", orderId, error);Produce un evento estructurado:
logger.error({
event: "order.create.failed",
requestId,
organizationId,
orderId,
errorClass: error.name,
durationMs,
release: process.env.APP_RELEASE,
});Campos útiles:
Evita mensajes libres como único formato; son difíciles de agrupar.
Nunca registres automáticamente:
Crea una allowlist de campos, no una blacklist interminable:
function safeRequestLog(request: Request) {
return {
method: request.method,
pathname: new URL(request.url).pathname,
userAgentFamily: classifyUserAgent(request.headers.get("user-agent")),
};
}Los query params pueden contener PII; no registres la URL completa por defecto.
debug: detalle temporal, normalmente sampleado o deshabilitado en producción.info: eventos normales relevantes.warn: degradación o estado inesperado recuperable.error: operación fallida que requiere atención.No marques cada validación de usuario como error. Aumenta ruido y alertas falsas.
Tipos principales:
orders_created_total
server_action_failures_totalrequest_duration_ms
query_duration_ms
rsc_payload_bytesqueue_depth
active_connectionsUsa nombres y unidades consistentes. Las métricas agregadas no deben llevar IDs únicos como labels.
Mala métrica:
request_duration{user_id="every unique user"}Crea una serie por usuario y puede volver caro/inestable el sistema.
Labels adecuados:
IDs individuales pertenecen a logs/traces, no a métricas agregadas.
Un Client Component pequeño puede reportar métricas reales:
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
sendWebVital({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id,
path: window.location.pathname,
release: window.__APP_RELEASE__,
});
});
return null;
}Usa un endpoint rápido o sendBeacon cuando sea apropiado. Samplea y evita enviar search params sensibles.
Agrupa por route template, dispositivo y release para encontrar regresiones.
error.tsx puede reportar errores capturados. También existen:
Configura source maps para reconstruir stack traces. Una extensión del navegador puede causar ruido; registra browser, release y si el stack pertenece a tu bundle.
Next.js puede ocultar detalles del error servidor y exponer un digest. Muestra al usuario un mensaje seguro y un código de referencia:
No pudimos completar la operación.
Código: abc123El digest no sustituye tu request ID, pero ayuda a correlacionar el error con logs del framework.
Opciones:
Revisa si contienen source code, comentarios o rutas internas. Sin source maps, los errores minificados pueden ser casi imposibles de diagnosticar.
export async function getAccessibleOrder(input: Input) {
const startedAt = performance.now();
try {
const order = await repository.findAccessible(input);
metrics.orderLookupDuration.observe(performance.now() - startedAt);
return order;
} catch (error) {
logger.error({ event: "order.lookup.failed", errorClass: error.name });
throw error;
}
}No dupliques la misma excepción en cinco capas. Una capa registra contexto de dominio y el handler global registra stack/request. Define ownership de cada log.
Registra por proveedor:
No registres el payload completo. Usa spans para ver dónde aparece el tiempo.
Métricas:
cache_hit_total{cache="products"}
cache_miss_total
cache_fill_duration_ms
cache_revalidation_failure_totalLogs de invalidación incluyen tags afectadas y mutation, sin valores sensibles.
Una cache hit ratio alta no basta si sirve datos incorrectos; acompaña con pruebas de consistencia.
No bloquees una respuesta crítica esperando un proveedor de observabilidad. Usa batching o plataforma cuando sea seguro.
No necesitas conservar cada trace de una ruta de alto volumen.
Políticas:
Sampling debe ocurrir de forma coordinada para no conservar spans hijos sin trace padre.
Una alerta útil se vincula a impacto:
checkout success rate < 99.5%
order Action p95 > 1.5 s
error budget burn > threshold
queue lag > 5 minNo alertes por un único error aislado de baja prioridad. Define:
Una vista operativa debe responder:
Evita dashboards con cientos de gráficos sin pregunta operacional.
alert
→ inspect release and scope
→ trace representative request
→ mitigate / rollback / feature flag
→ communicate
→ root cause
→ preventive action and monitorLos logs deben conservar la ventana necesaria para investigar. Las feature flags necesitan owner y expiración.
instrumentation.ts se registra en build/runtime esperado.onRequestError recibe errores de Server Components, Actions y Handlers relevantes.No hay estructura, correlación ni retención clara.
Filtra credenciales y PII.
Explota cardinalidad.
Duplica ruido.
Su caída afecta producto.
No identificas regresiones.
El tráfico cambia; usa tasas y SLO.
Se ejecuta por instancia, no una vez global.
instrumentation.ts inicializa por instancia.onRequestError centraliza errores servidor bajo la API actual.register() no sirve para una migración única?error.tsx?onRequestError/logging servidor para la causa.Seguridad en Next.js utiliza esta visibilidad para modelar amenazas, controles y auditoría en cada frontera.