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.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
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 el hot path difiriendo el formatting a un worker thread via transports. Esto lo hace 3-10x más rápido que las alternativas en benchmarks.
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 log aggregator (ELK, Loki, Datadog)
- Apps Express o Fastify que necesitan middleware de request logging
- Reemplazar
console.logcon output estructurado y filtrable
When NOT to Use
- CLI tools que necesitan output human-readable coloreado — usa
winstonconConsoletransport - Scripts donde
console.loges suficiente — pino añade una dependencia - Aplicaciones que necesitan routing de logs complejo (múltiples archivos, alertas por email) — Winston es más flexible
Solution
Setup
npm install pino
Logging básico
const pino = require("pino");
const logger = pino({
level: "info",
});
logger.info("server_started", { port: 3000 });
logger.warn("cache_miss", { key: "user:42" });
logger.error("database_error", { error: "Connection refused" });
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("processing_order", { order_id: "ord_456" });
requestLogger.info("order_validated", { items: 3 });
requestLogger.warn("inventory_low", { sku: "widget-001", stock: 2 });
Cada línea de log de requestLogger incluye request_id y user_id.
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("server_started", { port: 3000 });
Instala pino-pretty separado: npm install -D 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);
Instala pino-http: 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 built in — sin setup 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 key err con type, message y stack.
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);
}
Best Practices
-
For a deeper guide, see Rotate Logs Daily with Winston.
-
Usa
pino-prettysolo en desarrollo — añade overhead en producción -
Usa child loggers para context scoped al request en lugar de repetir campos en cada llamada
-
Setea
levelvia environment variable:level: process.env.LOG_LEVEL || "info" -
Usa paths de
redactpara strippear campos sensibles — no confíes en que los desarrolladores recuerden -
Usa serializers para objetos complejos (req, res, err) para controlar qué se loguea
-
Envía JSON a stdout en producción — deja que un log collector maneje la escritura a archivo y rotación
-
Usa
pino-httppara Express o logging built-in 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 error-prone. Bindea una vez conchild(). - Loguear objetos grandes: pino serializa sincrónicamente. Loguear un objeto de 10MB bloquea el event loop. Usa serializers para trimar.
- No setear
level: el default esinfo. Si necesitasdebugen staging, setealo via env var. - Usar
console.logjunto a pino:console.logescribe texto no estructurado a stdout, mezclándolo con el JSON de pino. Elimina todos losconsole.log.
FAQ
¿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?
Pipea stdout a un log collector (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 el hot path). Puedes llamar logger.info() dentro de funciones async sin issues.
¿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, soporta múltiples outputs). 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]. Con remove: true elimina la key completamente del output del log.
Recursos Relacionados
Structured JSON Logging with structlog
How to emit structured JSON logs in Python using structlog, including context binding, log levels, processors, and integration with standard logging.
RecipeError Tracking with Sentry in Express
How to integrate Sentry for error tracking in Node.js Express applications, including error handlers, performance monitoring, release tracking, and source maps.
RecipeRotate Logs Daily with Winston
How to configure daily log rotation in Node.js using winston and winston-daily-rotate-file, including size limits, retention, compression, and transport combining.