org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy Maven / Gradle / Ivy
/*
* 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.logging.log4j.core.appender.rolling;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.appender.rolling.action.Action;
import org.apache.logging.log4j.core.appender.rolling.action.CommonsCompressAction;
import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.Integers;
import org.apache.logging.log4j.status.StatusLogger;
/**
* When rolling over, DefaultRolloverStrategy
renames files according to an algorithm as described below.
*
*
* The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
* pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
* pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
*
*
* When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
* file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
* will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
* and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
* specifying a large maximum value may entirely avoid renaming files.
*
*
* When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
*
*
* Let max and min represent the values of respectively the MaxIndex and MinIndex
* options. Let "foo.log" be the value of the ActiveFile option and "foo.%i.log" the value of
* FileNamePattern. Then, when rolling over, the file foo.max.log
will be deleted, the file
* foo.max-1.log
will be renamed as foo.max.log
, the file
* foo.max-2.log
renamed as foo.max-1.log
, and so on, the file
* foo.min+1.log
renamed as foo.min+2.log
. Lastly, the active file
* foo.log
will be renamed as foo.min.log
and a new active file name
* foo.log
will be created.
*
*
* Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
* are discouraged.
*
*/
@Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
public class DefaultRolloverStrategy implements RolloverStrategy {
/**
* Enumerates over supported file extensions.
*
* Package-protected for unit tests.
*/
static enum FileExtensions {
ZIP(".zip") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
return new ZipCompressAction(source(renameTo), target(compressedName), deleteSource, compressionLevel);
}
},
GZ(".gz") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
return new GzCompressAction(source(renameTo), target(compressedName), deleteSource);
}
},
BZIP2(".bz2") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
// One of "gz", "bzip2", "xz", "pack200", or "deflate".
return new CommonsCompressAction("bzip2", source(renameTo), target(compressedName), deleteSource);
}
},
DEFLATE(".deflate") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
// One of "gz", "bzip2", "xz", "pack200", or "deflate".
return new CommonsCompressAction("deflate", source(renameTo), target(compressedName), deleteSource);
}
},
PACK200(".pack200") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
// One of "gz", "bzip2", "xz", "pack200", or "deflate".
return new CommonsCompressAction("pack200", source(renameTo), target(compressedName), deleteSource);
}
},
XY(".xy") {
@Override
Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
final int compressionLevel) {
// One of "gz", "bzip2", "xz", "pack200", or "deflate".
return new CommonsCompressAction("xy", source(renameTo), target(compressedName), deleteSource);
}
};
private final String extension;
private FileExtensions(final String extension) {
Objects.requireNonNull(extension, "extension");
this.extension = extension;
}
String getExtension() {
return extension;
}
boolean isExtensionFor(final String s) {
return s.endsWith(this.extension);
}
int length() {
return extension.length();
}
File source(String fileName) {
return new File(fileName);
}
File target(String fileName) {
return new File(fileName);
}
abstract Action createCompressAction(String renameTo, String compressedName, boolean deleteSource,
int compressionLevel);
static FileExtensions lookup(String fileExtension) {
for (FileExtensions ext : values()) {
if (ext.isExtensionFor(fileExtension)) {
return ext;
}
}
return null;
}
};
/**
* Allow subclasses access to the status logger without creating another instance.
*/
protected static final Logger LOGGER = StatusLogger.getLogger();
private static final int MIN_WINDOW_SIZE = 1;
private static final int DEFAULT_WINDOW_SIZE = 7;
/**
* Index for oldest retained log file.
*/
private final int maxIndex;
/**
* Index for most recent log file.
*/
private final int minIndex;
private final boolean useMax;
private final StrSubstitutor subst;
private final int compressionLevel;
private List customActions;
private boolean stopCustomActionsOnError;
/**
* Constructs a new instance.
*
* @param minIndex The minimum index.
* @param maxIndex The maximum index.
* @param customActions custom actions to perform asynchronously after rollover
* @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
*/
protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
final int compressionLevel, final StrSubstitutor subst, final Action[] customActions,
final boolean stopCustomActionsOnError) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
this.useMax = useMax;
this.compressionLevel = compressionLevel;
this.subst = subst;
this.stopCustomActionsOnError = stopCustomActionsOnError;
this.customActions = customActions == null ? Collections. emptyList() : Arrays.asList(customActions);
}
/**
* Create the DefaultRolloverStrategy.
*
* @param max The maximum number of files to keep.
* @param min The minimum number of files to keep.
* @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
* index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
* @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
* @param customActions custom actions to perform asynchronously after rollover
* @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
* @param config The Configuration.
* @return A DefaultRolloverStrategy.
*/
@PluginFactory
public static DefaultRolloverStrategy createStrategy(
// @formatter:off
@PluginAttribute("max") final String max,
@PluginAttribute("min") final String min,
@PluginAttribute("fileIndex") final String fileIndex,
@PluginAttribute("compressionLevel") final String compressionLevelStr,
@PluginElement("Actions") final Action[] customActions,
@PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
final boolean stopCustomActionsOnError,
@PluginConfiguration final Configuration config) {
// @formatter:on
final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
int minIndex = MIN_WINDOW_SIZE;
if (min != null) {
minIndex = Integer.parseInt(min);
if (minIndex < 1) {
LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
minIndex = MIN_WINDOW_SIZE;
}
}
int maxIndex = DEFAULT_WINDOW_SIZE;
if (max != null) {
maxIndex = Integer.parseInt(max);
if (maxIndex < minIndex) {
maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
}
}
final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
customActions, stopCustomActionsOnError);
}
public int getCompressionLevel() {
return this.compressionLevel;
}
public int getMaxIndex() {
return this.maxIndex;
}
public int getMinIndex() {
return this.minIndex;
}
private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
}
/**
* Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index, the
* newest the highest.
*
* @param lowIndex low index
* @param highIndex high index. Log file associated with high index will be deleted if needed.
* @param manager The RollingFileManager
* @return true if purge was successful and rollover should be attempted.
*/
private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
final List renames = new ArrayList<>();
final StringBuilder buf = new StringBuilder();
// LOG4J2-531: directory scan & rollover must use same format
manager.getPatternProcessor().formatFileName(subst, buf, highIndex);
String highFilename = subst.replace(buf);
final int suffixLength = suffixLength(highFilename);
int maxIndex = 0;
for (int i = highIndex; i >= lowIndex; i--) {
File toRename = new File(highFilename);
if (i == highIndex && toRename.exists()) {
maxIndex = highIndex;
} else if (maxIndex == 0 && toRename.exists()) {
maxIndex = i + 1;
break;
}
boolean isBase = false;
if (suffixLength > 0) {
final File toRenameBase = new File(highFilename.substring(0, highFilename.length() - suffixLength));
if (toRename.exists()) {
if (toRenameBase.exists()) {
LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
toRenameBase, toRename);
toRenameBase.delete();
}
} else {
toRename = toRenameBase;
isBase = true;
}
}
if (toRename.exists()) {
//
// if at lower index and then all slots full
// attempt to delete last file
// if that fails then abandon purge
if (i == lowIndex) {
LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.",
toRename, i);
if (!toRename.delete()) {
return -1;
}
break;
}
//
// if intermediate index
// add a rename action to the list
buf.setLength(0);
// LOG4J2-531: directory scan & rollover must use same format
manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
final String lowFilename = subst.replace(buf);
String renameTo = lowFilename;
if (isBase) {
renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
}
renames.add(new FileRenameAction(toRename, new File(renameTo), true));
highFilename = lowFilename;
} else {
buf.setLength(0);
// LOG4J2-531: directory scan & rollover must use same format
manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
highFilename = subst.replace(buf);
}
}
if (maxIndex == 0) {
maxIndex = lowIndex;
}
//
// work renames backwards
//
for (int i = renames.size() - 1; i >= 0; i--) {
final Action action = renames.get(i);
try {
LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
i, renames.size(), action);
if (!action.execute()) {
return -1;
}
} catch (final Exception ex) {
LOGGER.warn("Exception during purge in RollingFileAppender", ex);
return -1;
}
}
return maxIndex;
}
/**
* Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
* oldest will have the highest.
*
* @param lowIndex low index
* @param highIndex high index. Log file associated with high index will be deleted if needed.
* @param manager The RollingFileManager
* @return true if purge was successful and rollover should be attempted.
*/
private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
final List renames = new ArrayList<>();
final StringBuilder buf = new StringBuilder();
// LOG4J2-531: directory scan & rollover must use same format
manager.getPatternProcessor().formatFileName(subst, buf, lowIndex);
String lowFilename = subst.replace(buf);
final int suffixLength = suffixLength(lowFilename);
for (int i = lowIndex; i <= highIndex; i++) {
File toRename = new File(lowFilename);
boolean isBase = false;
if (suffixLength > 0) {
final File toRenameBase = new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
if (toRename.exists()) {
if (toRenameBase.exists()) {
LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
toRenameBase, toRename);
toRenameBase.delete();
}
} else {
toRename = toRenameBase;
isBase = true;
}
}
if (toRename.exists()) {
//
// if at upper index then
// attempt to delete last file
// if that fails then abandon purge
if (i == highIndex) {
LOGGER.debug(
"DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
toRename, i);
if (!toRename.delete()) {
return -1;
}
break;
}
//
// if intermediate index
// add a rename action to the list
buf.setLength(0);
// LOG4J2-531: directory scan & rollover must use same format
manager.getPatternProcessor().formatFileName(subst, buf, i + 1);
final String highFilename = subst.replace(buf);
String renameTo = highFilename;
if (isBase) {
renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
}
renames.add(new FileRenameAction(toRename, new File(renameTo), true));
lowFilename = highFilename;
} else {
break;
}
}
//
// work renames backwards
//
for (int i = renames.size() - 1; i >= 0; i--) {
final Action action = renames.get(i);
try {
LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
i, renames.size(), action);
if (!action.execute()) {
return -1;
}
} catch (final Exception ex) {
LOGGER.warn("Exception during purge in RollingFileAppender", ex);
return -1;
}
}
return lowIndex;
}
private int suffixLength(final String lowFilename) {
for (FileExtensions extension : FileExtensions.values()) {
if (extension.isExtensionFor(lowFilename)) {
return extension.length();
}
}
return 0;
}
/**
* Perform the rollover.
*
* @param manager The RollingFileManager name for current active log file.
* @return A RolloverDescription.
* @throws SecurityException if an error occurs.
*/
@Override
public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
if (maxIndex < 0) {
return null;
}
final long startNanos = System.nanoTime();
final int fileIndex = purge(minIndex, maxIndex, manager);
if (fileIndex < 0) {
return null;
}
if (LOGGER.isTraceEnabled()) {
final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
}
final StringBuilder buf = new StringBuilder(255);
manager.getPatternProcessor().formatFileName(subst, buf, fileIndex);
final String currentFileName = manager.getFileName();
String renameTo = buf.toString();
final String compressedName = renameTo;
Action compressAction = null;
for (FileExtensions ext : FileExtensions.values()) { // LOG4J2-1077 support other compression formats
if (ext.isExtensionFor(renameTo)) {
renameTo = renameTo.substring(0, renameTo.length() - ext.length()); // LOG4J2-1135 omit extension!
compressAction = ext.createCompressAction(renameTo, compressedName, true, compressionLevel);
break;
}
}
final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), false);
final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
}
private Action merge(final Action compressAction, final List custom, final boolean stopOnError) {
if (custom.isEmpty()) {
return compressAction;
}
if (compressAction == null) {
return new CompositeAction(custom, stopOnError);
}
final List all = new ArrayList<>();
all.add(compressAction);
all.addAll(custom);
return new CompositeAction(all, stopOnError);
}
@Override
public String toString() {
return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
}
}