Rotate Log Files
How to implement log rotation by size, date, and count to prevent disk exhaustion across Python, Node.js, Java, and Linux systems.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Log rotation prevents a single log file from growing unbounded and exhausting disk space. A proper rotation strategy compresses old logs, keeps a configurable number of backups, and optionally deletes archives beyond a retention age. This recipe shows size-based and time-based rotation across Python, Node.js, Java, and Linux.
When to Use
- Application logs grow continuously and risk filling the disk
- You need to retain historical logs for compliance or debugging
- Log analysis tools prefer smaller, time-bounded files
- You want to compress old logs to reduce storage costs
- Multiple processes write to the same log file
When NOT to Use
- You are using a centralized logging service (Datadog, Splunk, ELK) that ingests from stdout/stderr — let the platform handle retention
- You need millisecond-level log search across all history — use a log database instead
- Your application runs as ephemeral containers with read-only filesystems — stream to stdout
Step-by-Step Implementation
Python
import logging
import logging.handlers
# Size-based rotation: 10MB max, keep 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)
# Time-based rotation: daily at midnight, keep 30 days
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 for external rotation (logrotate compatibility)
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';
// Size-based rotation with 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()
)
});
// Daily rotation
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]
});
// Cleanup old archives automatically
dailyTransport.on('rotate', (oldFilename, newFilename) => {
console.log(`Rotated log: ${oldFilename} -> ${newFilename}`);
});
Java
import java.util.logging.*;
// Using java.util.logging with custom rotation
public class LogRotationExample {
public static void setupLogging() throws Exception {
Logger logger = Logger.getLogger("myapp");
logger.setLevel(Level.INFO);
// Size-based rotation: 10MB, 5 backups
FileHandler fileHandler = new FileHandler(
"app.log", // pattern
10 * 1024 * 1024, // limit bytes
5, // count
true // append
);
fileHandler.setFormatter(new SimpleFormatter());
logger.addHandler(fileHandler);
}
}
// Logback (more common in production)
// 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 # Rotate daily
missingok # OK if log file is missing
rotate 30 # Keep 30 backups
compress # Compress old logs with gzip
delaycompress # Compress the rotation after next
notifempty # Don't rotate empty files
create 0644 appuser appuser
sharedscripts # Run postrotate once for all matching files
dateext # Use date instead of number suffix
dateformat -%Y%m%d
postrotate
# Signal application to reopen log file
kill -HUP $(cat /var/run/myapp.pid) > /dev/null 2>&1 || true
endscript
}
# Size-based rotation with logrotate
/var/log/myapp/app.log {
size 100M # Rotate when file exceeds 100MB
rotate 10
compress
copytruncate # Copy then truncate (no signal needed)
delaycompress
}
Best Practices
- Use
copytruncateorcreatewith a postrotate signal to avoid losing log entries between copy and reopen. Applications must handleSIGHUPto reopen file descriptors. - Set
totalSizeCapor equivalent to cap total storage across all rotated logs, not just the count of files. - Compress rotated logs to reduce storage by 80-95%. Use
delaycompressto keep the most recent backup uncompressed for immediate grep access. - Run logrotate with
-d(debug mode) before deploying to production to verify paths and permissions without making changes. - Monitor disk usage independently. Rotation is a safety net, not a substitute for capacity planning.
Common Mistakes
- Not handling the reopen signal in the application. The application continues writing to the old inode after rotation, causing the deleted file to keep consuming space until the process restarts.
- Using
copytruncatewith buffered writers. Buffered data in the application may be lost when the file is truncated. - Setting
maxFilesorbackupCounttoo low for compliance. 5 backups at 10MB each is only 50MB of history — insufficient for most production debugging. - Ignoring time zones in
TimedRotatingFileHandler. Useutc=Trueto avoid ambiguous behavior around daylight saving time transitions. - Running multiple application instances with the same log file. Concurrent writers without a locking mechanism interleave log lines or corrupt the file.