StackPractices
beginner Por Mathias Paulenko

Logging en producción con Python, JavaScript y Java

Implementa logging estructurado por niveles con IDs de correlación y enmascarado de secretos en Python, JavaScript y Java.

Visión general

Cuando producción falla a las 3 AM, los logs suelen ser la única evidencia disponible. El logging captura lo que hizo tu aplicación —eventos, errores, cambios de estado— y suele ser la única forma de reconstruir un incidente a posteriori.

Una configuración lista para producción es estructurada (JSON, no texto libre), basada en niveles (de DEBUG a FATAL) y lleva contexto como timestamps, IDs de request e IDs de usuario, sin filtrar secretos ni datos personales. Esta receta cubre configuraciones funcionales en Python, JavaScript y Java, además de las piezas operativas que hacen útiles a los logs: rotación, agregación, alertas y enmascarado.

Cuándo usarla

Usa esta receta cuando:

  • Necesitas depurar un problema de producción que no puedes reproducir en local.
  • Necesitas un solo lugar para vigilar errores, latencia y eventos de negocio.
  • Necesitas trazas de auditoría de las acciones de usuario para revisiones de seguridad o cumplimiento. Consulta la Guía de Seguridad para los requisitos de auditoría.
  • Necesitas trazar un request a través de varios servicios con un ID de correlación. Para captura de request/response con nivel de auditoría, consulta Logging y auditoría en APIs.
  • Piensas construir dashboards y alertas sobre los datos de log.

Sáltate el pipeline estructurado para un script desechable o una migración puntual: ahí print es suficiente. Invierte en logging de verdad cuando el código corre sin supervisión.

Solución

Python (Loguru)

from loguru import logger
import sys

logger.remove()
# Salida legible para desarrollo local
logger.add(sys.stdout, format="{time:HH:mm:ss} | {level} | {message}", level="INFO")
# Líneas JSON con rotación y escritura asíncrona para los colectores de producción
logger.add("app.log", serialize=True, rotation="10 MB", retention="7 days",
           level="DEBUG", enqueue=True)

logger.debug("Processing user {}", user_id)
logger.info("User {} logged in", user_id)
logger.warning("Rate limit at 90% for API key {}", api_key[:4])

try:
    raise ConnectionError("database timeout")
except ConnectionError:
    logger.exception("Database connection failed")

# Adjunta contexto que viaja con cada log posterior
logger.bind(request_id="abc-123", duration_ms=45).info("Request completed")

serialize=True escribe un objeto JSON por línea, que Filebeat, Promtail o el agente de CloudWatch pueden parsear sin reglas grok personalizadas. enqueue=True saca la escritura a disco del hilo del request — en Cómo funciona se explica por qué importa.

JavaScript (Winston)

const winston = require('winston');

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(),
    new winston.transports.File({ filename: 'app.log', maxsize: 10_000_000, maxFiles: 5 }),
  ],
});

logger.debug('Processing user', { userId });
logger.info('User logged in', { userId });
logger.warn('Rate limit at 90%', { apiKey: apiKey.slice(0, 4) });
logger.error('Database connection failed', { error });

Los metadatos pasados como segundo argumento quedan como campos JSON, así que requestId se vuelve consultable en lugar de quedar enterrado en el texto del mensaje.

Java (SLF4J + Logback)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    public void login(String userId) {
        MDC.put("requestId", RequestContext.currentRequestId());
        try {
            log.info("User {} logged in", userId);
            // ...
        } catch (Exception e) {
            log.error("Login failed for user {}", userId, e);
        } finally {
            MDC.clear();
        }
    }
}

Los placeholders {} posponen la concatenación de strings hasta que el nivel pasa el filtro, así que un DEBUG deshabilitado casi no cuesta nada. MDC guarda el ID de correlación en un mapa thread-local que Logback imprime en cada línea con %X{requestId} en el patrón del encoder.

Niveles de log

