I use the Log4j DailyRollingFileAppender over many years and many projects but always end up controlling the retention via a Cron jobs. That's work well, but what if we can control the retention in the log4j configuration file? Wouldn't that be great? Well, that came up and I wanted to share my solution with you:
package org.cnci.util; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import org.apache.log4j.FileAppender; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.spi.LoggingEvent; /** * Provide AS IS, no warranty! * * Special log appender for log4j. Adds the current date (ie. year-mon) to the * end of the file name, so that rolling on to the next log is simply a case of * starting a new one - no renaming of old logs. * * ONLY support: yyyy (yearly) | yyyy-MM (monthly) | yyyy-MM-dd (daily) | * yyyy-ww (weekly) yyyy-MM-dd-a (midnight, midday) | yyyy-MM-dd-HH (hourly) | * yyyy-MM-dd-HH-mm (every minute) * * An example log4j.properties (one log per day, retains 30 days of logs) * * log4j.rootCategory=INFO, DAILY * log4j.appender.DAILY=org.cnci.util.DailyFileAppender * log4j.appender.DAILY.File=log/MyApplication.log * log4j.appender.DAILY.DatePattern=yyyy-MM-dd log4j.appender.DAILY.MaxLogs=30 * log4j.appender.DAILY.layout=org.apache.log4j.PatternLayout * log4j.appender.DAILY.layout.ConversionPattern=%d %-5p %c @ %m%n * * @author T.A. Nguyen **/ public class DailyFileAppender extends FileAppender { private static final String DEFAULT_DATE_PATTERN = "'.'yyyy-MM-dd"; private static final String[] SUPPORTED_DATE_PATTERNS = { "yyyy" /** (yearly) **/, "yyyy-MM" /** (monthly) **/, "yyyy-MM-dd" /** (daily) **/, "yyyy-ww" /** (weekly) **/, "yyyy-MM-dd-a" /** (midnight, midday) **/, "yyyy-MM-dd-HH" /** (hourly) **/, "yyyy-MM-dd-HH-mm" /** (every minute) **/ }; /** * Used internally and contains the name of the date derived from current * system date. **/ private Date now = new Date(); private String datePattern; private SimpleDateFormat formater; private int maxLogs = 0; private String scheduledFileName; /** * Default constructor. This is required as the appender class is dynamically * loaded. **/ public DailyFileAppender() { super(); } /** * computing for the scheduledFileName and set the now **/ @Override public void activateOptions() { super.activateOptions(); if (datePattern != null && fileName != null) { now.setTime(System.currentTimeMillis()); formater = new SimpleDateFormat(datePattern); File file = new File(fileName); scheduledFileName = fileName + formater.format(new Date(file.lastModified())); } else { LogLog.error("Either File or DatePattern options are not set for appender [" + name + "]."); } } public String getDatePattern() { return this.datePattern; } public void setDatePattern(String datePattern) { // make sure the the pattern conform to what we allow. this.datePattern = checkPattern(datePattern); } public int getMaxLogs() { return maxLogs; } public void setMaxLogs(int maxLogs) { this.maxLogs = maxLogs; } @Override protected void subAppend(LoggingEvent logEvent) { now.setTime(System.currentTimeMillis()); try { rollOver(); } catch (IOException IOEx) { LogLog.error("rollOver() failed!", IOEx); } super.subAppend(logEvent); } @Override public void setFile(String file) { try { // Fix the STUPID file path... Windows will use \ and *NIX will use // /, // so make sure we store the correct file path. File f = new File(file); file = f.getAbsolutePath(); } catch (Exception e) { // ignore! } super.setFile(file); } void rollOver() throws IOException { /** Compute filename, but only if datePattern is specified **/ if (datePattern == null) { // LogLog.error("Missing DatePattern option in rollOver()."); errorHandler.error("Missing DatePattern option in rollOver()."); return; } String datedFilename = fileName + formater.format(now); LogLog.debug(fileName + " ==> " + scheduledFileName + " ==> " + datedFilename); // It is too early to roll over because we are still within the // bounds of the current interval. Rollover will occur once the // next interval is reached. if (scheduledFileName.equals(datedFilename)) { return; } // close current file, and rename it to datedFilename this.closeFile(); File file = new File(fileName); File target = new File(fileName + formater.format(new Date(file.lastModified()))); if (target.exists()) { target.delete(); } boolean result = file.renameTo(target); if (result) { // LogLog.debug(fileName + " -> " + scheduledFileName); scheduledFileName = datedFilename; } else { LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFileName + "]."); } try { // This will also close the file. This is OK since multiple // close operations are safe. this.setFile(fileName, true, this.bufferedIO, this.bufferSize); } catch (IOException e) { // LogLog.error("setFile(" + fileName + ", true) call failed."); errorHandler.error("setFile(" + fileName + ", true) call failed."); } cleanupOldFiles(); } /** * The helper function to validate the DatePattern. * * @param pattern * The DatePattern to be validated. * @return The validated date pattern or default DEFAULT_DATE_PATTERN **/ private String checkPattern(String pattern) { if (isSupportedPattern(pattern)) { try { this.formater = new SimpleDateFormat(pattern); return pattern; } catch (Exception e) { LogLog.error("Invalid DatePattern " + pattern, e); } } return DEFAULT_DATE_PATTERN; } private boolean isSupportedPattern(String pattern) { if (pattern != null) { // give 'em that last chance to conform to our supported pattern. // Convert " " and ":" into "-" so that file operation will work // correctly! pattern = pattern.replaceAll(" |:", "-"); for (String p : SUPPORTED_DATE_PATTERNS) { if (pattern.contains(p)) return true; } } return false; } /** * Now delete those old files if needed. * * @param pstrName * The name of the new folder based on current system date. * @throws IOException **/ static private boolean deletingFiles = false; private static synchronized void setDeletingFiles(boolean isDelete) { deletingFiles = isDelete; } private static synchronized boolean isDeletingFiles() { return deletingFiles; } boolean isExtendedLogFile(String name) { if (name == null) return false; File logMaster = new File(fileName); String master = logMaster.getName(); return name.contains(master); } void cleanupOldFiles() { // If we need to delete log files if (maxLogs > 0 && !isDeletingFiles()) { // tell other thread, that we will do the clean up setDeletingFiles(true); try { // Array to hold the logs we are going to keep File[] logsToKeep = new File[maxLogs]; // Get a 'master' file handle, and the parent directory from it File logMaster = new File(fileName); File logDir = logMaster.getParentFile(); if (logDir.isDirectory()) { File[] logFiles = getLogFiles(logDir); // more files than expected, time to clean up! if (logFiles.length > maxLogs) for (File curLog : logFiles) { removeOldFile(logsToKeep, curLog); } } } catch (Exception e) { // ignore // LogLog.error("While deleting old files. " + e.getMessage(), e); } finally { // we are done with clean up. setDeletingFiles(false); } } } private void removeOldFile(File[] logsToKeep, File curLog) { String name = curLog.getAbsolutePath(); // Check that the file is indeed one we want // (contains the master file name) if (name.indexOf(fileName) >= 0) { // Iterate through the array of logs we are // keeping for (int i = 0; curLog != null && i < logsToKeep.length; i++) { // Have we exhausted the 'to keep' array? if (logsToKeep[i] == null) { // Empty space, retain this log file logsToKeep[i] = curLog; curLog = null; } // If the 'kept' file is older than the // current one else if (logsToKeep[i].getName().compareTo(curLog.getName()) < 0) { // Replace tested entry with current // file File temp = logsToKeep[i]; logsToKeep[i] = curLog; curLog = temp; } } // If we have a 'current' entry at this point, // it's a log we don't want if (curLog != null) { curLog.delete(); } } } private File[] getLogFiles(File logDir) { // Iterate all the files in that directory FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return isExtendedLogFile(name); } }; File[] logFiles = logDir.listFiles(filter); // only get the // extended // files return logFiles; } }
And here is the sample log4j.properties
#------------------------------------------------------------------------------ # Provide AS IS No Warranty! # Author: T.A. Nguyen # # Default log4j.properties file. This should be in the class path # # Possible Log Levels: # FATAL, ERROR, WARN, INFO, DEBUG # # Production Setting: #log4j.rootCategory=ERROR, DAILY # Development and Build Setting: log4j.rootCategory=DEBUG, DAILY, CONSOLE #------------------------------------------------------------------------------ # # The following properties configure the console (stdout) appender. # #------------------------------------------------------------------------------ log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender #log4j.appender.CONSOLE.Target=System.out log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern = %d{ABSOLUTE} %5p %c{1}:%L - %m%n #------------------------------------------------------------------------------ # # The following properties configure the Daily File appender. # #------------------------------------------------------------------------------ log4j.appender.DAILY = org.cnci.util.DailyFileAppender log4j.appender.DAILY.File = log/MyApplication.log log4j.appender.DAILY.MaxLogs = 30 log4j.appender.DAILY.Append = true log4j.appender.DAILY.DatePattern = '.'yyy-MM-dd log4j.appender.DAILY.layout = org.apache.log4j.PatternLayout log4j.appender.DAILY.layout.ConversionPattern = %d{ABSOLUTE} %5p %c{1}:%L - %m%n # Extracting all MyApplication logging into it own file #log4j.logger.org.cnci=DEBUG, DAILY #log4j.additivity.org.cnci=false