net.sf.eBus.util.logging.CalendarFileHandler Maven / Gradle / Ivy
//
// Copyright 2001 - 2005, 2013 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package net.sf.eBus.util.logging;
import com.google.common.base.Strings;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
import net.sf.eBus.util.TimerEvent;
import net.sf.eBus.util.TimerTask;
import net.sf.eBus.util.TimerTaskListener;
/**
* Logs messages to a user-specified file, rolling over to a
* new file at midnight. Log files are kept for only so many
* days before {@code CalendarFileHandler} deletes them.
* This retention limit is configurable but defaults to 10 days.
*
* The {@code CalendarFileHandler} uses three parameters to
* generate the complete file name:
*
* <base name>.<date pattern>.<extension>
*
*
* -
* Base Name: Store the log files here using this base
* name.
*
* Example: {@code /var/log/app/app}
*
* -
* Date Pattern: Use this pattern to format the date
* portion of the file name.
*
* The data pattern is passed to a
* {@link java.text.SimpleDateFormat#SimpleDateFormat(String)}. See
* {@link java.text.SimpleDateFormat} for a detailed
* explanation of valid date formats.
*
* -
* Extension: This is first part of the log file's
* name.
*
* Example: log
*
*
*
* Given the base name {@code /var/log/eBus/eBus}, a date
* pattern "ddMMyyyy" and extension {@code log}, the
* July 15, 2001 log file name is
* {@code /var/log/eBus/eBus.15072001.log}
*
* Configuration: {@code CalendarFileHandler}
* default configuration uses the following LogManager
* properties. If the named properties are either not defined or
* have invalid values, then the default settings are used.
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.basename
* (defaults to "./Logger")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.pattern
* (defaults to "yyyyMMdd")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.extension
* (defaults to "log")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.days_kept
* (defaults to 10 days)
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.formatter
* (defaults to "net.sf.eBus.util.logging.PatternFormatter")
*
* -
* net.sf.eBus.util.logging.CalendarFileHandler.level
* (defaults to system default)
*
*
*
* @author Charles Rapp
*/
public final class CalendarFileHandler
extends StreamHandler
implements TimerTaskListener
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* Log files are placed in the application's current working
* directory ({@value}) by default.
*/
public static final String DEFAULT_DIRECTORY = ".";
/**
* The log file's default base name is {@value}.
*/
public static final String DEFAULT_BASENAME = "Logger";
/**
* The log file's default data format is {@value}.
* For July 4, 1776 the formatted string is "17760704".
*/
public static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";
/**
* The log file handler default level is {@link Level#INFO}.
*/
public static final Level DEFAULT_LEVEL = Level.INFO;
/**
* The default file extension is {@value}.
*/
public static final String DEFAULT_EXTENSION = "log";
/**
* The minimum number of days a log file is kept is {@value}
* which means that the file is deleted as soon as the day
* ends.
*/
public static final int MIN_DAYS_KEPT = 0;
/**
* The maximum number of days a log file is kept is {@value}.
* That's three months. That is plenty of time to
* archive the file if necessary.
*/
public static final int MAX_DAYS_KEPT = 96;
/**
* Log files are kept for {@value} days by default.
*/
public static final int DEFAULT_DAYS_KEPT = 10;
/**
* Timeout callback method.
*/
private static final String TIMEOUT_METHOD_NAME =
"handleTimeout";
// Configuration property keys.
private static final String BASENAME_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.basename";
private static final String PATTERN_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.pattern";
private static final String EXTENSION_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.extension";
private static final String DAYS_KEPT_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.days_kept";
private static final String FORMATTER_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.formatter";
private static final String LEVEL_KEY =
"net.sf.eBus.util.logging.CalendarFileHandler.level";
//-----------------------------------------------------------
// Statics.
//
private static Timer sTimer =
new Timer("LogRollTimer", true);
//-----------------------------------------------------------
// Locals.
//
// The log file's info.
private final String mDirectory;
private final String mBasename;
private final String mDateFormat;
private final String mExtension;
private final int mDaysKept;
// When this timer expires, roll the log files into the
// gutter.
private TimerTask mLogRollTimer;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new {@link CalendarFileHandler} and configures
* it according to {@code LogManager} configuration
* properties.
* @throws IllegalArgumentException
* if {@code LogManager} configuration properties contains
* invalid or incorrect settings.
* @throws IOException
* if defined log directory is invalid.
* @throws ClassNotFoundException
* if defined formatter is not a known Java class.
* @throws NoSuchMethodException
* if defined formatter does not have a default
* constructor defined.
* @throws InstantiationException
* if defined formatter instantiation failed.
* @throws IllegalAccessException
* if defined formatter default constructor is
* inaccessible.
* @throws InvocationTargetException
* if formatter instantiation failed.
*/
public CalendarFileHandler()
throws IOException,
ClassNotFoundException,
NoSuchMethodException,
InstantiationException,
IllegalAccessException,
InvocationTargetException
{
this (getProperty(BASENAME_KEY, DEFAULT_BASENAME),
getProperty(PATTERN_KEY, DEFAULT_DATE_FORMAT),
getProperty(EXTENSION_KEY, DEFAULT_EXTENSION),
getDefaultDaysKept());
} // end of CalendarFileHandler()
/**
* Creates a new {@link CalendarFileHandler} instance for
* the specified base name, date format pattern, file name
* extension and how long to keep the files around.
* @param baseName where to put the log files.
* @param datePattern date format
* @param extension file name extension
* @param daysKept how long the log files are kept around
* (in days).
* @exception IllegalArgumentException
* if:
*
* -
* if {@code baseName}, {@code datePattern} or
* {@code extension} is {@code null}.
*
* -
* {@code baseName}, {@code datePattern} or
* {@code extension} is an empty string.
*
* -
* {@code daysKept} is < {@link #MIN_DAYS_KEPT}
* or > {@link #MAX_DAYS_KEPT}.
*
* -
* {@code datePattern} is an invalid date format
* pattern as per
* {@code java.text.SimpleDateFormat}.
*
* -
* {@code baseName} is in an unknown directory or
* directory cannot be accessed.
*
*
* @throws IllegalArgumentException
* if any of the given parameters is set to an invalid
* value.
* @throws IOException
* if defined log directory is invalid.
* @throws ClassNotFoundException
* if defined formatter is not a known Java class.
* @throws NoSuchMethodException
* if defined formatter does not have a default
* constructor defined.
* @throws InstantiationException
* if defined formatter instantiation failed.
* @throws IllegalAccessException
* if defined formatter default constructor is
* inaccessible.
* @throws InvocationTargetException
* if formatter instantiation failed.
*/
public CalendarFileHandler(final String baseName,
final String datePattern,
final String extension,
final int daysKept)
throws IOException,
ClassNotFoundException,
NoSuchMethodException,
InstantiationException,
IllegalAccessException,
InvocationTargetException
{
super ();
if (Strings.isNullOrEmpty(baseName))
{
throw (
new IllegalArgumentException(
"baseName is either null or an empty string"));
}
if (Strings.isNullOrEmpty(datePattern))
{
throw (
new IllegalArgumentException(
"datePattern is either null or an empty string"));
}
if (Strings.isNullOrEmpty(extension))
{
throw (
new IllegalArgumentException(
"extension is either null or an empty string"));
}
mDirectory = dirname(baseName);
mBasename = basename(baseName);
mDateFormat = datePattern;
mExtension = extension;
mDaysKept = daysKept;
mLogRollTimer = new TimerTask(this);
// Get the handler property settings.
final Formatter formatter = createFormatter();
if (formatter != null)
{
setFormatter(formatter);
}
// Set the log level.
setLevel(getDefaultLevel());
// Finish up by setting the handler's output stream as
// per the configuration.
final OutputStream logStream =
openLogStream(
mDirectory, mBasename, mDateFormat, mExtension);
if (logStream != null)
{
setOutputStream(logStream);
}
deleteLogFiles();
startMidnightTimer(mLogRollTimer);
} // end of CalendarFileHandler(String, String, String, int)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// TimerTaskListener Interface Implementation.
//
/**
* Time to roll over to the next log file.
* @param task the roll file timer task.
*/
@Override
public void handleTimeout(final TimerEvent task)
{
final OutputStream logStream =
openLogStream(
mDirectory, mBasename, mDateFormat, mExtension);
LogRecord logRecord;
logRecord =
new LogRecord(Level.INFO, "Rolling log file.");
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName(TIMEOUT_METHOD_NAME);
publish(logRecord);
deleteLogFiles();
// Close the current log file and open the new log
if (logStream != null)
{
setOutputStream(logStream);
}
logRecord =
new LogRecord(
Level.INFO, "Finished rolling log file.");
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName(TIMEOUT_METHOD_NAME);
publish(logRecord);
// Schedule the next log roll.
// Why keep rescheduling? Why not schedule a repeating
// timer task?
// Because the local timezone might use
// daylight savings time.
mLogRollTimer = new TimerTask(this);
startMidnightTimer(mLogRollTimer);
} // end of handleTimeout(TimerEvent)
//
// end of TimerTaskListener Interface Implementation.
//-----------------------------------------------------------
/**
* Flushes the output stream after {@link StreamHandler}
* publishes the log record. {@code StreamHandler} does not
* do this which means records are not seen in the log
* file as they are published.
* @param logRecord Publish this log record to the log file.
*/
@Override
public synchronized void publish(final LogRecord logRecord)
{
super.publish(logRecord);
super.flush();
} // end of publish(LogRecord)
// Performs the work of deleting out-of-date log files
private void deleteLogFiles()
{
final Calendar calendar = Calendar.getInstance();
final File directory = new File(mDirectory);
LogFileFilter logFilter;
File[] logFiles;
// Get today's date and subtract the days kept.
// Then set the time to midnight (00:00:00 AM).
// Delete any log files older than that date.
calendar.add(Calendar.DAY_OF_MONTH, (mDaysKept * -1));
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Create the log name filter.
logFilter =
new LogFileFilter(
directory,
mBasename + ".", mDateFormat,
"." + mExtension,
calendar.getTime());
// Now get the list of expired log files.
logFiles = directory.listFiles(logFilter);
// Are there any log files to be deleted?
if (logFiles != null && logFiles.length > 0)
{
final StringBuilder buffer = new StringBuilder();
LogRecord logRecord;
int index;
Path path;
buffer.append("Deleted the following log files:");
// Go through each of the log files.
for (index = 0; index < logFiles.length; ++index)
{
buffer.append("\n ")
.append(logFiles[index].getName());
try
{
path =
(FileSystems.getDefault()).getPath(
logFiles[index].getPath());
Files.delete(path);
}
catch (IOException jex)
{
buffer.append(" failed");
}
}
logRecord =
new LogRecord(Level.INFO, buffer.toString());
logRecord.setSourceClassName(
CalendarFileHandler.class.getName());
logRecord.setSourceMethodName(TIMEOUT_METHOD_NAME);
publish(logRecord);
}
} // end of deleteLogFiles()
// Opens an output stream for the calendar log file.
private static OutputStream openLogStream(
final String directory,
final String basename,
final String datePattern,
final String extension)
{
OutputStream retval = null;
try
{
final File logFile =
new File(
generateLogFilename(
directory,
basename,
datePattern,
extension));
retval = new FileOutputStream(logFile, true);
}
catch (IOException ioex)
{
// Return null.
}
return (retval);
} // end of openLogStream(String, String, String, String)
// Generates the log file name based on the given
// information.
private static String generateLogFilename(
final String directory,
final String basename,
final String datePattern,
final String extension)
{
final SimpleDateFormat formatter =
new SimpleDateFormat(datePattern);
return (
String.format(
"%s%c%s.%s.%s",
directory,
File.separatorChar,
basename,
formatter.format(new Date()),
extension));
} // end of generateLogFilename(String, String, String)
/**
* Returns number of days back which the calendar file
* handler will keep files. Log files matching the
* configured directory, base name, and date format which
* are older than this number of days are deleted.
* @return days back for keeping log files.
* @throws NumberFormatException
* if property is set to an invalid number.
* @throws IllegalArgumentException
* if the property is set to a value outside of
* [{@link #MIN_DAYS_KEPT}, {@link #MAX_DAYS_KEPT}].
*/
private static int getDefaultDaysKept()
{
final String value =
getProperty(DAYS_KEPT_KEY,
String.valueOf(DEFAULT_DAYS_KEPT));
int retval = DEFAULT_DAYS_KEPT;
if (!Strings.isNullOrEmpty(value))
{
retval = Integer.parseInt(value);
if (retval < MIN_DAYS_KEPT ||
retval > MAX_DAYS_KEPT)
{
throw (
new IllegalArgumentException(
String.format(
"%,d is either < %d or > %d",
retval,
MIN_DAYS_KEPT,
MAX_DAYS_KEPT)));
}
}
return (retval);
} // end of getDefaultDaysKept()
/**
* Creates a formatter instance based on the given class.
* name. This class must extend {@link Formatter} and
* have a {@code public}, no arguments default
* constructor defined.
* @param className {@link Formatter} subclass name.
* @return {@code this Builder} instance.
* @throws IllegalArgumentException
* if {@code className} is either {@code null} or an
* empty string.
* @throws ClassNotFoundException
* if {@code className} is not a known Java class.
* @throws NoSuchMethodException
* if {@code className} does not have a default
* constructor defined.
* @throws InstantiationException
* if {@code className} instantiation failed.
* @throws IllegalAccessException
* if {@code className} default constructor is
* inaccessible.
* @throws InvocationTargetException
* if {@code className} instantiation failed.
*/
@SuppressWarnings ("unchecked")
private static Formatter createFormatter()
throws ClassNotFoundException,
NoSuchMethodException,
InstantiationException,
IllegalAccessException,
InvocationTargetException
{
final String value = getProperty(FORMATTER_KEY, null);
Formatter retval = null;
// Was a formatter provided?
if (!Strings.isNullOrEmpty(value))
{
// Yes. Create an instance of the formatter class.
final Class extends Formatter> fc =
(Class extends Formatter>)
Class.forName(value);
final Constructor extends Formatter> ctor =
fc.getDeclaredConstructor();
retval = ctor.newInstance();
}
// No. Return null.
return (retval);
} // end of createFormatter()
/**
* Returns the log manager property stored in the given key
* as text. If there is no such property defined or is a
* {@code null} or empty string then returns the default
* value.
* @param key property key.
* @param defaultValue property default value.
* @return property value.
*/
private static String getProperty(final String key,
final String defaultValue)
{
final LogManager manager = LogManager.getLogManager();
String value = manager.getProperty(key);
return (Strings.isNullOrEmpty(value) ?
defaultValue :
value);
} // end of getProperty(String, String, LogManager)
/**
* Returns the log handler default log level.
* @return default log level.
* @throws IllegalArgumentException
* if log manager log level property is set to an invalid
* value.
*/
private Level getDefaultLevel()
{
final String value =
getProperty(LEVEL_KEY, DEFAULT_LEVEL.getName());
Level retval = DEFAULT_LEVEL;
if (!Strings.isNullOrEmpty(value))
{
retval = Level.parse(value);
}
return (retval);
} // end of getDefaultLevel()
/**
* Returns the log file directory. If undefined then returns
* current working directory (".").
* @param logname extract log file directory from this name.
* @return log file directory.
* @throws IOException
* if {@code baseName} contains an invalid directory.
*/
private static String dirname(final String baseName)
throws IOException
{
final int index =
baseName.lastIndexOf(File.separatorChar);
final File dir;
final String retval = (index < 0 ?
DEFAULT_DIRECTORY :
baseName.substring(0, index));
// Is this a valid directory?
dir = new File(retval);
if (!dir.exists() ||
!dir.isDirectory() ||
!dir.canWrite())
{
throw (
new IOException(
retval + " is an invalid directory"));
}
return (retval);
} // end of dirname(String)
/**
* Returns log file base name. If there is no
* {@link File#separatorChar} in {@code baseName} then
* returns {@code baseName} as the log file base name.
* @return log file base name.
*/
private static String basename(final String baseName)
{
final int index =
baseName.lastIndexOf(File.separatorChar);
return (index < 0 ?
baseName :
baseName.substring(index + 1));
} // end of basename(String)
// Starts the timer task to expire at midnight. When the
// task executes, it rolls the log file to the new day
// and deletes log files older than the configured days
// kept.
private void startMidnightTimer(final TimerTask task)
{
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
sTimer.schedule(task, calendar.getTime());
} // end of startMidnightTimer(TimerTask)
//---------------------------------------------------------------
// Inner classes.
//
/**
* This file name filter looks for log files in a specified
* directory and whose names start and end with the specified
* strings.
*/
private static final class LogFileFilter
implements FilenameFilter
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Constructors.
//
// Place the log files in this directory.
private final File mDirectory;
// All log file names start with the basename.
private final String mBasename;
private final int mBasenameSize;
// Parses the file names date format.
private final SimpleDateFormat mDateFormatter;
// All log file names end with this extension.
private final String mExtension;
private final int mExtensionSize;
// Accept only log files older than this date.
private final Date mExpiration;
// Use this calendar to normalize the log file date.
private final Calendar mCalendar;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* All log files are placed in the same directory, have
* the same base name and file name extension.
* @param directory the log file directory.
* @param basename the log file name's base name.
* @param dateFormat the file name date format.
* @param extension the log file name's extension.
* @param expiration the log file expiration date.
*/
private LogFileFilter(final File directory,
final String basename,
final String dateFormat,
final String extension,
final Date expiration)
{
mDirectory = directory;
mBasename = basename;
mBasenameSize = mBasename.length();
mDateFormatter = new SimpleDateFormat(dateFormat);
mExtension = extension;
mExtensionSize = mExtension.length();
mExpiration = expiration;
mCalendar = Calendar.getInstance();
} // end of LogFileFilter(File,String,String,String,Date)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// FilenameFilter Interface Implemenation.
//
/**
* Returns {@code true} if {@code directory}
* is the same as the logging directory and the file
* {@code name} starts with the expected basename
* and ends with the proper extension.
* @param directory the file's directory.
* @param name the file's name.
* @return {@code true} if {@code directory}
* is the same as the logging directory and the file
* {@code name} starts with the expected basename
* and ends with the proper extension.
*/
@Override
public boolean accept(final File directory,
final String name)
{
boolean retval =
(directory.equals(mDirectory) &&
name.startsWith(mBasename) &&
name.endsWith(mExtension));
if (retval)
{
String dateSubstring;
ParsePosition pos;
Date logFileDate;
// Get the date portion of the file name.
dateSubstring =
name.substring(
mBasenameSize,
(name.length() - mExtensionSize));
// Parse the log's name to get its date.
pos = new ParsePosition(0);
logFileDate =
mDateFormatter.parse(dateSubstring, pos);
if (logFileDate != null)
{
mCalendar.setTime(logFileDate);
mCalendar.set(Calendar.HOUR_OF_DAY, 0);
mCalendar.set(Calendar.MINUTE, 0);
mCalendar.set(Calendar.SECOND, 0);
mCalendar.set(Calendar.MILLISECOND, 0);
// Is the log file beyond keeping?
// Toss it if it is beyond expiration.
retval =
mCalendar.getTime().before(mExpiration);
}
}
return (retval);
} // end of accept(File, String)
//
// end of FilenameFilter Interface Implemenation.
//-------------------------------------------------------
} // end of class LogFileFilter
} // end of class CalendarFileHandler