NivelCuándo usarloEjemplo
DEBUGInformación de diagnóstico detalladaValores de variables, iteraciones de bucles
INFOEventos normales de la aplicaciónRequests procesados, trabajos completados
WARNAnomalías recuperablesLlamada a API deprecada, límite de tasa cercano
ERROROperaciones fallidasTimeout de base de datos, archivo no encontrado
FATAL/CRITICALSistema inutilizableSin memoria, disco lleno

En producción el nivel por defecto debería ser INFO, con DEBUG activable por servicio o por request mediante un flag. Una aplicación que lo registra todo en DEBUG ahoga la señal y puede multiplicar fácilmente tu factura de almacenamiento.

Cómo funciona

Cada llamada de log recorre el mismo pipeline corto:

flowchart diagram: Código de la app

El paso que duele en producción es la escritura. Un append síncrono a archivo puede bloquear el hilo del request en I/O de disco; por eso cada framework ofrece una vía asíncrona: enqueue=True en Loguru, AsyncAppender en Logback y transports asíncronos en Winston. Si trabajas con contenedores, mejor loguea JSON a stdout y deja que la plataforma lo recolecte: sin archivos ni rotación que gestionar.

Variantes

LenguajeLibreríaElígela cuando
Pythonlogging (stdlib)Quieres cero dependencias y configuras los handlers tú mismo
PythonstructlogQuieres logs estructurados JSON-first — ver Logging JSON estructurado con structlog
PythonloguruQuieres rotación, retención y asíncrono sin configuración extra (usada arriba)
Node.jspinoImporta el throughput; es JSON-first y rápido — ver Logging de alto rendimiento con pino
Node.jswinstonNecesitas muchos transports y formatos flexibles (usado arriba)
Node.jswinston-daily-rotate-fileRotas por fecha — ver Rotación diaria de logs con Winston
JVMSLF4J + LogbackEl estándar de facto; Log4j2 con loggers asíncronos para alto throughput

Para profundizar en la salida JSON y los campos de correlación, consulta Logging estructurado. Para el envío a ELK, Loki o Datadog, consulta Agregación de logs.

IDs de correlación

Un ID de correlación (o de request) permite sacar todas las líneas de log de un request entre millones. Genéralo en el punto de entrada — un middleware HTTP es el sitio habitual — y adjúntalo al contexto del logger durante todo el request.

  • Python: logger.bind(request_id=...) o una variable contextvars leída por el formatter.
  • Node.js: un logger hijo (logger.child({ requestId })) creado por request.
  • Java: MDC.put("requestId", ...) más %X{requestId} en el patrón de Logback, como en el ejemplo anterior.

Entre servicios, pásalo en un header (X-Request-ID, o el traceparent de W3C si ya usas OpenTelemetry) para que los logs de los servicios aguas abajo se unan a la misma traza.

Datos sensibles en los logs

Los logs se envían a agregadores de terceros y los lee más gente que tu base de datos. Trátalos como semipúblicos:

  • Nunca loguees contraseñas, tokens de sesión, API keys completas ni números de tarjeta. Enmascara dejando 4 caracteres como máximo.
  • Mantén los datos personales (emails, nombres, IPs) fuera de los campos extra cuando aplique normativa de privacidad, o documéntalos explícitamente en tu mapa de datos.
  • Redacta en el límite, antes de que el string llegue al logger:
import re

SENSITIVE = re.compile(r"(token|api_key|password)=\S+", re.IGNORECASE)

def mask(msg: str) -> str:
    return SENSITIVE.sub(r"\1=***", msg)

logger.info(mask(f"fetched profile token={token}"))

Lo mismo aplica a las excepciones: un stack trace puede contener connection strings o parámetros de query. Loguea el objeto de excepción, pero limpia antes el mensaje si lleva input del usuario.

Lo que funciona

  • Emite JSON en producción. El texto plano es para humanos en una terminal; los agregadores necesitan campos. Mantén un formato legible en local y JSON en despliegues.
  • Adjunta un ID de correlación a cada línea de log de un request. Sin él, depurar entre servicios es adivinar.
  • Loguea al nivel correcto. DEBUG para detalle de desarrollo, INFO para operación normal, WARN para anomalías, ERROR para fallos que requieren atención.
  • Rota los archivos. La rotación por tamaño o por tiempo evita que un servicio hablador llene el disco.
  • Loguea el objeto de excepción, no solo el mensaje. logger.exception / { error } / pasar e conserva el stack trace.
  • Controla la verbosidad con LOG_LEVEL. Leer el nivel desde el entorno te permite subir el detalle durante un incidente sin redesplegar.

Errores comunes

  • Loguear todo en INFO, de modo que los problemas reales se esconden en el ruido.
  • Usar print o console.log en producción en lugar de un framework de logging.
  • Loguear secretos o datos personales en bruto. Consulta Datos sensibles en los logs y la Guía de Seguridad.
  • Sin rotación: un endpoint hablador puede llenar el disco en horas.
  • Capturar una excepción y loguear solo str(e), perdiendo el stack trace que dice dónde falló.
  • Concatenación con f-strings o + en rutas calientes: el string se construye aunque el nivel esté deshabilitado. Usa placeholders perezosos {} o %s.

Agregación de logs y monitorización

Los archivos de log en bruto casi nunca se leen directamente en producción. Un shipper los reenvía a un agregador:

PlataformaIdeal paraMétodo de envío
ELK StackSelf-hosted, control totalFilebeat / Logstash
DatadogSaaS con integración APMDatadog Agent
AWS CloudWatchInfraestructura nativa de AWSCloudWatch Agent
Grafana LokiKubernetes / stack PrometheusPromtail o Alloy
SplunkCumplimiento empresarialUniversal Forwarder

Reglas de alertas

Basa las alertas en patrones de log, no solo en métricas:

  • Tasa de ERROR superior al 1% en una ventana de 5 minutos → avisar al on-call.
  • Cualquier log FATAL → alerta inmediata.
  • Volumen de logs a cero durante 10 minutos → el servicio puede estar muerto aunque el health check pase.

Dashboards

Los dashboards útiles responden preguntas concretas: requests por minuto, latencia p95, qué endpoints producen más errores y cómo evoluciona el recuento de errores en 24 horas.

Lectura adicional

Preguntas frecuentes

¿Debería loguear cada request de API?

Sí, en nivel INFO con método, path, código de estado y duración. Un middleware lo hace una vez para todas las rutas.

¿Qué es el logging estructurado y por qué usarlo?

El logging estructurado emite JSON o pares clave-valor en lugar de texto libre. Campos como user_id o duration_ms se vuelven filtrables y agregables en tu plataforma de logs, algo imposible con strings no estructurados.

¿El logging debe ser síncrono o asíncrono?

Asíncrono en producción. Un append síncrono bloquea el hilo del request en I/O de disco. Usa enqueue=True en Loguru, AsyncAppender en Logback o transports asíncronos en Winston. En contenedores, loguear JSON a stdout evita la pregunta por completo.

¿Cómo correlaciono logs entre microservicios?

Genera un request_id en el punto de entrada, propágalo en un header HTTP e inclúyelo en cada sentencia de log — consulta IDs de correlación.

¿Cuánto tiempo debería retener los logs de producción?

Una división habitual es 30 días para INFO y 90 días para ERROR, ajustada al volumen y el coste. Archiva en almacenamiento frío como S3 Glacier si el cumplimiento exige mayor retención.

¿Debería loguear igual en desarrollo que en producción?

Usa la misma configuración de logger en ambos, pero cambia el formato de salida: texto legible en local, JSON estructurado en producción. Así evitas sorpresas de «en mi máquina funciona» causadas por comportamientos de logging distintos.