001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.appender.rolling;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.List;
026import java.util.Map;
027import java.util.SortedMap;
028import java.util.concurrent.TimeUnit;
029import java.util.zip.Deflater;
030
031import org.apache.logging.log4j.core.Core;
032import org.apache.logging.log4j.core.appender.rolling.action.Action;
033import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
034import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
035import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
036import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
037import org.apache.logging.log4j.core.config.Configuration;
038import org.apache.logging.log4j.core.config.plugins.Plugin;
039import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
040import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
041import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
042import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
043import org.apache.logging.log4j.core.config.plugins.PluginElement;
044import org.apache.logging.log4j.core.config.plugins.PluginFactory;
045import org.apache.logging.log4j.core.lookup.StrSubstitutor;
046import org.apache.logging.log4j.core.util.Integers;
047
048/**
049 * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
050 *
051 * <p>
052 * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
053 * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
054 * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
055 * </p>
056 * <p>
057 * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
058 * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
059 * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
060 * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
061 * specifying a large maximum value may entirely avoid renaming files.
062 * </p>
063 * <p>
064 * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
065 * </p>
066 * <p>
067 * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b>
068 * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of
069 * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file
070 * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file
071 * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file
072 * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file
073 * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
074 * <code>foo.log</code> will be created.
075 * </p>
076 * <p>
077 * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
078 * are discouraged.
079 * </p>
080 */
081@Plugin(name = "DefaultRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
082public class DefaultRolloverStrategy extends AbstractRolloverStrategy {
083
084    private static final int MIN_WINDOW_SIZE = 1;
085    private static final int DEFAULT_WINDOW_SIZE = 7;
086
087    /**
088     * Builds DefaultRolloverStrategy instances.
089     */
090    public static class Builder implements org.apache.logging.log4j.core.util.Builder<DefaultRolloverStrategy> {
091        @PluginBuilderAttribute("max")
092        private String max;
093        
094        @PluginBuilderAttribute("min")
095        private String min;
096        
097        @PluginBuilderAttribute("fileIndex")
098        private String fileIndex;
099
100        @PluginBuilderAttribute("compressionLevel")
101        private String compressionLevelStr;
102
103        @PluginElement("Actions")
104        private Action[] customActions;
105
106        @PluginBuilderAttribute(value = "stopCustomActionsOnError")
107        private boolean stopCustomActionsOnError = true;
108
109        @PluginBuilderAttribute(value = "tempCompressedFilePattern")
110        private String tempCompressedFilePattern;
111
112        @PluginConfiguration
113        private Configuration config;
114
115        @Override
116        public DefaultRolloverStrategy build() {
117            int minIndex;
118            int maxIndex;
119            boolean useMax;
120
121            if (fileIndex != null && fileIndex.equalsIgnoreCase("nomax")) {
122                minIndex = Integer.MIN_VALUE;
123                maxIndex = Integer.MAX_VALUE;
124                useMax = false;
125            } else {
126                useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
127                minIndex = MIN_WINDOW_SIZE;
128                if (min != null) {
129                    minIndex = Integer.parseInt(min);
130                    if (minIndex < 1) {
131                        LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
132                        minIndex = MIN_WINDOW_SIZE;
133                    }
134                }
135                maxIndex = DEFAULT_WINDOW_SIZE;
136                if (max != null) {
137                    maxIndex = Integer.parseInt(max);
138                    if (maxIndex < minIndex) {
139                        maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
140                        LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
141                    }
142                }
143            }
144            final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
145            return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
146                    customActions, stopCustomActionsOnError, tempCompressedFilePattern);
147        }
148
149        public String getMax() {
150            return max;
151        }
152
153        /**
154         * Defines the maximum number of files to keep.
155         *
156         * @param max The maximum number of files to keep.
157         * @return This builder for chaining convenience
158         */
159        public Builder withMax(final String max) {
160            this.max = max;
161            return this;
162        }
163
164        public String getMin() {
165            return min;
166        }
167
168        /**
169         * Defines the minimum number of files to keep.
170         *
171         * @param min The minimum number of files to keep.
172         * @return This builder for chaining convenience
173         */
174        public Builder withMin(final String min) {
175            this.min = min;
176            return this;
177        }
178
179        public String getFileIndex() {
180            return fileIndex;
181        }
182
183        /**
184         * Defines the file index for rolling strategy.
185         *
186         * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
187         *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
188         * @return This builder for chaining convenience
189         */
190        public Builder withFileIndex(final String fileIndex) {
191            this.fileIndex = fileIndex;
192            return this;
193        }
194
195        public String getCompressionLevelStr() {
196            return compressionLevelStr;
197        }
198
199        /**
200         * Defines compression level.
201         *
202         * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
203         * @return This builder for chaining convenience
204         */
205        public Builder withCompressionLevelStr(final String compressionLevelStr) {
206            this.compressionLevelStr = compressionLevelStr;
207            return this;
208        }
209
210        public Action[] getCustomActions() {
211            return customActions;
212        }
213
214        /**
215         * Defines custom actions.
216         *
217         * @param customActions custom actions to perform asynchronously after rollover
218         * @return This builder for chaining convenience
219         */
220        public Builder withCustomActions(final Action[] customActions) {
221            this.customActions = customActions;
222            return this;
223        }
224
225        public boolean isStopCustomActionsOnError() {
226            return stopCustomActionsOnError;
227        }
228
229        /**
230         * Defines whether to stop executing asynchronous actions if an error occurs.
231         *
232         * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
233         * @return This builder for chaining convenience
234         */
235        public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
236            this.stopCustomActionsOnError = stopCustomActionsOnError;
237            return this;
238        }
239
240        public String getTempCompressedFilePattern() {
241            return tempCompressedFilePattern;
242        }
243
244        /**
245         * Defines temporary compression file pattern.
246         *
247         * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
248         * @return This builder for chaining convenience
249         */
250        public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) {
251            this.tempCompressedFilePattern = tempCompressedFilePattern;
252            return this;
253        }
254
255        public Configuration getConfig() {
256            return config;
257        }
258
259        /**
260         * Defines configuration.
261         * 
262         * @param config The Configuration.
263         * @return This builder for chaining convenience
264         */
265        public Builder withConfig(final Configuration config) {
266            this.config = config;
267            return this;
268        }
269    }
270
271    @PluginBuilderFactory
272    public static Builder newBuilder() {
273        return new Builder();
274    }
275
276    /**
277     * Creates the DefaultRolloverStrategy.
278     *
279     * @param max The maximum number of files to keep.
280     * @param min The minimum number of files to keep.
281     * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
282     *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
283     * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
284     * @param customActions custom actions to perform asynchronously after rollover
285     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
286     * @param config The Configuration.
287     * @return A DefaultRolloverStrategy.
288     * @deprecated Since 2.9 Usage of Builder API is preferable
289     */
290    @PluginFactory
291    @Deprecated
292    public static DefaultRolloverStrategy createStrategy(
293            // @formatter:off
294            @PluginAttribute("max") final String max,
295            @PluginAttribute("min") final String min,
296            @PluginAttribute("fileIndex") final String fileIndex,
297            @PluginAttribute("compressionLevel") final String compressionLevelStr,
298            @PluginElement("Actions") final Action[] customActions,
299            @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
300                    final boolean stopCustomActionsOnError,
301            @PluginConfiguration final Configuration config) {
302        return DefaultRolloverStrategy.newBuilder()
303                    .withMin(min)
304                    .withMax(max)
305                    .withFileIndex(fileIndex)
306                    .withCompressionLevelStr(compressionLevelStr)
307                    .withCustomActions(customActions)
308                    .withStopCustomActionsOnError(stopCustomActionsOnError)
309                    .withConfig(config)
310                .build();
311            // @formatter:on
312    }
313
314    /**
315     * Index for oldest retained log file.
316     */
317    private final int maxIndex;
318
319    /**
320     * Index for most recent log file.
321     */
322    private final int minIndex;
323    private final boolean useMax;
324    private final int compressionLevel;
325    private final List<Action> customActions;
326    private final boolean stopCustomActionsOnError;
327    private final PatternProcessor tempCompressedFilePattern;
328
329    /**
330     * Constructs a new instance.
331     *
332     * @param minIndex The minimum index.
333     * @param maxIndex The maximum index.
334     * @param customActions custom actions to perform asynchronously after rollover
335     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
336     * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
337     */
338    @Deprecated
339    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
340            final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
341            final boolean stopCustomActionsOnError) {
342        this(minIndex, maxIndex, useMax, compressionLevel,
343                       strSubstitutor, customActions, stopCustomActionsOnError, null);
344    }
345
346    /**
347     * Constructs a new instance.
348     *
349     * @param minIndex The minimum index.
350     * @param maxIndex The maximum index.
351     * @param customActions custom actions to perform asynchronously after rollover
352     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
353     * @param tempCompressedFilePatternString File pattern of the working file
354     *                                     used during compression, if null no temporary file are used
355     */
356    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
357            final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
358            final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
359        super(strSubstitutor);
360        this.minIndex = minIndex;
361        this.maxIndex = maxIndex;
362        this.useMax = useMax;
363        this.compressionLevel = compressionLevel;
364        this.stopCustomActionsOnError = stopCustomActionsOnError;
365        this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
366        this.tempCompressedFilePattern =
367                tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
368    }
369
370    public int getCompressionLevel() {
371        return this.compressionLevel;
372    }
373
374    public List<Action> getCustomActions() {
375        return customActions;
376    }
377
378    public int getMaxIndex() {
379        return this.maxIndex;
380    }
381
382    public int getMinIndex() {
383        return this.minIndex;
384    }
385
386    public boolean isStopCustomActionsOnError() {
387        return stopCustomActionsOnError;
388    }
389
390    public boolean isUseMax() {
391        return useMax;
392    }
393
394    public PatternProcessor getTempCompressedFilePattern() {
395        return tempCompressedFilePattern;
396    }
397
398    private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
399        return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
400    }
401
402    /**
403     * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
404     * newest the highest.
405     *
406     * @param lowIndex low index. Log file associated with low index will be deleted if needed.
407     * @param highIndex high index.
408     * @param manager The RollingFileManager
409     * @return true if purge was successful and rollover should be attempted.
410     */
411    private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
412        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
413        final int maxFiles = highIndex - lowIndex + 1;
414
415        boolean renameFiles = false;
416        while (eligibleFiles.size() >= maxFiles) {
417            try {
418                LOGGER.debug("Eligible files: {}", eligibleFiles);
419                final Integer key = eligibleFiles.firstKey();
420                LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
421                Files.delete(eligibleFiles.get(key));
422                eligibleFiles.remove(key);
423                renameFiles = true;
424            } catch (final IOException ioe) {
425                LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
426                break;
427            }
428        }
429        final StringBuilder buf = new StringBuilder();
430        if (renameFiles) {
431            for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
432                buf.setLength(0);
433                // LOG4J2-531: directory scan & rollover must use same format
434                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
435                final String currentName = entry.getValue().toFile().getName();
436                String renameTo = buf.toString();
437                final int suffixLength = suffixLength(renameTo);
438                if (suffixLength > 0 && suffixLength(currentName) == 0) {
439                   renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
440                }
441                final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
442                try {
443                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
444                    if (!action.execute()) {
445                        return -1;
446                    }
447                } catch (final Exception ex) {
448                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
449                    return -1;
450                }
451            }
452        }
453
454        return eligibleFiles.size() > 0 ?
455                (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
456    }
457
458    /**
459     * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the
460     * oldest will have the highest.
461     *
462     * @param lowIndex low index
463     * @param highIndex high index. Log file associated with high index will be deleted if needed.
464     * @param manager The RollingFileManager
465     * @return true if purge was successful and rollover should be attempted.
466     */
467    private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
468        // Retrieve the files in descending order, so the highest key will be first.
469        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false);
470        final int maxFiles = highIndex - lowIndex + 1;
471
472        while (eligibleFiles.size() >= maxFiles) {
473            try {
474                final Integer key = eligibleFiles.firstKey();
475                Files.delete(eligibleFiles.get(key));
476                eligibleFiles.remove(key);
477            } catch (final IOException ioe) {
478                LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
479                break;
480            }
481        }
482        final StringBuilder buf = new StringBuilder();
483        for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
484            buf.setLength(0);
485            // LOG4J2-531: directory scan & rollover must use same format
486            manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1);
487            final String currentName = entry.getValue().toFile().getName();
488            String renameTo = buf.toString();
489            final int suffixLength = suffixLength(renameTo);
490            if (suffixLength > 0 && suffixLength(currentName) == 0) {
491                renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
492            }
493            final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
494            try {
495                LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action);
496                if (!action.execute()) {
497                    return -1;
498                }
499            } catch (final Exception ex) {
500                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
501                return -1;
502            }
503        }
504
505        return lowIndex;
506    }
507
508    /**
509     * Performs the rollover.
510     *
511     * @param manager The RollingFileManager name for current active log file.
512     * @return A RolloverDescription.
513     * @throws SecurityException if an error occurs.
514     */
515    @Override
516    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
517        int fileIndex;
518        if (minIndex == Integer.MIN_VALUE) {
519            final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
520            fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
521        } else {
522            if (maxIndex < 0) {
523                return null;
524            }
525            final long startNanos = System.nanoTime();
526            fileIndex = purge(minIndex, maxIndex, manager);
527            if (fileIndex < 0) {
528                return null;
529            }
530            if (LOGGER.isTraceEnabled()) {
531                final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
532                LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
533            }
534        }
535        final StringBuilder buf = new StringBuilder(255);
536        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
537        final String currentFileName = manager.getFileName();
538
539        String renameTo = buf.toString();
540        final String compressedName = renameTo;
541        Action compressAction = null;
542
543        final FileExtension fileExtension = manager.getFileExtension();
544        if (fileExtension != null) {
545            final File renameToFile = new File(renameTo);
546            renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
547            if (tempCompressedFilePattern != null) {
548                buf.delete(0, buf.length());
549                tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
550                final String tmpCompressedName = buf.toString();
551                final File tmpCompressedNameFile = new File(tmpCompressedName);
552                final File parentFile = tmpCompressedNameFile.getParentFile();
553                if (parentFile != null) {
554                    parentFile.mkdirs();
555                }
556                compressAction = new CompositeAction(
557                        Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
558                                true, compressionLevel),
559                                new FileRenameAction(tmpCompressedNameFile,
560                                        renameToFile, true)),
561                        true);
562            } else {
563                compressAction = fileExtension.createCompressAction(renameTo, compressedName,
564                        true, compressionLevel);
565            }
566        }
567
568        if (currentFileName.equals(renameTo)) {
569            LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
570            return new RolloverDescriptionImpl(currentFileName, false, null, null);
571        }
572
573        if (compressAction != null && manager.isAttributeViewEnabled()) {
574            // Propagate posix attribute view to compressed file
575            // @formatter:off
576            final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
577                                                        .withBasePath(compressedName)
578                                                        .withFollowLinks(false)
579                                                        .withMaxDepth(1)
580                                                        .withPathConditions(new PathCondition[0])
581                                                        .withSubst(getStrSubstitutor())
582                                                        .withFilePermissions(manager.getFilePermissions())
583                                                        .withFileOwner(manager.getFileOwner())
584                                                        .withFileGroup(manager.getFileGroup())
585                                                        .build();
586            // @formatter:on
587            compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
588        }
589
590        final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
591                    manager.isRenameEmptyFiles());
592
593        final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
594        return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
595    }
596
597    @Override
598    public String toString() {
599        return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
600    }
601
602}