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.
| pino | winston | |
|---|---|---|
| Descargas en npm, por semana | 54 millones | 28 millones |
| Formato | JSON, de entrada | El que configures (JSON con format.json()) |
| Velocidad | De los más rápidos: lo pesado lo hace en otro hilo | Más lento: todo pasa por su cadena de formatos |
| Mandar a otros lados | «Transports» en un hilo aparte | Muchos transportes, en el mismo proceso |
| Peticiones HTTP | pino-http | Con morgan o a mano |
| Legible en tu terminal | pino-pretty, solo en desarrollo | format.prettyPrint() o colorize() |
| Versión actual | 10.4 | 3.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:
- Los datos van en el objeto, no pegados al texto.
{ pedido: 'A-1042' }se puede filtrar; «pedido A-1042 pagado» no. - El error va en
err. Pino lo convierte en tipo, mensaje y stack. - El mensaje dice qué pasó, en palabras de negocio: «pedido pagado», no «ok» ni «paso 3».
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
- Contraseñas, tokens y llaves, ni en los headers:
authorizationycookieson los que más se cuelan. - Números de tarjeta, completos o casi completos.
- Datos personales que no necesitas para entender el problema: con el id del cliente basta, no hace falta su correo ni su teléfono.
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… |
|---|---|
| pm2 | pm2 logs, y en archivos en ~/.pm2/logs/ |
| Docker | docker logs <contenedor>; se pierden si borras el contenedor |
| systemd | journalctl -u tu-servicio |
| Vercel, Railway, Render | En su panel, por un tiempo limitado según tu plan |
| AWS Lambda | CloudWatch 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:
- Errores nuevos, agrupados: mil veces el mismo error es un aviso, no mil.
- Un servicio que se calló. Si tu worker escribía cada minuto y lleva una hora sin escribir, no hay ningún error que buscar: simplemente dejó de trabajar.
- Un proceso que empezó y no terminó. El pedido pagado que nunca se surtió no escribe ninguna línea de error.
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.
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.