Saltar al contenido

Buenas prácticas de logging para sistemas en producción

Logging estructurado, niveles de log y correlation IDs: los patrones que convierten una salida ruidosa en observabilidad buscable y accionable.

4 min de lectura
Salida de log JSON estructurado en una terminal que muestra correlation IDs de solicitudes

La mayoría del logging en producción cae en dos categorías: demasiado ruido para ser útil, o muy poca información para depurar nada. Un buen logging es una decisión de diseño, no algo que se piensa al final. Requiere elegir el formato correcto, el nivel correcto y el contexto correcto para cada declaración de log.

Logs estructurados en vez de texto plano

Los logs de texto plano son legibles para humanos en una terminal e inútiles en cualquier otro lugar. Las herramientas de agregación de logs como Datadog, Elastic y CloudWatch necesitan datos estructurados para filtrar, buscar y generar alertas.

tstypescript
// ❌ Plain text — impossible to parse or filter at scale
console.log(`User ${userId} placed order ${orderId} for $${total}`);
// "User cust_123 placed order ord_456 for $99.50"
 
// ✅ Structured JSON — every field is searchable
logger.info("Order placed", {
  userId: "cust_123",
  orderId: "ord_456",
  total: 99.5,
  currency: "USD",
  itemCount: 3,
});
// {"level":"info","msg":"Order placed","userId":"cust_123","orderId":"ord_456","total":99.5,"currency":"USD","itemCount":3,"timestamp":"2020-02-19T14:30:00Z"}

Con logs estructurados, puedes consultar "muéstrame todas las órdenes de más de $100 del usuario cust_123" sin acrobacias con regex.

Niveles de log que significan algo

La mayoría de los equipos usan los niveles de log de forma inconsistente. Define semánticas claras y hazlas cumplir en el code review.

NivelCuándo usarloEjemplo
errorAlgo falló y necesita atención humanaFalló el cobro de un pago, se perdió la conexión a la base de datos
warnAlgo inesperado pero manejadoSe acerca al rate limit, se llamó a una API obsoleta
infoEventos de negocio significativosOrden realizada, usuario registrado, despliegue iniciado
debugDetalles internos para troubleshootingCache hit/miss, tiempos de query, estado intermedio
tstypescript
// ❌ Wrong levels — error used for non-errors, info used for noise
logger.error("User not found"); // This is expected behavior, not an error
logger.info(`Cache key: ${key}`); // Debug-level detail flooding production
 
// ✅ Correct levels — each level has clear semantics
logger.warn("User not found", { userId, endpoint: "/api/profile" });
logger.debug("Cache lookup", { key, hit: false, latencyMs: 2 });

En producción, configura el nivel mínimo en info. Habilita debug temporalmente cuando investigues un problema específico. Nunca dejes debug corriendo permanentemente — genera demasiado volumen y cuesta dinero real en almacenamiento de logs.

Correlation IDs para el trazado de solicitudes

Cuando una sola solicitud de usuario toca múltiples servicios, los correlation IDs conectan toda la cadena.

tstypescript
import { randomUUID } from "crypto";
 
function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
  const requestId = req.headers["x-request-id"] ?? randomUUID();
 
  // Attach to response for client debugging
  res.setHeader("x-request-id", requestId);
 
  // Attach to request context for downstream logging
  req.requestId = requestId;
 
  next();
}
tstypescript
// Every log in the request lifecycle includes the same correlationId
function createRequestLogger(requestId: string) {
  return {
    info: (msg: string, data?: Record<string, unknown>) =>
      logger.info(msg, { ...data, requestId }),
    warn: (msg: string, data?: Record<string, unknown>) =>
      logger.warn(msg, { ...data, requestId }),
    error: (msg: string, data?: Record<string, unknown>) =>
      logger.error(msg, { ...data, requestId }),
  };
}
 
// In a route handler:
app.post("/api/orders", async (req, res) => {
  const log = createRequestLogger(req.requestId);
 
  log.info("Order creation started", { userId: req.userId });
 
  const order = await createOrder(req.body, log);
  log.info("Order created", { orderId: order.id });
 
  await chargePayment(order, log);
  log.info("Payment charged", { orderId: order.id });
 
  res.json(order);
});

Cuando algo falla, busca por requestId para ver la línea de tiempo completa de la solicitud a través de todos los servicios.

Qué loguear (y qué no)

Siempre loguear

  • Inicio y fin de la solicitud (con duración)
  • Eventos de negocio (orden realizada, pago procesado, acción de usuario)
  • Errores y excepciones con stack traces completos
  • Llamadas a APIs externas (con tiempo de respuesta y status)

Nunca loguear

  • Contraseñas, tokens, API keys o secretos
  • Números completos de tarjetas de crédito o SSNs
  • Cuerpos de solicitud que contengan datos sensibles de usuario
  • Solicitudes de health check (ahogan el tráfico real)
tstypescript
// ❌ Logging sensitive data
logger.info("User login", { email, password: req.body.password });
 
// ✅ Redact sensitive fields
logger.info("User login", { email, passwordProvided: !!req.body.password });

Logging de errores con contexto

Un log de error sin contexto es casi inútil. Incluye todo lo necesario para reproducir el problema.

tstypescript
// ❌ Bare error message — who was affected? What were the inputs?
logger.error("Payment failed");
 
// ✅ Full context — actionable without opening a debugger
try {
  await paymentGateway.charge(amount, paymentMethodId);
} catch (error) {
  logger.error("Payment charge failed", {
    userId,
    orderId,
    amount,
    paymentMethodId,
    gateway: "stripe",
    errorCode: error.code,
    errorMessage: error.message,
    stack: error.stack,
  });
  throw error;
}

El objetivo es que alguien leyendo el log a las 3 AM pueda entender qué pasó, quién se vio afectado y qué investigar — sin tener que desplegar nuevo logging.

Logging de rendimiento

Registra las operaciones lentas de forma proactiva. No esperes a que los usuarios reporten latencia.

tstypescript
function withTiming<T>(
  name: string,
  fn: () => Promise<T>,
  log: Logger,
  thresholdMs = 1000,
): Promise<T> {
  const start = performance.now();
 
  return fn().finally(() => {
    const durationMs = Math.round(performance.now() - start);
 
    if (durationMs > thresholdMs) {
      log.warn("Slow operation detected", { operation: name, durationMs });
    } else {
      log.debug("Operation completed", { operation: name, durationMs });
    }
  });
}
 
// Usage
const users = await withTiming("getActiveUsers", () => getActiveUsers(), log);

Este patrón expone las regresiones de rendimiento antes de que se conviertan en incidentes.

Puntos clave

  1. Usa logs JSON estructurados — el texto plano no es consultable a escala
  2. Define los niveles de log de forma consistente — error significa "necesita atención humana", no "pasó algo inesperado"
  3. Los correlation IDs conectan todo el ciclo de vida de una solicitud a través de los servicios
  4. Nunca loguees datos sensibles — contraseñas, tokens y PII son riesgos de seguridad en los almacenes de logs
  5. Incluye contexto completo en los logs de error — haz posible depurar a las 3 AM sin cambios de código
  6. Registra las operaciones lentas de forma proactiva — los umbrales de rendimiento detectan regresiones a tiempo
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX