Logging de Alta Performance con pino
Cómo usar pino para logging estructurado JSON rápido en Node.js, incluyendo niveles de log, child loggers, transports e integración con Express y Fastify.
Overview
pino es el logger más rápido de Node.js — escribe JSON a stdout sincrónicamente con overhead mínimo. A diferencia de Winston o Bunyan, pino evita I/O async en la ruta crítica difiriendo el formateo a un worker thread via transports. Los benchmarks lo suelen situar 3-10x por delante de las alternativas.
When to Use
- APIs de alto throughput donde el overhead de logging importa (miles de requests por segundo)
- Microservicios que envían logs JSON a un agregador de logs (ELK, Loki, Datadog)
- Apps Express o Fastify que necesitan middleware de request logging
- Reemplazar
console.logcon salida estructurada y filtrable
When NOT to Use
- Herramientas de línea de comandos que necesitan salida legible con colores — usa
winstonconConsoletransport - Scripts donde
console.loges suficiente — pino añade una dependencia - Aplicaciones que necesitan enrutado de logs complejo (un archivo por nivel, alertas por email en errores) — Winston es más flexible
Solution
Setup
npm install pino
Logging básico
const pino = require("pino");
const logger = pino({
level: "info",
});
logger.info({ port: 3000 }, "server_started");
logger.warn({ key: "user:42" }, "cache_miss");
logger.error({ error: "Connection refused" }, "database_error");
La firma de pino es logger.info(mergingObject, message) — el objeto va primero. Si lo pasas después del string (al estilo Winston), se interpreta como argumento de interpolación printf y se descarta en silencio: los campos nunca llegan a la línea de log.
Output (JSON a stdout):
{"level":30,"time":1719900000000,"pid":1,"hostname":"api-01","msg":"server_started","port":3000}
Child loggers para context de request
const logger = pino();
const requestLogger = logger.child({ request_id: "req-abc-123", user_id: 42 });
requestLogger.info({ order_id: "ord_456" }, "processing_order");
requestLogger.info({ items: 3 }, "order_validated");
requestLogger.warn({ sku: "widget-001", stock: 2 }, "inventory_low");
Ambos campos viajan en cada línea que emite el child.
Pretty printing en desarrollo
const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:yyyy-mm-dd HH:MM:ss",
ignore: "pid,hostname",
},
},
});
logger.info({ port: 3000 }, "server_started");
Añádelo como dependencia de desarrollo: npm install -D pino-pretty. Una alternativa local más barata que ni toca tu proceso: node app.js | npx pino-pretty.
Niveles de log
const logger = pino({ level: "debug" });
logger.trace("very_detailed"); // level 10
logger.debug("debug_info"); // level 20
logger.info("general_info"); // level 30
logger.warn("warning"); // level 40
logger.error("error_occurred"); // level 50
logger.fatal("crash"); // level 60
Integración con Express
const express = require("express");
const pino = require("pino");
const pinoHttp = require("pino-http");
const logger = pino({ level: "info" });
const app = express();
app.use(pinoHttp({ logger }));
app.get("/api/users/:id", (req, res) => {
req.log.info({ user_id: req.params.id }, "fetching_user");
res.json({ id: req.params.id, name: "Alice" });
});
app.listen(3000);
pino-http es el middleware que lo conecta: npm install pino-http.
Integración con Fastify
const fastify = require("fastify")({
logger: {
level: "info",
transport: process.env.NODE_ENV === "development"
? { target: "pino-pretty" }
: undefined,
},
});
fastify.get("/api/users/:id", async (request, reply) => {
request.log.info({ user_id: request.params.id }, "fetching_user");
return { id: request.params.id, name: "Alice" };
});
fastify.listen({ port: 3000 });
Fastify tiene pino integrado — no necesita configuración extra.
Serializers personalizados
const logger = pino({
serializers: {
req(req) {
return {
method: req.method,
url: req.url,
headers: { "user-agent": req.headers["user-agent"] },
};
},
res(res) {
return { statusCode: res.statusCode };
},
err(err) {
return { type: err.constructor.name, message: err.message, stack: err.stack };
},
},
});
logger.info({ req: { method: "GET", url: "/api/users", headers: { "user-agent": "curl/8.0" } } }, "request_received");
File transport con rotación
const logger = pino({
transport: {
targets: [
{
target: "pino/file",
options: { destination: "./logs/app.log", mkdir: true },
level: "info",
},
{
target: "pino/file",
options: { destination: "./logs/errors.log", mkdir: true },
level: "error",
},
],
},
});
logger.info("goes_to_app_log");
logger.error("goes_to_both_logs");
Para rotación, usa pino-roll:
const logger = pino({
transport: {
target: "pino-roll",
options: {
file: "./logs/app",
frequency: "daily",
mkdir: true,
},
},
});
Redacción de campos sensibles
const logger = pino({
redact: {
paths: ["password", "api_key", "token", "headers.authorization", "*.credit_card"],
censor: "[REDACTED]",
},
});
logger.info({ user: { email: "alice@example.com", password: "secret123" } }, "user_login");
// Output: {"user": {"email": "alice@example.com", "password": "[REDACTED]"}, ...}
Logging de errores con stack traces
const logger = pino();
try {
JSON.parse("{invalid}");
} catch (err) {
logger.error({ err }, "json_parse_failed");
}
pino serializa automáticamente la clave err con type, message y stack. Para reporting de errores en producción con agrupación y alertas, combínalo con Sentry error tracking.
Variants
Usar pino con TypeScript
import pino from "pino";
const logger = pino({
level: "info",
base: { service: "user-api", version: "1.0.0" },
});
interface UserLogData {
user_id: number;
action: string;
}
function logUserAction(data: UserLogData): void {
logger.info(data, "user_action");
}
Usar pino con OpenTelemetry
const { trace, context } = require("@opentelemetry/api");
const logger = pino();
function logWithTrace(msg, data = {}) {
const span = trace.getSpan(context.active());
const traceId = span?.spanContext().traceId;
const spanId = span?.spanContext().spanId;
logger.info({ ...data, trace_id: traceId, span_id: spanId }, msg);
}
Explicación
pino saca su velocidad de dos decisiones de diseño. Primero, escribe JSON delimitado por líneas a stdout de forma sincrónica a través de un stream mínimo — no hay un pipeline de formateo entre logger.info() y la escritura. Segundo, los transports corren en un worker thread, así que enviar logs a archivos o servicios externos nunca bloquea tu event loop.
La contrapartida es que pino pide pensar distinto a Winston — menos en formatters, más en adónde va cada línea. Dos puntos de entrada importan:
pino.destination()— un stream sincrónico en bruto. La opción más rápida, pero con una sola salida.transport— corre el target (pino-pretty, pino-roll, shippers remotos) en un worker thread. Algo más lento por llamada, pero puedes repartir a varios targets sin frenar la app.
child() es barato: el nuevo logger comparte el stream del padre y simplemente fusiona los campos vinculados en cada línea. Es justo lo que hace pino-http por dentro — un child por request, sin que nadie repita request_id a mano.
pino-pretty es la excepción que confirma la regla: como transport inline formatea cada línea de forma sincrónica, así que sirve en desarrollo y perjudica bajo carga de producción. Si necesitas logs legibles en local sin coste dentro de la app, usa un pipe: node app.js | npx pino-pretty.
Un detalle que confunde a muchos: logger.info("msg", { data }) parece que debería funcionar — Winston acepta ese orden — pero pino trata el segundo argumento como interpolación estilo printf para el string del mensaje. Sin un placeholder %j, tus campos se descartan en silencio. Siempre logger.info({ data }, "msg").
Best Practices
-
Para rotación diaria de archivos, mira Rotate Logs Daily with Winston. Para código ejecutable, descarga los ejemplos del companion (config del logger, apps Express y Fastify).
-
Usa
pino-prettysolo en desarrollo — añade overhead en producción -
Usa child loggers para el contexto de la request en lugar de repetir campos en cada llamada
-
Establece
levelcon una variable de entorno:level: process.env.LOG_LEVEL || "info" -
Usa paths de
redactpara eliminar campos sensibles — no confíes en que los desarrolladores lo recuerden -
Usa serializers para objetos complejos (req, res, err) y controlar qué se registra
-
Envía JSON a stdout en producción — deja que un colector de logs maneje la escritura a archivo y la rotación
-
Usa
pino-httppara Express o el logging integrado de Fastify para logs automáticos de request/response
Common Mistakes
- Usar pino-pretty en producción: es sincrónico y bloquea el event loop. Solo úsalo en desarrollo.
- No usar child loggers: repetir
request_iden cadalogger.info()es propenso a errores. Vincula una vez conchild(). - Loguear objetos grandes: pino serializa sincrónicamente. Loguear un objeto de 10MB bloquea el event loop. Usa serializers para recortarlos.
- No establecer
level: el default esinfo. Si necesitasdebugen staging, establécelo con una variable de entorno. - Usar
console.logjunto a pino:console.logescribe texto no estructurado a stdout, mezclándolo con el JSON de pino. Elimina todos losconsole.log.
Preguntas frecuentes
¿Qué tan rápido es pino comparado con Winston?
En benchmarks, pino es 3-10x más rápido que Winston para logging JSON sincrónico. La diferencia viene de la escritura sincrónica de pino con formatting diferido vs la cadena async de Winston.
¿Cómo envío logs de pino a Elasticsearch?
Dirige stdout a un colector de logs (Filebeat, Fluent Bit, Logstash). El formato JSON ya está estructurado — sin reglas de parsing:
node app.js | filebeat --config=filebeat.yml
¿Puedo usar pino con async/await?
Sí. pino mismo es sincrónico (sin I/O async en la ruta crítica), así que llamar a logger.info() dentro de una función async es una escritura sincrónica más — nada que esperar con await.
¿Cómo agrego un middleware de request ID con pino?
const { randomUUID } = require("crypto");
app.use((req, res, next) => {
req.id = req.headers["x-request-id"] || randomUUID();
req.log = logger.child({ request_id: req.id });
next();
});
¿Cuál es la diferencia entre pino transports y pino destinations?
Destinations son writable streams (rápidos, sincrónicos). Transports corren en un worker thread (async, y pueden repartir a stdout más archivos o servicios remotos a la vez). Usa destinations para velocidad pura, transports para rutear a archivos o servicios externos.
¿Cómo redacto campos sensibles en logs de pino?
Usa la opción redact en la configuración de pino. Especifica paths a remover o censurar: redact: { paths: ['req.headers.authorization', 'password', '*.token'], remove: false }. Con remove: false reemplaza el valor con [Redacted] (el censor por defecto — el ejemplo de arriba muestra uno personalizado). Con remove: true elimina la clave completamente del log.
Recursos Relacionados
Logging Estructurado
Implementa logging estructurado con salida JSON, correlation IDs y agregación de logs para observabilidad en producción.
RecipeLogging Estructurado JSON con structlog
Cómo emitir logs estructurados JSON en Python usando structlog, incluyendo context binding, niveles de log, processors e integración con standard logging.
RecipeSeguimiento de Errores con Sentry en Express
Cómo integrar Sentry para seguimiento de errores en apps Node.js Express con el SDK v8+ — manejadores de error, spans de rendimiento, releases y source maps.
RecipeRotar Logs Diariamente con Winston
Cómo configurar rotación diaria de logs en Node.js usando winston y winston-daily-rotate-file, incluyendo límites de tamaño, retención, compresión y combinación de transports.