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
No comments:
Post a Comment