intermediate Por Mathias Paulenko

Rotar Archivos de Log

Cómo implementar rotación de logs por tamaño, fecha y cantidad para prevenir la exaustión de disco en Python, Node.js, Java y Linux.

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.

Descripción General

La rotación de logs previene que un solo archivo de log crezca sin límite y agote el espacio en disco. Una estrategia adecuada de rotación comprime logs antiguos, mantiene un número configurable de backups y opcionalmente elimina archivos que exceden una edad de retención. El patron a continuacion demuestra rotación basada en tamaño y tiempo en Python, Node.js, Java y Linux.

Cuándo Usar

  • Los logs de aplicación crecen continuamente y arriesgan llenar el disco
  • Necesitas retener logs históricos para cumplimiento o debugging
  • Las herramientas de análisis de logs prefieren archivos más pequeños acotados en tiempo
  • Quieres comprimir logs antiguos para reducir costos de almacenamiento
  • Múltiples procesos escriben al mismo archivo de log

Cuándo NO Usar

  • Estás usando un servicio de logging centralizado (Datadog, Splunk, ELK) que ingiere desde stdout/stderr — deja que la plataforma maneje la retención
  • Necesitas búsqueda de logs a nivel de milisegundo a través de todo el historial — usa una base de datos de logs en su lugar
  • Tu aplicación corre como contenedores efímeros con sistemas de archivos de solo lectura — envía a stdout

Implementación Paso a Paso

Python

import logging
import logging.handlers

# Rotación por tamaño: máximo 10MB, mantener 5 backups
handler = logging.handlers.RotatingFileHandler(
    'app.log',
    maxBytes=10 * 1024 * 1024,  # 10 MB
    backupCount=5,
    encoding='utf-8'
)
handler.setFormatter(logging.Formatter(
    '%(asctime)s %(levelname)s %(name)s: %(message)s'
))

logger = logging.getLogger('myapp')
logger.setLevel(logging.INFO)
logger.addHandler(handler)

# Rotación por tiempo: diaria a medianoche, mantener 30 días
from logging.handlers import TimedRotatingFileHandler

timed_handler = TimedRotatingFileHandler(
    'app_daily.log',
    when='midnight',
    interval=1,
    backupCount=30,
    encoding='utf-8',
    utc=True
)
timed_handler.suffix = '%Y-%m-%d'
timed_handler.extMatch = r'^\d{4}-\d{2}-\d{2}$'
logger.addHandler(timed_handler)

# WatchedFileHandler para rotación externa (compatibilidad con logrotate)
from logging.handlers import WatchedFileHandler
watched = WatchedFileHandler('app.log')
logger.addHandler(watched)

Node.js

import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';

// Rotación por tamaño con Winston
const sizeTransport = new winston.transports.File({
    filename: 'app.log',
    maxsize: 10 * 1024 * 1024,  // 10 MB
    maxFiles: 5,
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    )
});

// Rotación diaria
const dailyTransport = new DailyRotateFile({
    filename: 'app-%DATE%.log',
    datePattern: 'YYYY-MM-DD',
    zippedArchive: true,
    maxSize: '20m',
    maxFiles: '30d',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    )
});

const logger = winston.createLogger({
    level: 'info',
    transports: [sizeTransport, dailyTransport]
});

// Limpieza automática de archivos antiguos
dailyTransport.on('rotate', (oldFilename, newFilename) => {
    console.log(`Rotado log: ${oldFilename} -> ${newFilename}`);
});

Java

import java.util.logging.*;

// Usando java.util.logging con rotación personalizada
public class LogRotationExample {
    public static void setupLogging() throws Exception {
        Logger logger = Logger.getLogger("myapp");
        logger.setLevel(Level.INFO);

        // Rotación por tamaño: 10MB, 5 backups
        FileHandler fileHandler = new FileHandler(
            "app.log",           // patrón
            10 * 1024 * 1024,    // límite en bytes
            5,                   // cantidad
            true                 // append
        );
        fileHandler.setFormatter(new SimpleFormatter());
        logger.addHandler(fileHandler);
    }
}

// Logback (más común en producción)
// logback.xml:
/*
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="FILE" />
    </root>
</configuration>
*/

Linux (logrotate)

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily                  # Rotar diariamente
    missingok              # OK si el archivo de log no existe
    rotate 30              # Mantener 30 backups
    compress               # Comprimir logs antiguos con gzip
    delaycompress          # Comprimir la rotación después de la siguiente
    notifempty             # No rotar archivos vacíos
    create 0644 appuser appuser
    sharedscripts          # Ejecutar postrotate una vez para todos los archivos
    dateext                # Usar fecha en lugar de número como sufijo
    dateformat -%Y%m%d

    postrotate
        # Señalizar a la aplicación para reabrir el archivo de log
        kill -HUP $(cat /var/run/myapp.pid) > /dev/null 2>&1 || true
    endscript
}
# Rotación por tamaño con logrotate
/var/log/myapp/app.log {
    size 100M              # Rotar cuando el archivo exceda 100MB
    rotate 10
    compress
    copytruncate           # Copiar luego truncar (sin señal necesaria)
    delaycompress
}

