de.unkrig.commons.util.logging.handler.ArchivingFileHandler Maven / Gradle / Ivy
/*
* 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. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.DateFormat;
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.LogManager;
import java.util.logging.LogRecord;
import de.unkrig.commons.io.OutputStreams;
import de.unkrig.commons.lang.protocol.ConsumerUtil;
import de.unkrig.commons.lang.protocol.ConsumerUtil.Produmer;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.expression.EvaluationException;
import de.unkrig.commons.text.expression.Parser;
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 files which are automatically archived.
*
* Intially, if the "current log file" exists, it renames it (or, if it already exists and the "append" option is
* {@code true}, opens it in "append" mode).
*
*
* Initially, the "current log file" is renamed and re-created
* and log records are written to it. When it is time to "archive" the current log file,
* then it is closed, renamed and re-created.
*
*
* @see #ArchivingFileHandler() The zero-parameter constructor that is used for properties-based configuration
* (as through the "{@code logging.properties}" file)
*/
public
class ArchivingFileHandler extends AbstractStreamHandler {
/**
* The zero-parameter constructor (which is used by the {@link LogManager}).
* The {@link ArchivingFileHandler}, like all {@code java.util.logging} handlers, initializes itself with a set of
* "logging properties" which it retrieves through the {@link LogManager}, which normally reads them from the
* "{@code logging.properties}" file:
*
* - class-name{@code .pattern}
* -
* Determines the pathes of the log files; see the pattern parameter of {@link
* #ArchivingFileHandler(String, long, TimeTable, boolean, boolean, Level, Filter, Formatter, String)}.
* Defaults to {@value #DEFAULT_PATTERN}.
*
* - class-name{@code .sizeLimit}
* -
* Determines that log file archiving triggers when the size of the current log file exceeds a given threshold;
* an integral value, optionally followed by one of "k", "M", "G", "T", "P". Defaults to "none".
*
* - class-name{@code .timeTable}
* -
* An expression that defines the {@link TimeTable} for log file archiving with {@link Parser this syntax}.
* Defaults to "never".
*
* Example: "{@code de.unkrig.commons.util.TimeTable.parse("*:0")}" for "every full hour".
*
* - class-name{@code .append}
* -
* Whether to append to the current log file (if it initially exists); defaults to {@code false}.
*
*
*/
public
ArchivingFileHandler() throws ParseException, EvaluationException, IOException {
this(0, null);
}
/**
* Single-arg constructor to be used by proxies.
* Constructor for extending classes that wish to impose different logging property than that used
* by {@link #ArchivingFileHandler()}.
*
* @param dummy Only there to distinguish this constructor from {@link #ArchivingFileHandler(String)}
* @param propertyNamePrefix The property name prefix, or {@code null} to use the qualified name of the
* actual handler class
*/
protected
ArchivingFileHandler(int dummy, @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.fileNamePattern = ArchivingFileHandler.preprocessPattern(
LogUtil.getLoggingProperty(propertyNamePrefix + ".pattern", ArchivingFileHandler.DEFAULT_PATTERN)
);
this.currentFile = new File(ArchivingFileHandler.replaceAll(this.fileNamePattern, "%d", ""));
// Remember size limit and time table.
this.sizeLimit = LogUtil.getLoggingProperty(
propertyNamePrefix + ".sizeLimit",
ArchivingFileHandler.DEFAULT_SIZE_LIMIT
);
this.timeTable = LogUtil.getLoggingProperty(
propertyNamePrefix + ".timeTable",
TimeTable.class,
ArchivingFileHandler.DEFAULT_TIME_TABLE
);
this.init(LogUtil.getLoggingProperty(propertyNamePrefix + ".append", false));
this.nextArchiving = this.timeTable.next(new Date());
} catch (ParseException pe) {
pe.printStackTrace();
throw pe;
} catch (EvaluationException ee) {
ee.printStackTrace();
throw ee;
} catch (RuntimeException re) {
re.printStackTrace();
throw re;
}
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* {@link #DEFAULT_SIZE_LIMIT}{@code ,}
* {@link #DEFAULT_TIME_TABLE}{@code ,}
* {@value #DEFAULT_APPEND}{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern) throws IOException {
this(
pattern, // pattern
ArchivingFileHandler.DEFAULT_SIZE_LIMIT, // sizeLimit
ArchivingFileHandler.DEFAULT_TIME_TABLE, // timeTable
ArchivingFileHandler.DEFAULT_APPEND, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* {@value #DEFAULT_SIZE_LIMIT}{@code ,}
* {@link #DEFAULT_TIME_TABLE}{@code ,}
* append{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern, boolean append) throws IOException {
this(
pattern, // pattern
ArchivingFileHandler.DEFAULT_SIZE_LIMIT, // sizeLimit
ArchivingFileHandler.DEFAULT_TIME_TABLE, // timeTable
append, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* sizeLimit{@code ,}
* {@link #DEFAULT_TIME_TABLE}{@code ,}
* {@value de.unkrig.commons.util.logging.handler.ArchivingFileHandler#DEFAULT_APPEND}{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern, long sizeLimit) throws IOException {
this(
pattern, // pattern
sizeLimit, // sizeLimit
ArchivingFileHandler.DEFAULT_TIME_TABLE, // timeTable
ArchivingFileHandler.DEFAULT_APPEND, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* sizeLimit{@code ,}
* {@link #DEFAULT_TIME_TABLE}{@code ,}
* append{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern, long sizeLimit, boolean append) throws IOException {
this(
pattern, // pattern
sizeLimit, // sizeLimit
ArchivingFileHandler.DEFAULT_TIME_TABLE, // timeTable
append, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* {@value #DEFAULT_SIZE_LIMIT}{@code ,}
* timeTable{@code ,}
* {@value #DEFAULT_APPEND}{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern, TimeTable timeTable) throws IOException {
this(
pattern, // pattern
ArchivingFileHandler.DEFAULT_SIZE_LIMIT, // sizeLimit
timeTable, // timeTable
ArchivingFileHandler.DEFAULT_APPEND, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* Equivalent with {@code ArchivingFileHandler(}pattern{@code ,}
* {@value #DEFAULT_SIZE_LIMIT}{@code ,}
* timeTable{@code ,}
* append{@code ,}
* {@value de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_AUTO_FLUSH}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_LEVEL}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FILTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,}
* {@link AbstractStreamHandler#DEFAULT_ENCODING}{@code )}.
*/
public
ArchivingFileHandler(String pattern, TimeTable timeTable, boolean append) throws IOException {
this(
pattern, // pattern
ArchivingFileHandler.DEFAULT_SIZE_LIMIT, // sizeLimit
timeTable, // timeTable
append, // append
AbstractStreamHandler.DEFAULT_AUTO_FLUSH, // autoFlush
AbstractStreamHandler.DEFAULT_LEVEL, // level
AbstractStreamHandler.DEFAULT_FILTER, // filter
AbstractStreamHandler.DEFAULT_FORMATTER, // formatter
AbstractStreamHandler.DEFAULT_ENCODING // encoding
);
}
/**
* The pattern parameter determines the pathes of the log files, relative to the "user directory" (on
* most systems: the "current working directory"), with the following placeholder replacements:
*
* - {@code %%}
* - A literal {@code "%"}
*
* - {@code %h}
* - The "user home directory" (as determined by the "{@code user.home}" system property)
*
* - {@code %t}
* - The "default temp file path" (as determined by the "{@code java.io.tmpdir}" system property)
*
* - {@code %d}
* -
* The date and time of the archiving, in the {@link DateFormat} {@code _yyyy-MM-dd_HH-mm-ss}. For the
* "current log file", "{@code %d}" is replaced with the empty string.
*
* Iff the pattern does not contain a "{@code %d}" placeholder, then "{@code %d}" is silently appended to the
* pattern.
*
*
*
* @param pattern See the {@link #ArchivingFileHandler(String, long, TimeTable, boolean, boolean, Level, Filter,
* Formatter, String) method description}
* @param sizeLimit The size limit for the current file
* @param timeTable The time table for time-based archiving
* @param append Whether to append to the current log file (if it initially exists)
* @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,
@Nullable Filter filter,
Formatter formatter,
@Nullable String encoding
) throws IOException {
super(autoFlush, level, filter, formatter, encoding);
this.fileNamePattern = ArchivingFileHandler.preprocessPattern(pattern);
this.currentFile = new File(ArchivingFileHandler.replaceAll(this.fileNamePattern, "%d", ""));
// Remember size limit and time table.
this.sizeLimit = sizeLimit;
this.timeTable = timeTable;
this.init(append);
this.nextArchiving = this.timeTable.next(new Date());
}
@Override public synchronized void
publish(@Nullable 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 = ConsumerUtil.cumulate(0);
// CONSTANTS
/**
* A special value for the 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";
// Declare constants for the handler propery default values, so they can be used with the {@value} doc tag.
// Must be public so that "@value" works for them.
// SUPPRESS CHECKSTYLE JavadocVariable:4
public static final String DEFAULT_PATTERN = "%h/java%d.log";
public static final long DEFAULT_SIZE_LIMIT = ArchivingFileHandler.NO_LIMIT;
public static final TimeTable DEFAULT_TIME_TABLE = TimeTable.NEVER;
public static final boolean DEFAULT_APPEND = false;
// IMPLEMENTATION
private void
init(boolean append) throws IOException {
// 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;
return;
}
}
this.openCurrentFile();
}
private static String
preprocessPattern(String pattern) {
// Preprocess the file name pattern.
pattern = pattern.replace('/', File.separatorChar);
// Temporarily replace "%%" with "\u0001" to prevent misinterpretation of, e.g. "%%h".
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";
}
pattern = pattern.replace('\u0001', '%');
return pattern;
}
/**
* @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 alternativeArchiveFile = new File(archiveFileName + '.' + uniqueId);
if (!alternativeArchiveFile.exists()) {
archiveFile = alternativeArchiveFile;
break;
}
}
}
return archiveFile;
}
private void
openCurrentFile() throws IOException {
this.setOutputStream(OutputStreams.tee(
new FileOutputStream(this.currentFile, true),
OutputStreams.lengthWritten((this.byteCount = ConsumerUtil.cumulate(0)))
));
}
/**
* 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 infix within subject with the given 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