Blog · 12 de octubre de 2026

Logs en Node.js en producción: pino o winston, y lo que casi nadie te dice

Cuál librería elegir, cómo configurarla en cinco minutos, dónde quedan tus logs, cómo seguir una petición de punta a punta y cómo enterarte cuando algo deja de escribir. Con código que puedes copiar.

1. La respuesta corta: pino o winston

Para un proyecto nuevo, pino. Escribe JSON desde el principio, es de los más rápidos y hoy es el más descargado. Si ya tienes winston y funciona, no lo cambies: configúralo para que escriba JSON con el stack de cada error y listo.

pinowinston
Descargas en npm, por semana54 millones28 millones
FormatoJSON, de entradaEl que configures (JSON con format.json())
VelocidadDe los más rápidos: lo pesado lo hace en otro hiloMás lento: todo pasa por su cadena de formatos
Mandar a otros lados«Transports» en un hilo aparteMuchos transportes, en el mismo proceso
Peticiones HTTPpino-httpCon morgan o a mano
Legible en tu terminalpino-pretty, solo en desarrolloformat.prettyPrint() o colorize()
Versión actual10.43.19

Descargas de api.npmjs.org, semana del 5 al 11 de octubre de 2026; versiones del registro de npm, el mismo día. Muchas guías todavía dicen que winston se descarga más: ya no es así.

¿Y morgan? No compite con ninguno de los dos: solo escribe una línea por cada petición HTTP. Si usas pino, pino-http hace lo mismo y en el mismo formato que el resto de tus logs.

2. ¿Y console.log?

En producción, console.log tiene tres problemas: no tiene niveles (no puedes apagar el «debug» sin tocar código), escribe texto libre (difícil de buscar) y no dice de qué petición viene cada línea.

Pero si tu app ya está llena de console.log, no tienes que reescribirla para empezar. Lo importante es que esas líneas lleguen a un lugar donde se puedan buscar. Las nuevas, escríbelas con pino.

3. Pino en cinco minutos

npm install pino
npm install --save-dev pino-pretty
// logger.js
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  // legible en tu terminal; JSON puro en producción
  transport: process.env.NODE_ENV === 'production' ? undefined : { target: 'pino-pretty' },
  // lo que nunca debe quedar escrito
  redact: ['req.headers.authorization', '*.password', '*.tarjeta'],
});
import { logger } from './logger.js';

logger.info({ pedido: 'A-1042', monto: 640 }, 'pedido pagado');
logger.warn({ pedido: 'A-1042' }, 'sin existencias, se surte mañana');

try {
  await surtir(pedido);
} catch (err) {
  logger.error({ err, pedido: pedido.id }, 'no se pudo surtir');   // con su stack
}

// un logger por módulo, con su etiqueta en cada línea
const log = logger.child({ modulo: 'pagos' });

Tres reglas que te ahorran horas después:

4. Si ya usas winston

Lo que más se olvida: sin format.errors({ stack: true }), el error llega sin stack. Y si lo metes dentro de un objeto ({ error: err }), también.

import winston from 'winston';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json(),
  ),
  transports: [new winston.transports.Console()],
});

// el error como segundo argumento: así llega con su stack
logger.error('no se pudo surtir', err);
// y los datos, con un logger hijo
logger.child({ pedido: 'A-1042' }).warn('sin existencias');

5. Cada petición de Express, con su id

npm install pino-http
import pinoHttp from 'pino-http';
import { randomUUID } from 'node:crypto';
import { logger } from './logger.js';

app.use(pinoHttp({
  logger,
  // respeta el id que traiga tu balanceador; si no, uno nuevo
  genReqId: (req) => req.headers['x-request-id'] || randomUUID(),
}));

app.post('/pedidos/:id/pagar', async (req, res) => {
  req.log.info({ pedido: req.params.id }, 'cobrando');   // ya lleva el id de la petición
  // …
});

Cada petición escribe una línea al terminar, con método, ruta, código y cuánto tardó, y todo lo que escribas con req.log lleva el mismo id. Cuando un cliente diga «me falló a las 3:12», buscas ese id y ves todo lo que pasó.

6. El id en todo tu código, sin pasarlo a mano

req.log sirve en la ruta, pero tu función de cobro, tres archivos más abajo, no tiene req. En vez de pasarlo de mano en mano, guárdalo en el contexto de la petición con AsyncLocalStorage, que viene en Node:

import { AsyncLocalStorage } from 'node:async_hooks';

const contexto = new AsyncLocalStorage();

// después de pinoHttp
app.use((req, res, next) => contexto.run(req.log, next));

// en cualquier parte de tu código
export const log = () => contexto.getStore() ?? logger;

log().info({ pedido }, 'cargo aprobado');   // con el id de la petición que lo pidió

Y si la petición pasa por varios servicios, el estándar para llevar el mismo id de uno a otro es la cabecera traceparent de W3C.

7. Lo que no va en tus logs

Con pino, redact los quita antes de escribir (arriba, en el paso 3). Con winston, un format propio que borre esos campos.

8. ¿Dónde quedan mis logs?

Tu app debe escribir a la salida estándar (stdout) y dejar que el lugar donde corre los guarde. Dónde quedan, según dónde corre:

Si corre en…Tus logs están en…
pm2pm2 logs, y en archivos en ~/.pm2/logs/
Dockerdocker logs <contenedor>; se pierden si borras el contenedor
systemdjournalctl -u tu-servicio
Vercel, Railway, RenderEn su panel, por un tiempo limitado según tu plan
AWS LambdaCloudWatch Logs

El problema de todos: están en un solo servidor o por poco tiempo, y nadie los está mirando. Sirven para investigar algo que ya sabes que pasó, no para enterarte de que pasó.

9. Que alguien los lea por ti

Lo último que falta, y lo que casi ninguna guía de logs cubre: enterarte sin abrirlos. Tres cosas que vale la pena vigilar:

Con Estiaje, tus logs de pino o winston llegan con una línea más, sin cambiar cómo escribes:

import { estiajeFromEnv } from '@estiaje/sdk';
const estiaje = estiajeFromEnv({ service: 'tienda' });

// pino: a tu terminal y a Estiaje
const logger = pino(pino.multistream([
  { stream: process.stdout },
  { stream: estiaje.destino() },
]));

// winston: un transporte más
logger.add(new winston.transports.Stream({ stream: estiaje.destino() }));

// ¿todavía con console.log? también llegan
estiaje.capturarConsola();

Los buscas por texto y los filtras por servicio, nivel o cualquier campo. Los errores llegan agrupados con su stack, el servicio que se calla te avisa al teléfono y, si le dices qué proceso debe terminar, también te avisa del que se quedó a medias, con el dinero en riesgo. Los logs vienen desde el plan Gratis.

Prueba 14 días de Pro, sin tarjeta Al terminar, sigues en Gratis con tus logs y tus errores.

Preguntas frecuentes

¿Pino o winston?

Para un proyecto nuevo, pino: escribe JSON de entrada, es de los más rápidos y hoy se descarga el doble que winston (54 contra 28 millones por semana en npm, octubre de 2026). Si ya usas winston y funciona, quédate con él y configúralo para escribir JSON con el stack de cada error.

¿Es malo usar console.log en producción?

No tiene niveles, escribe texto libre y no dice de qué petición viene cada línea. No hace falta reescribir la app: lo importante es que esas líneas lleguen a un lugar donde se puedan buscar, y escribir las nuevas con una librería.

¿Dónde se guardan los logs de Node.js?

Node no los guarda: los escribe a la salida estándar y los guarda lo que lo corre. Con pm2, en ~/.pm2/logs/; con Docker, en docker logs; con systemd, en journalctl; en Vercel, Railway o Render, en su panel.

¿Cómo uso pino-pretty solo en desarrollo?

Con transport: { target: 'pino-pretty' } solo cuando NODE_ENV no es production, e instálalo como dependencia de desarrollo. En producción, pino escribe JSON puro, que es lo que quieres guardar y buscar.

Descargas de api.npmjs.org y versiones del registro de npm, consultadas el 12 de octubre de 2026.

Sigue leyendo