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