blob: 18de6d19010fe677985c71d6563d5cf6699f8b32 [file] [log] [blame]
/*
* 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 org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.appender.rolling.action.Action;
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.PathCondition;
import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.core.util.Integers;
import org.apache.logging.log4j.plugins.Plugin;
import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.plugins.PluginElement;
import org.apache.logging.log4j.plugins.PluginFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
/**
* When rolling over, <code>DirectWriteRolloverStrategy</code> writes directly to the file as resolved by the file
* pattern. Files will be renamed files according to an algorithm as described below.
*
* <p>
* The DirectWriteRolloverStrategy uses similar logic as DefaultRolloverStrategy to determine the file name based
* on the file pattern, however the DirectWriteRolloverStrategy writes directly to a file and does not rename it
* during rollover, except if it is compressed, in which case it will add the appropriate file extension.
* </p>
*
* @since 2.8
*/
@Plugin(name = "DirectWriteRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
public class DirectWriteRolloverStrategy extends AbstractRolloverStrategy implements DirectFileRolloverStrategy {
private static final int DEFAULT_MAX_FILES = 7;
/**
* Builds DirectWriteRolloverStrategy instances.
*/
public static class Builder implements org.apache.logging.log4j.plugins.util.Builder<DirectWriteRolloverStrategy> {
@PluginBuilderAttribute("maxFiles")
private String maxFiles;
@PluginBuilderAttribute("compressionLevel")
private String compressionLevelStr;
@PluginElement("Actions")
private Action[] customActions;
@PluginBuilderAttribute(value = "stopCustomActionsOnError")
private boolean stopCustomActionsOnError = true;
@PluginBuilderAttribute(value = "tempCompressedFilePattern")
private String tempCompressedFilePattern;
@PluginConfiguration
private Configuration config;
@Override
public DirectWriteRolloverStrategy build() {
int maxIndex = Integer.MAX_VALUE;
if (maxFiles != null) {
maxIndex = Integer.parseInt(maxFiles);
if (maxIndex < 0) {
maxIndex = Integer.MAX_VALUE;
} else if (maxIndex < 2) {
LOGGER.error("Maximum files too small. Limited to " + DEFAULT_MAX_FILES);
maxIndex = DEFAULT_MAX_FILES;
}
}
final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
return new DirectWriteRolloverStrategy(maxIndex, compressionLevel, config.getStrSubstitutor(),
customActions, stopCustomActionsOnError, tempCompressedFilePattern);
}
public String getMaxFiles() {
return maxFiles;
}
/**
* Defines the maximum number of files to keep.
*
* @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
* @return This builder for chaining convenience
*/
public Builder setMaxFiles(final String maxFiles) {
this.maxFiles = maxFiles;
return this;
}
public String getCompressionLevelStr() {
return compressionLevelStr;
}
/**
* Defines compression level.
*
* @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
* @return This builder for chaining convenience
*/
public Builder setCompressionLevelStr(final String compressionLevelStr) {
this.compressionLevelStr = compressionLevelStr;
return this;
}
public Action[] getCustomActions() {
return customActions;
}
/**
* Defines custom actions.
*
* @param customActions custom actions to perform asynchronously after rollover
* @return This builder for chaining convenience
*/
public Builder setCustomActions(final Action... customActions) {
this.customActions = customActions;
return this;
}
public boolean isStopCustomActionsOnError() {
return stopCustomActionsOnError;
}
/**
* Defines whether to stop executing asynchronous actions if an error occurs.
*
* @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
* @return This builder for chaining convenience
*/
public Builder setStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
this.stopCustomActionsOnError = stopCustomActionsOnError;
return this;
}
public String getTempCompressedFilePattern() {
return tempCompressedFilePattern;
}
/**
* Defines temporary compression file pattern.
*
* @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
* @return This builder for chaining convenience
*/
public Builder setTempCompressedFilePattern(final String tempCompressedFilePattern) {
this.tempCompressedFilePattern = tempCompressedFilePattern;
return this;
}
public Configuration getConfig() {
return config;
}
/**
* Defines configuration.
*
* @param config The Configuration.
* @return This builder for chaining convenience
*/
public Builder setConfig(final Configuration config) {
this.config = config;
return this;
}
}
@PluginFactory
public static Builder newBuilder() {
return new Builder();
}
/**
* Index for most recent log file.
*/
private final int maxFiles;
private final int compressionLevel;
private final List<Action> customActions;
private final boolean stopCustomActionsOnError;
private volatile String currentFileName;
private int nextIndex = -1;
private final PatternProcessor tempCompressedFilePattern;
/**
* Constructs a new instance.
*
* @param maxFiles The maximum number of files that match the date portion of the pattern to keep.
* @param customActions custom actions to perform asynchronously after rollover
* @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
* @param tempCompressedFilePatternString File pattern of the working file
* used during compression, if null no temporary file are used
*/
protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel,
final StrSubstitutor strSubstitutor, final Action[] customActions,
final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
super(strSubstitutor);
this.maxFiles = maxFiles;
this.compressionLevel = compressionLevel;
this.stopCustomActionsOnError = stopCustomActionsOnError;
this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
this.tempCompressedFilePattern =
tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
}
public int getCompressionLevel() {
return this.compressionLevel;
}
public List<Action> getCustomActions() {
return customActions;
}
public int getMaxFiles() {
return this.maxFiles;
}
public boolean isStopCustomActionsOnError() {
return stopCustomActionsOnError;
}
public PatternProcessor getTempCompressedFilePattern() {
return tempCompressedFilePattern;
}
private int purge(final RollingFileManager manager) {
final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
LOGGER.debug("Found {} eligible files, max is {}", eligibleFiles.size(), maxFiles);
while (eligibleFiles.size() >= maxFiles) {
try {
final Integer key = eligibleFiles.firstKey();
Files.delete(eligibleFiles.get(key));
eligibleFiles.remove(key);
} catch (final IOException ioe) {
LOGGER.error("Unable to delete {}", eligibleFiles.firstKey(), ioe);
break;
}
}
return eligibleFiles.size() > 0 ? eligibleFiles.lastKey() : 1;
}
@Override
public String getCurrentFileName(final RollingFileManager manager) {
if (currentFileName == null) {
final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
final int fileIndex = eligibleFiles.size() > 0 ? (nextIndex > 0 ? nextIndex : eligibleFiles.size()) : 1;
final StringBuilder buf = new StringBuilder(255);
manager.getPatternProcessor().formatFileName(strSubstitutor, buf, true, fileIndex);
final int suffixLength = suffixLength(buf.toString());
final String name = suffixLength > 0 ? buf.substring(0, buf.length() - suffixLength) : buf.toString();
currentFileName = name;
}
return currentFileName;
}
@Override
public void clearCurrentFileName() {
currentFileName = null;
}
/**
* Performs 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 {
LOGGER.debug("Rolling " + currentFileName);
if (maxFiles < 0) {
return null;
}
final long startNanos = System.nanoTime();
final int fileIndex = purge(manager);
if (LOGGER.isTraceEnabled()) {
final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
LOGGER.trace("DirectWriteRolloverStrategy.purge() took {} milliseconds", durationMillis);
}
Action compressAction = null;
final String sourceName = getCurrentFileName(manager);
String compressedName = sourceName;
currentFileName = null;
nextIndex = fileIndex + 1;
final FileExtension fileExtension = manager.getFileExtension();
if (fileExtension != null) {
compressedName += fileExtension.getExtension();
if (tempCompressedFilePattern != null) {
final StringBuilder buf = new StringBuilder();
tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
final String tmpCompressedName = buf.toString();
final File tmpCompressedNameFile = new File(tmpCompressedName);
final File parentFile = tmpCompressedNameFile.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
compressAction = new CompositeAction(
Arrays.asList(fileExtension.createCompressAction(sourceName, tmpCompressedName,
true, compressionLevel),
new FileRenameAction(tmpCompressedNameFile,
new File(compressedName), true)),
true);
} else {
compressAction = fileExtension.createCompressAction(sourceName, compressedName,
true, compressionLevel);
}
}
if (compressAction != null && manager.isAttributeViewEnabled()) {
// Propagate posix attribute view to compressed file
// @formatter:off
final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
.setBasePath(compressedName)
.setFollowLinks(false)
.setMaxDepth(1)
.setPathConditions(PathCondition.EMPTY_ARRAY)
.setSubst(getStrSubstitutor())
.setFilePermissions(manager.getFilePermissions())
.setFileOwner(manager.getFileOwner())
.setFileGroup(manager.getFileGroup())
.build();
// @formatter:on
compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
}
final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
return new RolloverDescriptionImpl(sourceName, false, null, asyncAction);
}
@Override
public String toString() {
return "DirectWriteRolloverStrategy(maxFiles=" + maxFiles + ')';
}
}