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     */
086    private enum FileExtensions {
087        ZIP(".zip") {
088            @Override
089            Action createCompressAction(final String renameTo, final String compressedName,
090                    final boolean deleteSource, final int compressionLevel) {
091                return new ZipCompressAction(new File(baseName(renameTo)), new File(compressedName), deleteSource,
092                        compressionLevel);
093            }
094        },
095        GZIP(".gz") {
096            @Override
097            Action createCompressAction(final String renameTo, final String compressedName,
098                    final boolean deleteSource, final int compressionLevel) {
099                return new GzCompressAction(new File(baseName(renameTo)), new File(compressedName), deleteSource);
100            }
101        },
102        BZIP2(".bz2") {
103            @Override
104            Action createCompressAction(final String renameTo, final String compressedName,
105                    final boolean deleteSource, final int compressionLevel) {
106                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
107                return new CommonsCompressAction("bzip2", new File(baseName(renameTo)), new File(compressedName),
108                        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", new File(baseName(renameTo)), new File(compressedName),
117                        deleteSource);
118            }            
119        },
120        PACK200(".pack200") {
121            @Override
122            Action createCompressAction(final String renameTo, final String compressedName,
123                    final boolean deleteSource, final int compressionLevel) {
124                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
125                return new CommonsCompressAction("pack200", new File(baseName(renameTo)), new File(compressedName),
126                        deleteSource);
127            }            
128        },
129        XY(".xy") {
130            @Override
131            Action createCompressAction(final String renameTo, final String compressedName,
132                    final boolean deleteSource, final int compressionLevel) {
133                // One of "gz", "bzip2", "xz", "pack200", or "deflate".
134                return new CommonsCompressAction("xy", new File(baseName(renameTo)), new File(compressedName),
135                        deleteSource);
136            }            
137        };
138
139        private final String extension;
140
141        private FileExtensions(final String extension) {
142            Objects.requireNonNull(extension, "extension");
143            this.extension = extension;
144        }
145
146        String getExtension() {
147            return extension;
148        }
149
150        boolean isExtensionFor(final String s) {
151            return s.endsWith(this.extension);
152        }
153
154        int length() {
155            return extension.length();
156        }
157
158        String baseName(final String name) {
159            return name.substring(0, name.length() - length());
160        }
161
162        abstract Action createCompressAction(String renameTo, String compressedName, boolean deleteSource,
163                int compressionLevel);
164    };
165
166    /**
167     * Allow subclasses access to the status logger without creating another instance.
168     */
169    protected static final Logger LOGGER = StatusLogger.getLogger();
170
171    private static final int MIN_WINDOW_SIZE = 1;
172    private static final int DEFAULT_WINDOW_SIZE = 7;
173
174    /**
175     * Create the DefaultRolloverStrategy.
176     * @param max The maximum number of files to keep.
177     * @param min The minimum number of files to keep.
178     * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a
179     * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
180     * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
181     * @param config The Configuration.
182     * @return A DefaultRolloverStrategy.
183     */
184    @PluginFactory
185    public static DefaultRolloverStrategy createStrategy(
186            @PluginAttribute("max") final String max,
187            @PluginAttribute("min") final String min,
188            @PluginAttribute("fileIndex") final String fileIndex,
189            @PluginAttribute("compressionLevel") final String compressionLevelStr,
190            @PluginConfiguration final Configuration config) {
191        final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
192        int minIndex = MIN_WINDOW_SIZE;
193        if (min != null) {
194            minIndex = Integer.parseInt(min);
195            if (minIndex < 1) {
196                LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
197                minIndex = MIN_WINDOW_SIZE;
198            }
199        }
200        int maxIndex = DEFAULT_WINDOW_SIZE;
201        if (max != null) {
202            maxIndex = Integer.parseInt(max);
203            if (maxIndex < minIndex) {
204                maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
205                LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
206            }
207        }
208        final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
209        return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor());
210    }
211
212    /**
213     * Index for oldest retained log file.
214     */
215    private final int maxIndex;
216
217    /**
218     * Index for most recent log file.
219     */
220    private final int minIndex;
221    private final boolean useMax;
222    private final StrSubstitutor subst;
223    private final int compressionLevel;
224
225    /**
226     * Constructs a new instance.
227     * @param minIndex The minimum index.
228     * @param maxIndex The maximum index.
229     */
230    protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
231            final int compressionLevel, final StrSubstitutor subst) {
232        this.minIndex = minIndex;
233        this.maxIndex = maxIndex;
234        this.useMax = useMax;
235        this.compressionLevel = compressionLevel;
236        this.subst = subst;
237    }
238
239    public int getCompressionLevel() {
240        return this.compressionLevel;
241    }
242
243    public int getMaxIndex() {
244        return this.maxIndex;
245    }
246
247    public int getMinIndex() {
248        return this.minIndex;
249    }
250
251    private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
252        return useMax ? purgeAscending(lowIndex, highIndex, manager) :
253            purgeDescending(lowIndex, highIndex, manager);
254    }
255
256    /**
257     * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index,
258     * the newest the highest.
259     *
260     * @param lowIndex  low index
261     * @param highIndex high index.  Log file associated with high index will be deleted if needed.
262     * @param manager The RollingFileManager
263     * @return true if purge was successful and rollover should be attempted.
264     */
265    private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
266        final List<FileRenameAction> renames = new ArrayList<>();
267        final StringBuilder buf = new StringBuilder();
268
269        // LOG4J2-531: directory scan & rollover must use same format
270        manager.getPatternProcessor().formatFileName(subst, buf, highIndex);
271        String highFilename = subst.replace(buf);
272        final int suffixLength = suffixLength(highFilename);
273        int maxIndex = 0;
274
275        for (int i = highIndex; i >= lowIndex; i--) {
276            File toRename = new File(highFilename);
277            if (i == highIndex && toRename.exists()) {
278                maxIndex = highIndex;
279            } else if (maxIndex == 0 && toRename.exists()) {
280                maxIndex = i + 1;
281                break;
282            }
283
284            boolean isBase = false;
285
286            if (suffixLength > 0) {
287                final File toRenameBase =
288                    new File(highFilename.substring(0, highFilename.length() - suffixLength));
289
290                if (toRename.exists()) {
291                    if (toRenameBase.exists()) {
292                        LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
293                                toRenameBase, toRename);
294                        toRenameBase.delete();
295                    }
296                } else {
297                    toRename = toRenameBase;
298                    isBase = true;
299                }
300            }
301
302            if (toRename.exists()) {
303                //
304                //    if at lower index and then all slots full
305                //        attempt to delete last file
306                //        if that fails then abandon purge
307                if (i == lowIndex) {
308                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.", //
309                            toRename, i);
310                    if (!toRename.delete()) {
311                        return -1;
312                    }
313
314                    break;
315                }
316
317                //
318                //   if intermediate index
319                //     add a rename action to the list
320                buf.setLength(0);
321                // LOG4J2-531: directory scan & rollover must use same format
322                manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
323
324                final String lowFilename = subst.replace(buf);
325                String renameTo = lowFilename;
326
327                if (isBase) {
328                    renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
329                }
330
331                renames.add(new FileRenameAction(toRename, new File(renameTo), true));
332                highFilename = lowFilename;
333            } else {
334                buf.setLength(0);
335                // LOG4J2-531: directory scan & rollover must use same format
336                manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
337
338                highFilename = subst.replace(buf);
339            }
340        }
341        if (maxIndex == 0) {
342            maxIndex = lowIndex;
343        }
344
345        //
346        //   work renames backwards
347        //
348        for (int i = renames.size() - 1; i >= 0; i--) {
349            final Action action = renames.get(i);
350            try {
351                LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
352                        i, renames.size(), action);
353                if (!action.execute()) {
354                    return -1;
355                }
356            } catch (final Exception ex) {
357                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
358                return -1;
359            }
360        }
361        return maxIndex;
362    }
363
364    /**
365     * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
366     * oldest will have the highest.
367     *
368     * @param lowIndex  low index
369     * @param highIndex high index.  Log file associated with high index will be deleted if needed.
370     * @param manager The RollingFileManager
371     * @return true if purge was successful and rollover should be attempted.
372     */
373    private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
374        final List<FileRenameAction> renames = new ArrayList<>();
375        final StringBuilder buf = new StringBuilder();
376
377        // LOG4J2-531: directory scan & rollover must use same format
378        manager.getPatternProcessor().formatFileName(subst, buf, lowIndex);
379
380        String lowFilename = subst.replace(buf);
381        final int suffixLength = suffixLength(lowFilename);
382
383        for (int i = lowIndex; i <= highIndex; i++) {
384            File toRename = new File(lowFilename);
385            boolean isBase = false;
386
387            if (suffixLength > 0) {
388                final File toRenameBase =
389                    new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
390
391                if (toRename.exists()) {
392                    if (toRenameBase.exists()) {
393                        LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
394                                toRenameBase, toRename);
395                        toRenameBase.delete();
396                    }
397                } else {
398                    toRename = toRenameBase;
399                    isBase = true;
400                }
401            }
402
403            if (toRename.exists()) {
404                //
405                //    if at upper index then
406                //        attempt to delete last file
407                //        if that fails then abandon purge
408                if (i == highIndex) {
409                    LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
410                            toRename, i);
411                    if (!toRename.delete()) {
412                        return -1;
413                    }
414
415                    break;
416                }
417
418                //
419                //   if intermediate index
420                //     add a rename action to the list
421                buf.setLength(0);
422                // LOG4J2-531: directory scan & rollover must use same format
423                manager.getPatternProcessor().formatFileName(subst, buf, i + 1);
424
425                final String highFilename = subst.replace(buf);
426                String renameTo = highFilename;
427
428                if (isBase) {
429                    renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
430                }
431
432                renames.add(new FileRenameAction(toRename, new File(renameTo), true));
433                lowFilename = highFilename;
434            } else {
435                break;
436            }
437        }
438
439        //
440        //   work renames backwards
441        //
442        for (int i = renames.size() - 1; i >= 0; i--) {
443            final Action action = renames.get(i);
444            try {
445                LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
446                        i, renames.size(), action);
447                if (!action.execute()) {
448                    return -1;
449                }
450            } catch (final Exception ex) {
451                LOGGER.warn("Exception during purge in RollingFileAppender", ex);
452                return -1;
453            }
454        }
455
456        return lowIndex;
457    }
458
459    private int suffixLength(final String lowFilename) {
460        for (FileExtensions extension : FileExtensions.values()) {
461            if (extension.isExtensionFor(lowFilename)) {
462                return extension.length();
463            }
464        }
465        return 0;
466    }
467
468    /**
469     * Perform the rollover.
470     * @param manager The RollingFileManager name for current active log file.
471     * @return A RolloverDescription.
472     * @throws SecurityException if an error occurs.
473     */
474    @Override
475    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
476        if (maxIndex < 0) {
477            return null;
478        }
479        final long startNanos = System.nanoTime();
480        final int fileIndex = purge(minIndex, maxIndex, manager);
481        if (fileIndex < 0) {
482            return null;
483        }
484        if (LOGGER.isTraceEnabled()) {
485            final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
486            LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
487        }
488        final StringBuilder buf = new StringBuilder(255);
489        manager.getPatternProcessor().formatFileName(subst, buf, fileIndex);
490        final String currentFileName = manager.getFileName();
491
492        final String renameTo = buf.toString();
493        final String compressedName = renameTo;
494        Action compressAction = null;
495
496        if (FileExtensions.GZIP.isExtensionFor(renameTo)) {
497            compressAction = FileExtensions.GZIP.createCompressAction(renameTo, compressedName, true, compressionLevel);
498        } else if (FileExtensions.ZIP.isExtensionFor(renameTo)) {
499            compressAction = FileExtensions.ZIP.createCompressAction(renameTo, compressedName, true, compressionLevel);
500        } else if (FileExtensions.BZIP2.isExtensionFor(renameTo)) {
501            compressAction = FileExtensions.BZIP2.createCompressAction(renameTo, compressedName, true, compressionLevel);
502        }
503
504        final FileRenameAction renameAction =
505            new FileRenameAction(new File(currentFileName), new File(renameTo), false);
506
507        return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction);
508    }
509
510    @Override
511    public String toString() {
512        return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
513    }
514
515}