All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.util.logging.handler.ArchivingFileHandler Maven / Gradle / Ivy

Go to download

A versatile Java(TM) library that implements many useful container and utility classes.

There is a newer version: 1.1.12
Show newest version

/*
 * 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.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.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.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.
 * 

* Initially, the "current log file" is created (or, if it already exists and the "append" option is {@code true}, * opened in "append" mode), 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) */ @NotNullByDefault(false) 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 {@code 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.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; } } /** * 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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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 de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_LEVEL}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FILTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.AbstractStreamHandler#DEFAULT_FORMATTER}{@code ,} * {@link de.unkrig.commons.util.logging.handler.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, 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"; // 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(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 alternativeArchiveFile = new File(archiveFileName + '.' + uniqueId); if (!alternativeArchiveFile.exists()) { archiveFile = alternativeArchiveFile; 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 - 2024 Weber Informatics LLC | Privacy Policy