Lo que funciona

  • Usa copytruncate o create con una señal postrotate para evitar perder entradas de log entre la copia y la reapertura. Las aplicaciones deben manejar SIGHUP para reabrir descriptores de archivo.
  • Configura totalSizeCap o equivalente para limitar el almacenamiento total entre todos los logs rotados, no solo la cantidad de archivos.
  • Comprime logs rotados para reducir almacenamiento en un 80-95%. Usa delaycompress para mantener el backup más reciente sin comprimir para acceso inmediato con grep.
  • Ejecuta logrotate con -d (modo debug) antes de desplegar en producción para verificar rutas y permisos sin hacer cambios.
  • Monitorea el uso de disco de forma independiente. La rotación es una red de seguridad, no un sustituto de la planificación de capacidad.

Errores Comunes

  • No manejar la señal de reapertura en la aplicación. La aplicación continúa escribiendo al inodo viejo después de la rotación, haciendo que el archivo eliminado siga consumiendo espacio hasta que el proceso se reinicia.
  • Usar copytruncate con writers con buffer. Los datos en buffer de la aplicación pueden perderse cuando el archivo es truncado.
  • Configurar maxFiles o backupCount demasiado bajo para cumplimiento. 5 backups de 10MB cada uno son solo 50MB de historial — insuficiente para la mayoría del debugging en producción.
  • Ignorar zonas horarias en TimedRotatingFileHandler. Usa utc=True para evitar comportamiento ambiguo durante transiciones de horario de verano.
  • Ejecutar múltiples instancias de la aplicación con el mismo archivo de log. Writers concurrentes sin un mecanismo de bloqueo entrelazan líneas de log o corrompen el archivo.

Mejores Prácticas Adicionales

  1. Usa zstd en lugar de gzip para compresión más rápida. zstd ofrece ratios de compresión similares con descompresión 3-5x más rápida:
# logrotate con zstd (requiere logrotate 3.18+)
/var/log/myapp/*.log {
    daily
    rotate 30
    compress
    compresscmd /usr/bin/zstd
    compressoptions -19
    compressext .zst
    delaycompress
    missingok
    notifempty
}
  1. Prueba la rotación en un entorno staging. Simula alto volumen de logs para verificar que la rotación dispara en el umbral correcto:
#!/bin/bash
# Generar 15MB de datos de log de prueba para disparar rotación de 10MB
head -c 15M /dev/urandom | base64 >> /var/log/myapp/test.log

# Verificar que la rotación ocurrió
ls -la /var/log/myapp/test.log*
  1. Usa logging estructurado con rotación. Combina logging JSON con rotación para parsing más fácil por herramientas de análisis de logs:
import logging
import json
from logging.handlers import RotatingFileHandler


class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        if record.exc_info:
            log_entry["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_entry)


handler = RotatingFileHandler(
    "app.json.log",
    maxBytes=10 * 1024 * 1024,
    backupCount=10,
)
handler.setFormatter(JsonFormatter())

logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

Errores Comunes Adicionales

  1. Olvidar rotar logs de cron o systemd timers. La rotación a nivel de aplicación maneja logs in-app, pero la salida de cron jobs y los logs de systemd journal necesitan rotación separada:
# Rotar salida de cron redirigida a un archivo
/var/log/cron-output.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
    copytruncate
}

# systemd: journald tiene su propia retención
# /etc/systemd/journald.conf:
# SystemMaxUse=500M
# MaxRetentionSec=30day
  1. No establecer permisos en archivos rotados. Los logs pueden contener datos sensibles. Asegura que los archivos rotados mantengan permisos restrictivos:
# logrotate: establecer permisos en archivos rotados
/var/log/myapp/*.log {
    daily
    rotate 30
    compress
    delaycompress
    create 0640 myapp myapp
    su myapp myapp
    missingok
    notifempty
}

Preguntas frecuentes

¿Qué es la rotación de logs y por qué importa?
La rotación de logs archiva o elimina archivos de log antiguos para evitar que se agote el disco. Sin rotación, un solo servicio puede llenar todo el disco.
¿Cómo elijo una política de retención?
Equilibra cumplimiento, necesidades de debugging y costo de almacenamiento. Una aplicación web común conserva 7-30 días de logs localmente y archiva logs más antiguos en almacenamiento frío.
¿Debo comprimir logs rotados?
Sí. La compresión reduce considerablemente el uso de almacenamiento. La mayoría de las herramientas de rotación soportan compresión gzip o zstd de forma nativa.