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            // The config object can be null when this object is built programmatically.
146            final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor();
147                        return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, nonNullStrSubstitutor,
148                    customActions, stopCustomActionsOnError, tempCompressedFilePattern);
149        }
150
151        public String getMax() {
152            return max;
153        }
154
155        /**
156         * Defines the maximum number of files to keep.
157         *
158         * @param max The maximum number of files to keep.
159         * @return This builder for chaining convenience
160         */
161        public Builder withMax(final String max) {
162            this.max = max;
163            return this;
164        }
165
166        public String getMin() {
167            return min;
168        }
169
170        /**
171         * Defines the minimum number of files to keep.
172         *
173         * @param min The minimum number of files to keep.
174         * @return This builder for chaining convenience
175         */
176        public Builder withMin(final String min) {
177            this.min = min;
178            return this;
179        }
180
181        public String getFileIndex() {
182            return fileIndex;
183        }
184
185        /**
186         * Defines the file index for rolling strategy.
187         *
188         * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
189         *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
190         * @return This builder for chaining convenience
191         */
192        public Builder withFileIndex(final String fileIndex) {
193            this.fileIndex = fileIndex;
194            return this;
195        }
196
197        public String getCompressionLevelStr() {
198            return compressionLevelStr;
199        }
200
201        /**
202         * Defines compression level.
203         *
204         * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
205         * @return This builder for chaining convenience
206         */
207        public Builder withCompressionLevelStr(final String compressionLevelStr) {
208            this.compressionLevelStr = compressionLevelStr;
209            return this;
210        }
211
212        public Action[] getCustomActions() {
213            return customActions;
214        }
215
216        /**
217         * Defines custom actions.
218         *
219         * @param customActions custom actions to perform asynchronously after rollover
220         * @return This builder for chaining convenience
221         */
222        public Builder withCustomActions(final Action[] customActions) {
223            this.customActions = customActions;
224            return this;
225        }
226
227        public boolean isStopCustomActionsOnError() {
228            return stopCustomActionsOnError;
229        }
230
231        /**
232         * Defines whether to stop executing asynchronous actions if an error occurs.
233         *
234         * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
235         * @return This builder for chaining convenience
236         */
237        public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
238            this.stopCustomActionsOnError = stopCustomActionsOnError;
239            return this;
240        }
241
242        public String getTempCompressedFilePattern() {
243            return tempCompressedFilePattern;
244        }
245
246        /**
247         * Defines temporary compression file pattern.
248         *
249         * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
250         * @return This builder for chaining convenience
251         */
252        public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) {
253            this.tempCompressedFilePattern = tempCompressedFilePattern;
254            return this;
255        }
256
257        public Configuration getConfig() {
258            return config;
259        }
260
261        /**
262         * Defines configuration.
263         *
264         * @param config The Configuration.
265         * @return This builder for chaining convenience
266         */
267        public Builder withConfig(final Configuration config) {
268            this.config = config;
269            return this;
270        }
271    }
272
273    @PluginBuilderFactory
274    public static Builder newBuilder() {
275        return new Builder();
276    }
277
278    /**
279     * Creates the DefaultRolloverStrategy.
280     *
281     * @param max The maximum number of files to keep.
282     * @param min The minimum number of files to keep.
283     * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
284     *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
285     * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
286     * @param customActions custom actions to perform asynchronously after rollover
287     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
288     * @param config The Configuration.
289     * @return A DefaultRolloverStrategy.
290     * @deprecated Since 2.9 Usage of Builder API is preferable
291     */
292    @PluginFactory
293    @Deprecated
294    public static DefaultRolloverStrategy createStrategy(
295            // @formatter:off
296            @PluginAttribute("max") final String max,
297            @PluginAttribute("min") final String min,
298            @PluginAttribute("fileIndex") final String fileIndex,
299            @PluginAttribute("compressionLevel") final String compressionLevelStr,
300            @PluginElement("Actions") final Action[] customActions,
301            @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
302                    final boolean stopCustomActionsOnError,
303            @PluginConfiguration final Configuration config) {
304        return DefaultRolloverStrategy.newBuilder()
305                    .withMin(min)
306                    .withMax(max)
307                    .withFileIndex(fileIndex)
308                    .withCompressionLevelStr(compressionLevelStr)
309                    .withCustomActions(customActions)
310                    .withStopCustomActionsOnError(stopCustomActionsOnError)
311                    .withConfig(config)
312                .build();
313            // @formatter:on
314    }
315
316    /**
317     * Index for oldest retained log file.
318     */
319    private final int maxIndex;
320
321    /**
322     * Index for most recent log file.
323     */
324    private final int minIndex;
325    private final boolean useMax;
326    private final int compressionLevel;
327    private final List<Action> customActions;
328    private final boolean stopCustomActionsOnError;
329    private final PatternProcessor tempCompressedFilePattern;
330
331    /**
332     * Constructs a new instance.
333     *
334     * @param minIndex The minimum index.
335     * @param maxIndex The maximum index.
336     * @param customActions custom actions to perform asynchronously after rollover
337     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
338     * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
339     */
340    @Deprecated
341    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
342            final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
343            final boolean stopCustomActionsOnError) {
344        this(minIndex, maxIndex, useMax, compressionLevel,
345                       strSubstitutor, customActions, stopCustomActionsOnError, null);
346    }
347
348    /**
349     * Constructs a new instance.
350     *
351     * @param minIndex The minimum index.
352     * @param maxIndex The maximum index.
353     * @param customActions custom actions to perform asynchronously after rollover
354     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
355     * @param tempCompressedFilePatternString File pattern of the working file
356     *                                     used during compression, if null no temporary file are used
357     */
358    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
359            final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
360            final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
361        super(strSubstitutor);
362        this.minIndex = minIndex;
363        this.maxIndex = maxIndex;
364        this.useMax = useMax;
365        this.compressionLevel = compressionLevel;
366        this.stopCustomActionsOnError = stopCustomActionsOnError;
367        this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
368        this.tempCompressedFilePattern =
369                tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
370    }
371
372    public int getCompressionLevel() {
373        return this.compressionLevel;
374    }
375
376    public List<Action> getCustomActions() {
377        return customActions;
378    }
379
380    public int getMaxIndex() {
381        return this.maxIndex;
382    }
383
384    public int getMinIndex() {
385        return this.minIndex;
386    }
387
388    public boolean isStopCustomActionsOnError() {
389        return stopCustomActionsOnError;
390    }
391
392    public boolean isUseMax() {
393        return useMax;
394    }
395
396    public PatternProcessor getTempCompressedFilePattern() {
397        return tempCompressedFilePattern;
398    }
399
400    private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
401        return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
402    }
403
404    /**
405     * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
406     * newest the highest.
407     *
408     * @param lowIndex low index. Log file associated with low index will be deleted if needed.
409     * @param highIndex high index.
410     * @param manager The RollingFileManager
411     * @return true if purge was successful and rollover should be attempted.
412     */
413    private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
414        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
415        final int maxFiles = highIndex - lowIndex + 1;
416
417        boolean renameFiles = !eligibleFiles.isEmpty() && eligibleFiles.lastKey() >= maxIndex;
418        while (eligibleFiles.size() >= maxFiles) {
419            try {
420                LOGGER.debug("Eligible files: {}", eligibleFiles);
421                final Integer key = eligibleFiles.firstKey();
422                LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
423                Files.delete(eligibleFiles.get(key));
424                eligibleFiles.remove(key);
425                renameFiles = true;
426            } catch (final IOException ioe) {
427                LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
428                break;
429            }
430        }
431        final StringBuilder buf = new StringBuilder();
432        if (renameFiles) {
433            for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
434                buf.setLength(0);
435                // LOG4J2-531: directory scan & rollover must use same format
436                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
437                final String currentName = entry.getValue().toFile().getName();
438                String renameTo = buf.toString();
439                final int suffixLength = suffixLength(renameTo);
440                if (suffixLength > 0 && suffixLength(currentName) == 0) {
441                   renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
442                }
443                final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
444                try {
445                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
446                    if (!action.execute()) {
447                        return -1;
448                    }
449                } catch (final Exception ex) {
450                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
451                    return -1;
452                }
453            }
454        }
455
456        return eligibleFiles.size() > 0 ?
457                (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
458    }
459
460    /**
461     * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the
462     * oldest will have the highest.
463     *
464     * @param lowIndex low index
465     * @param highIndex high index. Log file associated with high index will be deleted if needed.
466     * @param manager The RollingFileManager
467     * @return true if purge was successful and rollover should be attempted.
468     */
469    private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
470        // Retrieve the files in descending order, so the highest key will be first.
471        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false);
472        final int maxFiles = highIndex - lowIndex + 1;
473
474        while (eligibleFiles.size() >= maxFiles) {
475            try {
476                final Integer key = eligibleFiles.firstKey();
477                Files.delete(eligibleFiles.get(key));
478                eligibleFiles.remove(key);
479            } catch (final IOException ioe) {
480                LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
481                break;
482            }
483        }
484        final StringBuilder buf = new StringBuilder();
485        for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
486            buf.setLength(0);
487            // LOG4J2-531: directory scan & rollover must use same format
488            manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1);
489            final String currentName = entry.getValue().toFile().getName();
490            String renameTo = buf.toString();
491            final int suffixLength = suffixLength(renameTo);
492            if (suffixLength > 0 && suffixLength(currentName) == 0) {
493                renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
494            }
495            final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
496            try {
497                LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action);
498                if (!action.execute()) {
499                    return -1;
500                }
501            } catch (final Exception ex) {
502                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
503                return -1;
504            }
505        }
506
507        return lowIndex;
508    }
509
510    /**
511     * Performs the rollover.
512     *
513     * @param manager The RollingFileManager name for current active log file.
514     * @return A RolloverDescription.
515     * @throws SecurityException if an error occurs.
516     */
517    @Override
518    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
519        int fileIndex;
520                final StringBuilder buf = new StringBuilder(255);
521        if (minIndex == Integer.MIN_VALUE) {
522            final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
523            fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
524                        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
525        } else {
526            if (maxIndex < 0) {
527                return null;
528            }
529            final long startNanos = System.nanoTime();
530            fileIndex = purge(minIndex, maxIndex, manager);
531            if (fileIndex < 0) {
532                return null;
533            }
534                        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
535            if (LOGGER.isTraceEnabled()) {
536                final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
537                LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
538            }
539        }
540
541        final String currentFileName = manager.getFileName();
542
543        String renameTo = buf.toString();
544        final String compressedName = renameTo;
545        Action compressAction = null;
546
547        final FileExtension fileExtension = manager.getFileExtension();
548        if (fileExtension != null) {
549            final File renameToFile = new File(renameTo);
550            renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
551            if (tempCompressedFilePattern != null) {
552                buf.delete(0, buf.length());
553                tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
554                final String tmpCompressedName = buf.toString();
555                final File tmpCompressedNameFile = new File(tmpCompressedName);
556                final File parentFile = tmpCompressedNameFile.getParentFile();
557                if (parentFile != null) {
558                    parentFile.mkdirs();
559                }
560                compressAction = new CompositeAction(
561                        Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
562                                true, compressionLevel),
563                                new FileRenameAction(tmpCompressedNameFile,
564                                        renameToFile, true)),
565                        true);
566            } else {
567                compressAction = fileExtension.createCompressAction(renameTo, compressedName,
568                        true, compressionLevel);
569            }
570        }
571
572        if (currentFileName.equals(renameTo)) {
573            LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
574            return new RolloverDescriptionImpl(currentFileName, false, null, null);
575        }
576
577        if (compressAction != null && manager.isAttributeViewEnabled()) {
578            // Propagate posix attribute view to compressed file
579            // @formatter:off
580            final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
581                                                        .withBasePath(compressedName)
582                                                        .withFollowLinks(false)
583                                                        .withMaxDepth(1)
584                                                        .withPathConditions(new PathCondition[0])
585                                                        .withSubst(getStrSubstitutor())
586                                                        .withFilePermissions(manager.getFilePermissions())
587                                                        .withFileOwner(manager.getFileOwner())
588                                                        .withFileGroup(manager.getFileGroup())
589                                                        .build();
590            // @formatter:on
591            compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
592        }
593
594        final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
595                    manager.isRenameEmptyFiles());
596
597        final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
598        return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
599    }
600
601    @Override
602    public String toString() {
603        return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
604    }
605
606}