Diagnóstico en Node.js: AsyncLocalStorage, diagnostics_channel, --prof y GC tracking

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP