de.unkrig.commons.util.logging.handler.ArchivingFileHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of de-unkrig-commons Show documentation
Show all versions of de-unkrig-commons Show documentation
A versatile Java(TM) library that implements many useful container and utility classes.
/*
* de.unkrig.commons - A general-purpose Java class library
*
* Copyright (c) 2012, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.commons.util.logging.handler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import de.unkrig.commons.io.IoUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil.Produmer;
import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.expression.EvaluationException;
import de.unkrig.commons.text.parser.ParseException;
import de.unkrig.commons.util.TimeTable;
import de.unkrig.commons.util.logging.LogUtil;
/**
* A log record handler which writes formatted records to an {@link OutputStream} which must be specified through
* a ".outputStream" logging property.
*/
@NotNullByDefault(false) public
class ArchivingFileHandler extends AbstractStreamHandler {
/**
* No-arg constructor to be used by the log manager.
*/
public
ArchivingFileHandler() throws ParseException, EvaluationException, IOException {
this(null);
}
/**
* Single-arg constructor to be used by proxies.
*/
public
ArchivingFileHandler(@Nullable String propertyNamePrefix) throws ParseException, EvaluationException, IOException {
super(propertyNamePrefix);
if (propertyNamePrefix == null) propertyNamePrefix = this.getClass().getName();
// We cannot be sure how the LogManager processes exceptions thrown by this constructor, so we print a stack
// trace to STDERR before we rethrow the exception.
// (The JRE default log manager prints a stack trace, too, so we'll see two.)
try {
this.init(
LogUtil.getLoggingProperty(propertyNamePrefix + ".pattern", ArchivingFileHandler.DEFAULT_PATTERN),
LogUtil.getLoggingProperty(propertyNamePrefix + ".sizeLimit", ArchivingFileHandler.DEFAULT_SIZE_LIMIT),
LogUtil.getLoggingProperty(
propertyNamePrefix + ".timeTable",
TimeTable.class,
ArchivingFileHandler.DEFAULT_TIME_TABLE
),
LogUtil.getLoggingProperty(propertyNamePrefix + ".append", false)
);
} catch (ParseException pe) {
pe.printStackTrace();
throw pe;
} catch (EvaluationException ee) {
ee.printStackTrace();
throw ee;
} catch (RuntimeException re) {
re.printStackTrace();
throw re;
}
}
/**
* @param pattern The pattern for the archive file names: '%d' is replaced with the current date
* @param sizeLimit The size limit for the current file
* @param timeTable The time table for time-based archiving
* @param append Whether to append to an existring current file
* @param autoFlush See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
* @param level See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
* @param filter See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
* @param formatter See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
* @param encoding See {@link StreamHandler#StreamHandler(OutputStream, boolean, Level, Filter, Formatter, String)}
*/
public
ArchivingFileHandler(
String pattern,
long sizeLimit,
TimeTable timeTable,
boolean append,
boolean autoFlush,
Level level,
Filter filter,
Formatter formatter,
String encoding
) throws IOException {
super(autoFlush, level, filter, formatter, encoding);
this.init(pattern, sizeLimit, timeTable, append);
}
@Override public synchronized void
publish(LogRecord record) {
// Check the current time.
if (new Date().compareTo(this.nextArchiving) >= 0) {
try {
this.archive(this.nextArchiving);
} catch (Exception e) {
this.reportError(null, e, ErrorManager.WRITE_FAILURE);
}
}
super.publish(record);
// Check the current file size.
Long fileSize = this.byteCount.produce();
assert fileSize != null;
if (fileSize >= this.sizeLimit) {
try {
this.archive(new Date());
} catch (Exception e) {
this.reportError(null, e, ErrorManager.WRITE_FAILURE);
}
}
}
// CONFIGURATION
/**
* The pattern for the archive file names: Contains '%d', which must be replaced with the current date.
*/
private String fileNamePattern;
/**
* The size limit for the current file.
*/
private long sizeLimit;
/**
* The time table for time-based archiving.
*/
private TimeTable timeTable;
// STATE
/**
* Name of the "current file", i.e. the file that is written to, as opposed to the "archive files".
*/
private File currentFile;
/**
* The point-in-time of the next time table-based archiving.
*/
private Date nextArchiving;
/**
* Tracks the size of the current file.
*/
private Produmer byteCount;
// CONSTANTS
/**
* A special value for the {@code sizeLimit} paramter of {@link ArchivingFileHandler#ArchivingFileHandler(String,
* long, TimeTable, boolean, boolean, Level, Filter, Formatter, String)} indicating that no limit should apply.
*/
public static final long NO_LIMIT = Long.MAX_VALUE;
/**
* The {@link SimpleDateFormat} pattern for the '%d' variable.
*/
private static final String DATE_PATTERN = "_yyyy-MM-dd_HH-mm-ss";
// Default values for the configuration parameters:
private static final String DEFAULT_PATTERN = "%h/java%d.log";
private static final long DEFAULT_SIZE_LIMIT = ArchivingFileHandler.NO_LIMIT;
private static final TimeTable DEFAULT_TIME_TABLE = TimeTable.NEVER;
// IMPLEMENTATION
private void
init(String pattern, long sizeLimit, TimeTable timeTable, boolean append)
throws IOException {
// Preprocess the file name pattern.
pattern = pattern.replace('/', File.separatorChar);
pattern = ArchivingFileHandler.replaceAll(pattern, "%%", "\u0001");
pattern = ArchivingFileHandler.replaceAll(
pattern,
"%h",
System.getProperty("user.home", ".") + File.separatorChar
);
pattern = ArchivingFileHandler.replaceAll(
pattern,
"%t",
System.getProperty("java.io.tmpdir", ".") + File.separatorChar
);
if (!pattern.contains("%d")) pattern += "%d";
this.fileNamePattern = (pattern = pattern.replace('\u0001', '%'));
this.currentFile = new File(ArchivingFileHandler.replaceAll(pattern, "%d", ""));
// Remember size limit and time table.
this.sizeLimit = sizeLimit;
this.timeTable = timeTable;
// Rename an existing "current file" if we're not in APPEND mode or the file is too large.
if (this.currentFile.exists() && (!append || this.currentFile.length() >= this.sizeLimit)) {
if (!this.currentFile.renameTo(this.getArchiveFile(new Date()))) {
// For whatever reason, the current file could not be renamed. Most likely (under Windows) another
// program (or logger) has the file open.
// The fallback strategy is to forget about size limit, appending etc. and start appending to the
// current log file.
this.sizeLimit = ArchivingFileHandler.NO_LIMIT;
this.nextArchiving = TimeTable.MAX_DATE;
}
}
this.openCurrentFile();
this.nextArchiving = this.timeTable.next(new Date());
}
/**
* @return A file that is guaranteed not to exist
*/
private File
getArchiveFile(Date date) {
// Compute the archiv file name.
String archiveFileName = ArchivingFileHandler.replaceAll(
this.fileNamePattern,
"%d",
new SimpleDateFormat(ArchivingFileHandler.DATE_PATTERN).format(date)
);
File archiveFile = new File(archiveFileName);
// If log files rotate very quickly, there might be a name clash. Append an integer number to the archive file
// name to make it unique.
if (archiveFile.exists()) {
for (int uniqueId = 1;; uniqueId++) {
File alternateArchiveFile = new File(archiveFileName + '.' + uniqueId);
if (!alternateArchiveFile.exists()) {
archiveFile = alternateArchiveFile;
break;
}
}
}
return archiveFile;
}
private void
openCurrentFile() throws IOException {
this.byteCount = ConsumerUtil.store();
this.setOutputStream(IoUtil.tee(
new FileOutputStream(this.currentFile, true),
IoUtil.lengthWritten(ConsumerUtil.cumulate(this.byteCount, 0L))
));
}
/**
* Closes the current file, renames it, and recreates the current file.
*/
private void
archive(Date date) throws IOException {
// Close the current file.
this.close();
// Archive (rename) the current file.
if (!this.currentFile.renameTo(this.getArchiveFile(date))) {
// For whatever reason, the current file could not be renamed. Most likely (under Windows) another program
// (or logger) has the file open.
// The fallback strategy is to forget about size limits, appending etc. and re-open the current log file.
this.sizeLimit = ArchivingFileHandler.NO_LIMIT;
this.nextArchiving = TimeTable.MAX_DATE;
} else {
this.nextArchiving = this.timeTable.next(new Date());
}
// Re-open/re-create the current file.
this.openCurrentFile();
}
/**
* Replace all occurrences of {@code infix} within {@subject} with the given {@code replacement}.
*/
private static String
replaceAll(String subject, String infix, String replacement) {
for (int idx = subject.indexOf(infix); idx != -1; idx = subject.indexOf(infix, idx + replacement.length())) {
subject = subject.substring(0, idx) + replacement + subject.substring(idx + infix.length());
}
return subject;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy