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.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.List;
024import java.util.Objects;
025import java.util.concurrent.TimeUnit;
026import java.util.zip.Deflater;
027
028import org.apache.logging.log4j.Logger;
029import org.apache.logging.log4j.core.appender.rolling.action.Action;
030import org.apache.logging.log4j.core.appender.rolling.action.CommonsCompressAction;
031import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
032import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
033import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
034import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
035import org.apache.logging.log4j.core.config.Configuration;
036import org.apache.logging.log4j.core.config.plugins.Plugin;
037import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
038import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
039import org.apache.logging.log4j.core.config.plugins.PluginElement;
040import org.apache.logging.log4j.core.config.plugins.PluginFactory;
041import org.apache.logging.log4j.core.lookup.StrSubstitutor;
042import org.apache.logging.log4j.core.util.Integers;
043import org.apache.logging.log4j.status.StatusLogger;
044
045/**
046 * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
047 *
048 * <p>
049 * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
050 * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
051 * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
052 * </p>
053 * <p>
054 * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
055 * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
056 * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
057 * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
058 * specifying a large maximum value may entirely avoid renaming files.
059 * </p>
060 * <p>
061 * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
062 * </p>
063 * <p>
064 * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b>
065 * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of
066 * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file
067 * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file
068 * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file
069 * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file
070 * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
071 * <code>foo.log</code> will be created.
072 * </p>
073 * <p>
074 * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
075 * are discouraged.
076 * </p>
077 */
078@Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
079public class DefaultRolloverStrategy implements RolloverStrategy {
080
081    /**
082     * Enumerates over supported file extensions.
083     * <p>
084     * Package-protected for unit tests.
085     */
086    static enum FileExtensions {
087        ZIP(".zip") {
088            @Override
089            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
090                    final int compressionLevel) {
091                return new ZipCompressAction(source(renameTo), target(compressedName), deleteSource, compressionLevel);
092            }
093        },
094        GZ(".gz") {
095            @Override
096            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
097                    final int compressionLevel) {
098                return new GzCompressAction(source(renameTo), target(compressedName), deleteSource);
099            }
100        },
101        BZIP2(".bz2") {
102            @Override
103            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
104                    final int compressionLevel) {
105                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
106                return new CommonsCompressAction("bzip2", source(renameTo), target(compressedName), deleteSource);
107            }
108        },
109        DEFLATE(".deflate") {
110            @Override
111            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
112                    final int compressionLevel) {
113                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
114                return new CommonsCompressAction("deflate", source(renameTo), target(compressedName), deleteSource);
115            }
116        },
117        PACK200(".pack200") {
118            @Override
119            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
120                    final int compressionLevel) {
121                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
122                return new CommonsCompressAction("pack200", source(renameTo), target(compressedName), deleteSource);
123            }
124        },
125        XZ(".xz") {
126            @Override
127            Action createCompressAction(final String renameTo, final String compressedName, final boolean deleteSource,
128                    final int compressionLevel) {
129                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
130                return new CommonsCompressAction("xz", source(renameTo), target(compressedName), deleteSource);
131            }
132        };
133
134        static FileExtensions lookup(String fileExtension) {
135            for (FileExtensions ext : values()) {
136                if (ext.isExtensionFor(fileExtension)) {
137                    return ext;
138                }
139            }
140            return null;
141        }
142
143        private final String extension;
144
145        private FileExtensions(final String extension) {
146            Objects.requireNonNull(extension, "extension");
147            this.extension = extension;
148        }
149
150        abstract Action createCompressAction(String renameTo, String compressedName, boolean deleteSource,
151                int compressionLevel);
152
153        String getExtension() {
154            return extension;
155        }
156
157        boolean isExtensionFor(final String s) {
158            return s.endsWith(this.extension);
159        }
160
161        int length() {
162            return extension.length();
163        }
164
165        File source(String fileName) {
166            return new File(fileName);
167        }
168
169        File target(String fileName) {
170            return new File(fileName);
171        }
172    };
173
174    /**
175     * Allow subclasses access to the status logger without creating another instance.
176     */
177    protected static final Logger LOGGER = StatusLogger.getLogger();
178
179    private static final int MIN_WINDOW_SIZE = 1;
180    private static final int DEFAULT_WINDOW_SIZE = 7;
181
182    /**
183     * Create the DefaultRolloverStrategy.
184     * 
185     * @param max The maximum number of files to keep.
186     * @param min The minimum number of files to keep.
187     * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
188     *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
189     * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
190     * @param customActions custom actions to perform asynchronously after rollover
191     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
192     * @param config The Configuration.
193     * @return A DefaultRolloverStrategy.
194     */
195    @PluginFactory
196    public static DefaultRolloverStrategy createStrategy(
197            // @formatter:off
198            @PluginAttribute("max") final String max,
199            @PluginAttribute("min") final String min,
200            @PluginAttribute("fileIndex") final String fileIndex,
201            @PluginAttribute("compressionLevel") final String compressionLevelStr,
202            @PluginElement("Actions") final Action[] customActions,
203            @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
204                    final boolean stopCustomActionsOnError,
205            @PluginConfiguration final Configuration config) {
206            // @formatter:on
207        final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
208        int minIndex = MIN_WINDOW_SIZE;
209        if (min != null) {
210            minIndex = Integer.parseInt(min);
211            if (minIndex < 1) {
212                LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
213                minIndex = MIN_WINDOW_SIZE;
214            }
215        }
216        int maxIndex = DEFAULT_WINDOW_SIZE;
217        if (max != null) {
218            maxIndex = Integer.parseInt(max);
219            if (maxIndex < minIndex) {
220                maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
221                LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
222            }
223        }
224        final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
225        return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
226                customActions, stopCustomActionsOnError);
227    }
228
229    /**
230     * Index for oldest retained log file.
231     */
232    private final int maxIndex;
233
234    /**
235     * Index for most recent log file.
236     */
237    private final int minIndex;
238    private final boolean useMax;
239    private final StrSubstitutor strSubstitutor;
240    private final int compressionLevel;
241    private final List<Action> customActions;
242    private final boolean stopCustomActionsOnError;
243
244    /**
245     * Constructs a new instance.
246     * 
247     * @param minIndex The minimum index.
248     * @param maxIndex The maximum index.
249     * @param customActions custom actions to perform asynchronously after rollover
250     * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
251     */
252    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
253            final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
254            final boolean stopCustomActionsOnError) {
255        this.minIndex = minIndex;
256        this.maxIndex = maxIndex;
257        this.useMax = useMax;
258        this.compressionLevel = compressionLevel;
259        this.strSubstitutor = strSubstitutor;
260        this.stopCustomActionsOnError = stopCustomActionsOnError;
261        this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
262    }
263
264    public int getCompressionLevel() {
265        return this.compressionLevel;
266    }
267
268    public List<Action> getCustomActions() {
269        return customActions;
270    }
271
272    public int getMaxIndex() {
273        return this.maxIndex;
274    }
275
276    public int getMinIndex() {
277        return this.minIndex;
278    }
279
280    public StrSubstitutor getStrSubstitutor() {
281        return strSubstitutor;
282    }
283
284    public boolean isStopCustomActionsOnError() {
285        return stopCustomActionsOnError;
286    }
287
288    public boolean isUseMax() {
289        return useMax;
290    }
291
292    private Action merge(final Action compressAction, final List<Action> custom, final boolean stopOnError) {
293        if (custom.isEmpty()) {
294            return compressAction;
295        }
296        if (compressAction == null) {
297            return new CompositeAction(custom, stopOnError);
298        }
299        final List<Action> all = new ArrayList<>();
300        all.add(compressAction);
301        all.addAll(custom);
302        return new CompositeAction(all, stopOnError);
303    }
304
305    private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
306        return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
307    }
308
309    /**
310     * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index, the
311     * newest the highest.
312     *
313     * @param lowIndex low index
314     * @param highIndex high index. Log file associated with high index will be deleted if needed.
315     * @param manager The RollingFileManager
316     * @return true if purge was successful and rollover should be attempted.
317     */
318    private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
319        final List<FileRenameAction> renames = new ArrayList<>();
320        final StringBuilder buf = new StringBuilder();
321
322        // LOG4J2-531: directory scan & rollover must use same format
323        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, highIndex);
324        String highFilename = strSubstitutor.replace(buf);
325        final int suffixLength = suffixLength(highFilename);
326        int curMaxIndex = 0;
327
328        for (int i = highIndex; i >= lowIndex; i--) {
329            File toRename = new File(highFilename);
330            if (i == highIndex && toRename.exists()) {
331                curMaxIndex = highIndex;
332            } else if (curMaxIndex == 0 && toRename.exists()) {
333                curMaxIndex = i + 1;
334                break;
335            }
336
337            boolean isBase = false;
338
339            if (suffixLength > 0) {
340                final File toRenameBase = new File(highFilename.substring(0, highFilename.length() - suffixLength));
341
342                if (toRename.exists()) {
343                    if (toRenameBase.exists()) {
344                        LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
345                                toRenameBase, toRename);
346                        toRenameBase.delete();
347                    }
348                } else {
349                    toRename = toRenameBase;
350                    isBase = true;
351                }
352            }
353
354            if (toRename.exists()) {
355                //
356                // if at lower index and then all slots full
357                // attempt to delete last file
358                // if that fails then abandon purge
359                if (i == lowIndex) {
360                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.",
361                            toRename, i);
362                    if (!toRename.delete()) {
363                        return -1;
364                    }
365
366                    break;
367                }
368
369                //
370                // if intermediate index
371                // add a rename action to the list
372                buf.setLength(0);
373                // LOG4J2-531: directory scan & rollover must use same format
374                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
375
376                final String lowFilename = strSubstitutor.replace(buf);
377                String renameTo = lowFilename;
378
379                if (isBase) {
380                    renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
381                }
382
383                renames.add(new FileRenameAction(toRename, new File(renameTo), true));
384                highFilename = lowFilename;
385            } else {
386                buf.setLength(0);
387                // LOG4J2-531: directory scan & rollover must use same format
388                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
389
390                highFilename = strSubstitutor.replace(buf);
391            }
392        }
393        if (curMaxIndex == 0) {
394            curMaxIndex = lowIndex;
395        }
396
397        //
398        // work renames backwards
399        //
400        for (int i = renames.size() - 1; i >= 0; i--) {
401            final Action action = renames.get(i);
402            try {
403                LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
404                        i, renames.size(), action);
405                if (!action.execute()) {
406                    return -1;
407                }
408            } catch (final Exception ex) {
409                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
410                return -1;
411            }
412        }
413        return curMaxIndex;
414    }
415
416    /**
417     * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
418     * oldest will have the highest.
419     *
420     * @param lowIndex low index
421     * @param highIndex high index. Log file associated with high index will be deleted if needed.
422     * @param manager The RollingFileManager
423     * @return true if purge was successful and rollover should be attempted.
424     */
425    private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
426        final List<FileRenameAction> renames = new ArrayList<>();
427        final StringBuilder buf = new StringBuilder();
428
429        // LOG4J2-531: directory scan & rollover must use same format
430        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, lowIndex);
431
432        String lowFilename = strSubstitutor.replace(buf);
433        final int suffixLength = suffixLength(lowFilename);
434
435        for (int i = lowIndex; i <= highIndex; i++) {
436            File toRename = new File(lowFilename);
437            boolean isBase = false;
438
439            if (suffixLength > 0) {
440                final File toRenameBase = new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
441
442                if (toRename.exists()) {
443                    if (toRenameBase.exists()) {
444                        LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
445                                toRenameBase, toRename);
446                        toRenameBase.delete();
447                    }
448                } else {
449                    toRename = toRenameBase;
450                    isBase = true;
451                }
452            }
453
454            if (toRename.exists()) {
455                //
456                // if at upper index then
457                // attempt to delete last file
458                // if that fails then abandon purge
459                if (i == highIndex) {
460                    LOGGER.debug(
461                            "DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
462                            toRename, i);
463                    if (!toRename.delete()) {
464                        return -1;
465                    }
466
467                    break;
468                }
469
470                //
471                // if intermediate index
472                // add a rename action to the list
473                buf.setLength(0);
474                // LOG4J2-531: directory scan & rollover must use same format
475                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i + 1);
476
477                final String highFilename = strSubstitutor.replace(buf);
478                String renameTo = highFilename;
479
480                if (isBase) {
481                    renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
482                }
483
484                renames.add(new FileRenameAction(toRename, new File(renameTo), true));
485                lowFilename = highFilename;
486            } else {
487                break;
488            }
489        }
490
491        //
492        // work renames backwards
493        //
494        for (int i = renames.size() - 1; i >= 0; i--) {
495            final Action action = renames.get(i);
496            try {
497                LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
498                        i, renames.size(), action);
499                if (!action.execute()) {
500                    return -1;
501                }
502            } catch (final Exception ex) {
503                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
504                return -1;
505            }
506        }
507
508        return lowIndex;
509    }
510
511    /**
512     * Perform the rollover.
513     * 
514     * @param manager The RollingFileManager name for current active log file.
515     * @return A RolloverDescription.
516     * @throws SecurityException if an error occurs.
517     */
518    @Override
519    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
520        if (maxIndex < 0) {
521            return null;
522        }
523        final long startNanos = System.nanoTime();
524        final int fileIndex = purge(minIndex, maxIndex, manager);
525        if (fileIndex < 0) {
526            return null;
527        }
528        if (LOGGER.isTraceEnabled()) {
529            final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
530            LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
531        }
532        final StringBuilder buf = new StringBuilder(255);
533        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
534        final String currentFileName = manager.getFileName();
535
536        String renameTo = buf.toString();
537        final String compressedName = renameTo;
538        Action compressAction = null;
539
540        for (FileExtensions ext : FileExtensions.values()) { // LOG4J2-1077 support other compression formats
541            if (ext.isExtensionFor(renameTo)) {
542                renameTo = renameTo.substring(0, renameTo.length() - ext.length()); // LOG4J2-1135 omit extension!
543                compressAction = ext.createCompressAction(renameTo, compressedName, true, compressionLevel);
544                break;
545            }
546        }
547
548        final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), false);
549
550        final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
551        return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
552    }
553
554    private int suffixLength(final String lowFilename) {
555        for (FileExtensions extension : FileExtensions.values()) {
556            if (extension.isExtensionFor(lowFilename)) {
557                return extension.length();
558            }
559        }
560        return 0;
561    }
562
563    @Override
564    public String toString() {
565        return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
566    }
567
568}