Node.js incluye herramientas nativas de diagnóstico que van mucho más allá del simple console.log. AsyncLocalStorage propaga contexto por cadenas asíncronas sin pasar parámetros, diagnostics_channel permite observabilidad desacoplada, y las herramientas de profiling de V8 revelan cuellos de botella reales en producción.
AsyncLocalStorage: contexto de request sin prop drilling
En aplicaciones web, necesitas propagar información de contexto (el ID de la petición, el usuario autenticado, el trace ID) a través de toda la cadena asíncrona sin tener que pasarlo como argumento en cada función. AsyncLocalStorage resuelve esto de forma transparente:
import { AsyncLocalStorage } from 'async_hooks';
// Crear el store del contexto
const contextoRequest = new AsyncLocalStorage();
// Middleware Express que inicializa el contexto
function middlewareContexto(req, res, next) {
const contexto = {
requestId: crypto.randomUUID(),
usuario: req.user?.id ?? 'anónimo',
inicio: Date.now(),
logs: [],
};
contextoRequest.run(contexto, () => {
next(); // Todo el código en la cadena de middlewares ve este contexto
});
}
// Logger que obtiene el requestId automáticamente
function log(nivel, mensaje, datos = {}) {
const ctx = contextoRequest.getStore();
const entrada = {
timestamp: new Date().toISOString(),
requestId: ctx?.requestId ?? 'sin-contexto',
usuario: ctx?.usuario,
nivel,
mensaje,
...datos,
};
console.log(JSON.stringify(entrada));
ctx?.logs.push(entrada);
}
// Función de negocio no necesita recibir el contexto como argumento
async function obtenerPedidos(userId) {
log('info', 'Consultando pedidos', { userId });
const pedidos = await db.query('SELECT * FROM pedidos WHERE user_id = $1', [userId]);
log('info', 'Pedidos obtenidos', { count: pedidos.length });
return pedidos;
}
// Ruta Express
app.get('/pedidos', middlewareContexto, async (req, res) => {
const pedidos = await obtenerPedidos(req.query.userId);
const ctx = contextoRequest.getStore();
log('info', 'Request completada', { duracion: Date.now() - ctx.inicio });
res.json(pedidos);
});
diagnostics_channel: observabilidad desacoplada
diagnostics_channel permite publicar eventos de diagnóstico que cualquier observador externo puede escuchar sin que el código de la librería o la aplicación sepa que alguien está escuchando. Es la base de la instrumentación automática de herramientas como OpenTelemetry:
import diagnostics_channel from 'diagnostics_channel';
// Librería de HTTP publica eventos sin saber quién los escucha
const canalHTTP = diagnostics_channel.channel('mi-app:http:request');
async function hacerRequest(url, opciones = {}) {
const inicio = performance.now();
// Publicar evento de inicio (cualquier observador puede escucharlo)
if (canalHTTP.hasSubscribers) {
canalHTTP.publish({ tipo: 'inicio', url, metodo: opciones.method ?? 'GET' });
}
try {
const res = await fetch(url, opciones);
const duracion = performance.now() - inicio;
if (canalHTTP.hasSubscribers) {
canalHTTP.publish({ tipo: 'fin', url, status: res.status, duracion });
}
return res;
} catch (err) {
if (canalHTTP.hasSubscribers) {
canalHTTP.publish({ tipo: 'error', url, error: err.message });
}
throw err;
}
}
// Observador externo (APM, OpenTelemetry, logging) desacoplado del código anterior
diagnostics_channel.subscribe('mi-app:http:request', (datos) => {
if (datos.tipo === 'fin') {
metricas.histogram('http.request.duration', datos.duracion, {
url: datos.url,
status: datos.status,
});
}
});
Profiling de CPU con --prof
// Ejecutar con el profiler de V8:
// node --prof mi-servidor.js
// Generar carga durante unos segundos, luego detener con Ctrl+C
// Esto genera un archivo isolate-XXXX-v8.log
// Procesar el log:
// node --prof-process isolate-XXXX-v8.log > perfil-legible.txt
// O generar directamente en formato cpuprofile para Chrome DevTools:
// node --cpu-prof mi-servidor.js
// Genera: CPU.FECHA.cpuprofile abrir en Chrome DevTools ? Performance
// Profiling programático
import v8 from 'v8';
import { writeFileSync } from 'fs';
// Después de cargar el módulo v8-profiler-next:
// const profiler = require('v8-profiler-next');
// profiler.startProfiling('API Profile', true);
// ... operaciones a perfilar ...
// const profile = profiler.stopProfiling('API Profile');
// profile.export((err, data) => writeFileSync('api.cpuprofile', data));
GC Tracking con PerformanceObserver
import { PerformanceObserver } from 'perf_hooks';
// Observar eventos de GC
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const tipo = entry.detail?.kind === 1 ? 'Major GC' :
entry.detail?.kind === 2 ? 'Minor GC' : 'GC';
console.log(`${tipo}: ${entry.duration.toFixed(2)}ms`);
// Alertar si el GC está tardando demasiado (sign de memory pressure)
if (entry.duration > 100) {
console.warn(`GC lento detectado: ${entry.duration.toFixed(0)}ms`);
}
}
});
obs.observe({ entryTypes: ['gc'] });
// Forzar GC para pruebas (requiere --expose-gc)
// node --expose-gc mi-script.js
if (global.gc) {
global.gc();
console.log('GC forzado');
}
Heap snapshots con v8.writeHeapSnapshot
import v8 from 'v8';
import { writeFileSync } from 'fs';
// Tomar un snapshot del heap en un momento concreto
function tomarHeapSnapshot(prefijo = 'heap') {
const nombre = `${prefijo}-${Date.now()}.heapsnapshot`;
v8.writeHeapSnapshot(nombre);
console.log(`Snapshot guardado: ${nombre}`);
return nombre;
}
// Comparar snapshots para detectar leaks
// Tomar uno antes:
tomarHeapSnapshot('antes');
// Ejecutar la operación sospechosa N veces
for (let i = 0; i < 100; i++) {
await operacionSospechosa();
}
// Tomar uno después:
tomarHeapSnapshot('despues');
// Abrir ambos en Chrome DevTools ? Memory ? Load para comparar
// En producción: tomar snapshot ante señal del sistema
process.on('SIGUSR2', () => {
tomarHeapSnapshot('produccion');
});
// kill -USR2 PID_DEL_PROCESO
PerformanceObserver para medir rendimiento de la app
import { performance, PerformanceObserver } from 'perf_hooks';
// Marcar puntos y medir entre ellos
async function procesarLote(ids) {
performance.mark('lote:inicio');
const resultados = [];
for (const id of ids) {
performance.mark(`item:${id}:inicio`);
resultados.push(await procesarItem(id));
performance.mark(`item:${id}:fin`);
performance.measure(`item:${id}`, `item:${id}:inicio`, `item:${id}:fin`);
}
performance.mark('lote:fin');
performance.measure('lote:total', 'lote:inicio', 'lote:fin');
return resultados;
}
// Observar las medidas
const obsPerf = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.startsWith('lote:')) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
}
});
obsPerf.observe({ entryTypes: ['measure'] });
Las herramientas de diagnóstico nativas de Node.js están diseñadas para usarse en producción con bajo overhead. AsyncLocalStorage es imprescindible en cualquier aplicación con trazado distribuido, diagnostics_channel es la base de la instrumentación moderna con OpenTelemetry, y el profiling de V8 es la forma más directa de encontrar cuellos de botella reales sin tener que adivinar qué parte del código es lenta.
