| /* |
| * 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 + ')'; |
| } |
| |
| } |