/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.log4j; import org.apache.log4j.helpers.CountingQuietWriter; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.helpers.OptionConverter; import org.apache.log4j.spi.LoggingEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPOutputStream; /** *
CompositeRollingAppender combines RollingFileAppender and DailyRollingFileAppender
It can function as either
* or do both at the same time (making size based rolling files like RollingFileAppender until a data/time boundary is
* crossed at which time it rolls all of those files as per the DailyRollingFileAppender) based on the setting for
* rollingStyle
.
To use CompositeRollingAppender to roll log files as they reach a certain size
* (like RollingFileAppender), set rollingStyle=1 (@see config.size)
To use CompositeRollingAppender to roll log
* files at certain time intervals (daily for example), set rollingStyle=2 and a datePattern (@see config.time)
To
* have CompositeRollingAppender roll log files at a certain size AND rename those according to time intervals, set
* rollingStyle=3 (@see config.composite)
*
*
A of few additional optional features have been added:
-- Attach date pattern for current log file (@see
* staticLogFileName)
-- Backup number increments for newer files (@see countDirection)
-- Infinite number of
* backups by file size (@see maxSizeRollBackups)
A few notes and warnings: For large or infinite number of
* backups countDirection {@literal >} 0 is highly recommended, with staticLogFileName = false if time based rolling is also used
* -- this will reduce the number of file renamings to few or none. Changing staticLogFileName or countDirection
* without clearing the directory could have nasty side effects. If Date/Time based rolling is enabled,
* CompositeRollingAppender will attempt to roll existing files in the directory without a date/time tag based on the
* last modified date of the base log files last modification.
A maximum number of backups based on
* date/time boundaries would be nice but is not yet implemented. If the The file will be appended to. DatePattern is default.
*/
public QpidCompositeRollingAppender(Layout layout, String filename) throws IOException
{
super(layout, filename);
}
/**
* The DatePattern takes a string in the same format as expected by {@link java.text.SimpleDateFormat}. This
* options determines the rollover schedule.
*/
public void setDatePattern(String pattern)
{
datePattern = pattern;
}
/** Returns the value of the DatePattern option. */
public String getDatePattern()
{
return datePattern;
}
/** There is zero backup files by default. */ /** Returns the value of the maxSizeRollBackups option. */
public int getMaxSizeRollBackups()
{
return maxSizeRollBackups;
}
/**
* Get the maximum size that the output file is allowed to reach before being rolled over to backup files.
*
* @since 1.1
*/
public long getMaximumFileSize()
{
return maxFileSize;
}
/**
* Set the maximum number of backup files to keep around based on file size.
*
* The MaxSizeRollBackups option determines how many backup files are kept before the oldest is erased.
* This option takes an integer value. If set to zero, then there will be no backup files and the log file will be
* truncated when it reaches The maximum applies to -each- time based group of files and -not- the total. Using a daily roll the maximum
* total files would be (#days run) * (maxSizeRollBackups)
*/
public void setMaxSizeRollBackups(int maxBackups)
{
maxSizeRollBackups = maxBackups;
}
/**
* Set the maximum size that the output file is allowed to reach before being rolled over to backup files.
*
* This method is equivalent to {@link #setMaxFileSize} except that it is required for differentiating the setter
* taking a This method is equivalent to {@link #setMaxFileSize} except that it is required for differentiating the setter
* taking a In configuration files, the MaxFileSize option takes an long integer in the range 0 - 2^63. You can
* specify the value with the suffixes "KB", "MB" or "GB" so that the integer is interpreted being expressed
* respectively in kilobytes, megabytes or gigabytes. For example, the value "10KB" will be interpreted as 10240.
*/
public void setMaxFileSize(String value)
{
maxFileSize = OptionConverter.toFileSize(value, maxFileSize + 1);
}
protected void setQWForFiles(Writer writer)
{
qw = new CountingQuietWriter(writer, errorHandler);
}
// Taken verbatim from DailyRollingFileAppender
int computeCheckPeriod()
{
RollingCalendar c = new RollingCalendar();
// set sate to 1970-01-01 00:00:00 GMT
Date epoch = new Date(0);
if (datePattern != null)
{
for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++)
{
String r0 = sdf.format(epoch);
c.setType(i);
Date next = new Date(c.getNextCheckMillis(epoch));
String r1 = sdf.format(next);
if ((r0 != null) && (r1 != null) && !r0.equals(r1))
{
return i;
}
}
}
return TOP_OF_TROUBLE; // Deliberately head for trouble...
}
// Now for the new stuff
/**
* Handles append time behavior for CompositeRollingAppender. This checks if a roll over either by date (checked
* first) or time (checked second) is need and then appends to the file last.
*/
protected void subAppend(LoggingEvent event)
{
if (rollDate)
{
long n = System.currentTimeMillis();
if (n >= nextCheck)
{
now.setTime(n);
nextCheck = rc.getNextCheckMillis(now);
rollOverTime();
}
}
if (rollSize)
{
if ((fileName != null) && (((CountingQuietWriter) qw).getCount() >= maxFileSize))
{
rollOverSize();
}
}
super.subAppend(event);
}
public void setFile(String file)
{
baseFileName = file.trim();
fileName = file.trim();
}
/**
* Creates and opens the file for logging. If If the maximum number of size based backups is reached ( If If
*
* @author Kevin Steppe
* @author Heinz Richter
* @author Eirik Lygre
* @author Ceki Gülcü
* @author Martin Ritchie
*/
public class QpidCompositeRollingAppender extends FileAppender
{
// The code assumes that the following 'time' constants are in a increasing
// sequence.
static final int TOP_OF_TROUBLE = -1;
static final int TOP_OF_MINUTE = 0;
static final int TOP_OF_HOUR = 1;
static final int HALF_DAY = 2;
static final int TOP_OF_DAY = 3;
static final int TOP_OF_WEEK = 4;
static final int TOP_OF_MONTH = 5;
/** Style of rolling to use */
static final int BY_SIZE = 1;
static final int BY_DATE = 2;
static final int BY_COMPOSITE = 3;
// Not currently used
static final String S_BY_SIZE = "Size";
static final String S_BY_DATE = "Date";
static final String S_BY_COMPOSITE = "Composite";
/** The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" meaning daily rollover. */
private String datePattern = "'.'yyyy-MM-dd";
/**
* The actual formatted filename that is currently being written to or will be the file transferred to on roll over
* (based on staticLogFileName).
*/
private String scheduledFilename = null;
/** The timestamp when we shall next recompute the filename. */
private long nextCheck = System.currentTimeMillis() - 1;
/** Holds date of last roll over */
private Date now = new Date();
private SimpleDateFormat sdf;
/** Helper class to determine next rollover time */
private RollingCalendar rc = new RollingCalendar();
private long maxFileSize = 10 * 1024 * 1024;
private int maxSizeRollBackups = 0;
private int curSizeRollBackups = 0;
private int maxTimeRollBackups = -1;
private int curTimeRollBackups = 0;
private int countDirection = -1;
private int rollingStyle = BY_COMPOSITE;
private boolean rollDate = true;
private boolean rollSize = true;
private boolean staticLogFileName = true;
private String baseFileName;
private boolean compress = false;
private boolean compressAsync = false;
private boolean zeroBased = false;
private String backupFilesToPath = null;
private final ConcurrentLinkedQueueCompositeRollingAppender
and open the file designated by filename
. The
* opened filename will become the ouput destination for this appender.
*/
public QpidCompositeRollingAppender(Layout layout, String filename, String datePattern) throws IOException
{
this(layout, filename, datePattern, true);
}
/**
* Instantiate a CompositeRollingAppender and open the file designated by filename
. The opened filename
* will become the ouput destination for this appender.
*
* append
parameter is true, the file will be appended to. Otherwise, the file desginated by
* filename
will be truncated before being opened.
*/
public QpidCompositeRollingAppender(Layout layout, String filename, boolean append) throws IOException
{
super(layout, filename, append);
}
/**
* Instantiate a CompositeRollingAppender and open the file designated by filename
. The opened filename
* will become the ouput destination for this appender.
*/
public QpidCompositeRollingAppender(Layout layout, String filename, String datePattern, boolean append)
throws IOException
{
super(layout, filename, append);
this.datePattern = datePattern;
activateOptions();
}
/**
* Instantiate a CompositeRollingAppender and open the file designated by filename
. The opened filename
* will become the output destination for this appender.
*
* MaxFileSize
. If a negative number is supplied then no deletions will be
* made. Note that this could result in very slow performance as a large number of files are rolled over unless
* {@link #setCountDirection} up is used.
*
* long
argument from the setter taking a String
argument by the JavaBeans {@link
* java.beans.Introspector Introspector}.
*
* @see #setMaxFileSize(String)
*/
public void setMaxFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* Set the maximum size that the output file is allowed to reach before being rolled over to backup files.
*
* long
argument from the setter taking a String
argument by the JavaBeans {@link
* java.beans.Introspector Introspector}.
*
* @see #setMaxFileSize(String)
*/
public void setMaximumFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* Set the maximum size that the output file is allowed to reach before being rolled over to backup files.
*
* staticLogFileName
is false then the fully qualified name
* is determined and used.
*/
public synchronized void setFile(String fileName, boolean append) throws IOException
{
if (!staticLogFileName)
{
scheduledFilename = fileName = fileName.trim() + sdf.format(now);
}
super.setFile(fileName, append, bufferedIO, bufferSize);
if (append)
{
File f = new File(fileName);
((CountingQuietWriter) qw).setCount(f.length());
}
}
/**
* By default newer files have lower numbers. (countDirection {@literal <} 0) ie. log.1 is most recent, log.5 is the 5th
* backup, etc... countDirection {@literal >} 0 does the opposite ie. log.1 is the first backup made, log.5 is the 5th backup
* made, etc. For infinite backups use countDirection {@literal >} 0 to reduce rollOver costs.
*/
public int getCountDirection()
{
return countDirection;
}
public void setCountDirection(int direction)
{
countDirection = direction;
}
/** Style of rolling to Use. BY_SIZE (1), BY_DATE(2), BY COMPOSITE(3) */
public int getRollingStyle()
{
return rollingStyle;
}
public void setRollingStyle(int style)
{
rollingStyle = style;
switch (rollingStyle)
{
case BY_SIZE:
rollDate = false;
rollSize = true;
break;
case BY_DATE:
rollDate = true;
rollSize = false;
break;
case BY_COMPOSITE:
rollDate = true;
rollSize = true;
break;
default:
errorHandler.error("Invalid rolling Style, use 1 (by size only), 2 (by date only) or 3 (both)");
}
}
public boolean getStaticLogFileName()
{
return staticLogFileName;
}
public void setStaticLogFileName(boolean s)
{
staticLogFileName = s;
}
public void setStaticLogFileName(String value)
{
setStaticLogFileName(OptionConverter.toBoolean(value, true));
}
public boolean getCompressBackupFiles()
{
return compress;
}
public void setCompressBackupFiles(boolean c)
{
compress = c;
}
public boolean getCompressAsync()
{
return compressAsync;
}
public void setCompressAsync(boolean c)
{
compressAsync = c;
if (compressAsync)
{
executor = Executors.newFixedThreadPool(1);
compressor = new Compressor();
}
}
public boolean getZeroBased()
{
return zeroBased;
}
public void setZeroBased(boolean z)
{
zeroBased = z;
}
/** Path provided in configuration. Used for moving backup files to */
public String getBackupFilesToPath()
{
return backupFilesToPath;
}
public void setbackupFilesToPath(String path)
{
File td = new File(path);
if (!td.exists())
{
td.mkdirs();
}
backupFilesToPath = path;
}
/**
* Initializes based on existing conditions at time of activateOptions
. The following is done:
*
A) determine curSizeRollBackups
B) determine curTimeRollBackups (not implemented)
C) initiates a
* roll over if needed for crossing a date boundary since the last run.
*/
protected void existingInit()
{
curTimeRollBackups = 0;
// part A starts here
// This is now down at first log when curSizeRollBackup==0 see rollFile
// part A ends here
// part B not yet implemented
// part C
if (staticLogFileName && rollDate)
{
File old = new File(baseFileName);
if (old.exists())
{
Date last = new Date(old.lastModified());
if (!(sdf.format(last).equals(sdf.format(now))))
{
scheduledFilename = baseFileName + sdf.format(last);
LogLog.debug("Initial roll over to: " + scheduledFilename);
rollOverTime();
}
}
}
LogLog.debug("curSizeRollBackups after rollOver at: " + curSizeRollBackups);
// part C ends here
}
/**
* Sets initial conditions including date/time roll over information, first check, scheduledFilename, and calls
* existingInit
to initialize the current # of backups.
*/
public void activateOptions()
{
// REMOVE removed rollDate from boolean to enable Alex's change
if (datePattern != null)
{
now.setTime(System.currentTimeMillis());
sdf = new SimpleDateFormat(datePattern);
int type = computeCheckPeriod();
rc.setType(type);
// next line added as this removes the name check in rollOver
nextCheck = rc.getNextCheckMillis(now);
}
else
{
if (rollDate)
{
LogLog.error("Either DatePattern or rollingStyle options are not set for [" + name + "].");
}
}
existingInit();
if (rollDate && (fileName != null) && (scheduledFilename == null))
{
scheduledFilename = fileName + sdf.format(now);
}
try
{
this.setFile(fileName, true);
}
catch (IOException e)
{
errorHandler.error("Cannot set file name:" + fileName);
}
super.activateOptions();
}
/**
* Rollover the file(s) to date/time tagged file(s). Opens the new file (through setFile) and resets
* curSizeRollBackups.
*/
protected void rollOverTime()
{
curTimeRollBackups++;
this.closeFile(); // keep windows happy.
rollFile();
try
{
curSizeRollBackups = 0; // We're cleared out the old date and are ready for the new
// new scheduled name
scheduledFilename = fileName + sdf.format(now);
this.setFile(baseFileName, false);
}
catch (IOException e)
{
errorHandler.error("setFile(" + fileName + ", false) call failed.");
}
}
/**
* Renames file from
to file to
. It also checks for existence of target file and deletes
* if it does.
*/
protected void rollFile(String from, String to, boolean compress)
{
if (from.equals(to))
{
if (compress)
{
LogLog.error("Attempting to compress file with same output name.");
}
return;
}
if (backupFilesToPath != null)
{
to = backupFilesToPath + System.getProperty("file.separator") + new File(to).getName();
}
File target = new File(to);
File file = new File(from);
// Perform Roll by renaming
if (!file.getPath().equals(target.getPath()))
{
file.renameTo(target);
}
// Compress file after it has been moved out the way... this is safe
// as it will gain a .gz ending and we can then safely delete this file
// as it will not be the statically named value.
if (compress)
{
compress(target);
}
LogLog.debug(from + " -> " + to);
}
private void compress(File target)
{
if (compressAsync)
{
synchronized (_compress)
{
_compress.offer(new CompressJob(target, target));
}
startCompression();
}
else
{
doCompress(target, target);
}
}
private void startCompression()
{
if (_compressing.compareAndSet(false, true))
{
executor.execute(compressor);
}
}
/**
* Delete the given file that is prepended with the relative path to the log
* directory.
*
* Compress is enabled check for file with COMPRESS_EXTENSION(.gz)
*
* if backupFilesToPath is set then check in this directory not the
* main log directory.
*/
protected void deleteFile(String relativeFileName)
{
String fileName="";
// If we have configured a backup location then we should look in there
// for the file we are trying to delete
if (backupFilesToPath != null)
{
File file = new File(relativeFileName);
fileName = backupFilesToPath + System.getProperty("file.separator") + file.getName();
}
// If we are compressing the at the extension
if (compress)
{
fileName += COMPRESS_EXTENSION;
}
File file = new File(fileName);
if (file.exists())
{
file.delete();
}
}
/**
* Implements roll overs base on file size.
*
* curSizeRollBackups {@literal ==} maxSizeRollBackups
)
* then the oldest file is deleted -- it's index determined by the sign of countDirection.
If
* countDirection
{@literal <} 0, then files {File.1
, ..., File.curSizeRollBackups -1
}
* are renamed to {File.2
, ..., File.curSizeRollBackups
}. Moreover, File
is
* renamed File.1
and closed.
*
* A new file is created to receive further log output.
*
* maxSizeRollBackups
is equal to zero, then the File
is truncated with no backup
* files created.
*
* maxSizeRollBackups
{@literal <} 0, then File
is renamed if needed and no files are deleted.
*/
// synchronization not necessary since doAppend is already synched
protected void rollOverSize()
{
File file;
this.closeFile(); // keep windows happy.
LogLog.debug("rolling over count=" + ((CountingQuietWriter) qw).getCount());
LogLog.debug("maxSizeRollBackups = " + maxSizeRollBackups);
LogLog.debug("curSizeRollBackups = " + curSizeRollBackups);
LogLog.debug("countDirection = " + countDirection);
// If maxBackups <= 0, then there is no file renaming to be done.
if (maxSizeRollBackups != 0)
{
rollFile();
}
try
{
// This will also close the file. This is OK since multiple
// close operations are safe.
this.setFile(baseFileName, false);
}
catch (IOException e)
{
LogLog.error("setFile(" + fileName + ", false) call failed.", e);
}
}
/**
* Perform file Rollover ensuring the countDirection is applied along with
* the other options
*/
private void rollFile()
{
LogLog.debug("CD="+countDirection+",start");
if (countDirection < 0)
{
// If we haven't rolled yet then validate we have the right value
// for curSizeRollBackups
if (curSizeRollBackups == 0)
{
//Validate curSizeRollBackups
curSizeRollBackups = countFileIndex(fileName);
// decrement to offset the later increment
curSizeRollBackups--;
}
// If we are not keeping an infinite set of backups the delete oldest
if (maxSizeRollBackups > 0)
{
LogLog.debug("CD=-1,curSizeRollBackups:"+curSizeRollBackups);
LogLog.debug("CD=-1,maxSizeRollBackups:"+maxSizeRollBackups);
// Delete the oldest file.
// curSizeRollBackups is never -1 so infinite backups are ok here
if ((curSizeRollBackups - maxSizeRollBackups) >= 0)
{
//The oldest file is the one with the largest number
// as the 0 is always fileName
// which moves to fileName.1 etc.
LogLog.debug("CD=-1,deleteFile:"+curSizeRollBackups);
deleteFile(fileName + '.' + curSizeRollBackups);
// decrement to offset the later increment
curSizeRollBackups--;
}
}
/*
map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3, 2}.
*/
for (int i = curSizeRollBackups; i >= 1; i--)
{
String oldName = (fileName + "." + i);
String newName = (fileName + '.' + (i + 1));
// Ensure that when compressing we rename the compressed archives
if (compress)
{
rollFile(oldName + COMPRESS_EXTENSION, newName + COMPRESS_EXTENSION, false);
}
else
{
rollFile(oldName, newName, false);
}
}
curSizeRollBackups++;
// Rename fileName to fileName.1
rollFile(fileName, fileName + ".1", compress);
} // REMOVE This code branching for Alexander Cerna's request
else if (countDirection == 0)
{
// rollFile based on date pattern
now.setTime(System.currentTimeMillis());
String newFile = fileName + sdf.format(now);
// If we haven't rolled yet then validate we have the right value
// for curSizeRollBackups
if (curSizeRollBackups == 0)
{
//Validate curSizeRollBackups
curSizeRollBackups = countFileIndex(newFile);
// to balance the increment just coming up. as the count returns
// the next free number not the last used.
curSizeRollBackups--;
}
// If we are not keeping an infinite set of backups the delete oldest
if (maxSizeRollBackups > 0)
{
// Don't prune older files if they exist just go for the last
// one based on our maxSizeRollBackups. This means we may have
// more files left on disk that maxSizeRollBackups if this value
// is adjusted between runs but that is an acceptable state.
// Otherwise we would have to check on startup that we didn't
// have more than maxSizeRollBackups and prune then.
if (((curSizeRollBackups - maxSizeRollBackups) >= 0))
{
LogLog.debug("CD=0,curSizeRollBackups:"+curSizeRollBackups);
LogLog.debug("CD=0,maxSizeRollBackups:"+maxSizeRollBackups);
// delete the first and keep counting up.
int oldestFileIndex = curSizeRollBackups - maxSizeRollBackups + 1;
LogLog.debug("CD=0,deleteFile:"+oldestFileIndex);
deleteFile(newFile + '.' + oldestFileIndex);
}
}
String finalName = newFile;
curSizeRollBackups++;
// Add rollSize if it is > 0
if (curSizeRollBackups > 0 )
{
finalName = newFile + '.' + curSizeRollBackups;
}
rollFile(fileName, finalName, compress);
}
else
{ // countDirection > 0
// If we haven't rolled yet then validate we have the right value
// for curSizeRollBackups
if (curSizeRollBackups == 0)
{
//Validate curSizeRollBackups
curSizeRollBackups = countFileIndex(fileName);
// to balance the increment just coming up. as the count returns
// the next free number not the last used.
curSizeRollBackups--;
}
// If we are not keeping an infinite set of backups the delete oldest
if (maxSizeRollBackups > 0)
{
LogLog.debug("CD=1,curSizeRollBackups:"+curSizeRollBackups);
LogLog.debug("CD=1,maxSizeRollBackups:"+maxSizeRollBackups);
// Don't prune older files if they exist just go for the last
// one based on our maxSizeRollBackups. This means we may have
// more files left on disk that maxSizeRollBackups if this value
// is adjusted between runs but that is an acceptable state.
// Otherwise we would have to check on startup that we didn't
// have more than maxSizeRollBackups and prune then.
if (((curSizeRollBackups - maxSizeRollBackups) >= 0))
{
// delete the first and keep counting up.
int oldestFileIndex = curSizeRollBackups - maxSizeRollBackups + 1;
LogLog.debug("CD=1,deleteFile:"+oldestFileIndex);
deleteFile(fileName + '.' + oldestFileIndex);
}
}
curSizeRollBackups++;
rollFile(fileName, fileName + '.' + curSizeRollBackups, compress);
}
LogLog.debug("CD="+countDirection+",done");
}
private int countFileIndex(String fileName)
{
return countFileIndex(fileName, true);
}
/**
* Use filename as a base name and find what count number we are up to by
* looking at the files in this format:
*
*