View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.appender.rolling;
18  
19  import java.io.File;
20  import java.util.ArrayList;
21  import java.util.List;
22  import java.util.Objects;
23  import java.util.concurrent.TimeUnit;
24  import java.util.zip.Deflater;
25  
26  import org.apache.logging.log4j.Logger;
27  import org.apache.logging.log4j.core.appender.rolling.action.Action;
28  import org.apache.logging.log4j.core.appender.rolling.action.CommonsCompressAction;
29  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
30  import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
31  import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
32  import org.apache.logging.log4j.core.config.Configuration;
33  import org.apache.logging.log4j.core.config.plugins.Plugin;
34  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
35  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
36  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
37  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
38  import org.apache.logging.log4j.core.util.Integers;
39  import org.apache.logging.log4j.status.StatusLogger;
40  
41  /**
42   * When rolling over, <code>DefaultRolloverStrategy</code> renames files
43   * according to an algorithm as described below.
44   *
45   * <p>
46   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When
47   * the file name pattern contains a date format then the rollover time interval will be used to calculate the
48   * time to use in the file pattern. When the file pattern contains an integer replacement token one of the
49   * counting techniques will be used.
50   * </p>
51   * <p>
52   * When the ascending attribute is set to true (the default) then the counter will be incremented and the
53   * current log file will be renamed to include the counter value. If the counter hits the maximum value then
54   * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to
55   * have their counter decremented and then the current file will be renamed to have the maximum counter value.
56   * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.
57   * </p>
58   * <p>
59   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
60   * </p>
61   * <p>
62   * Let <em>max</em> and <em>min</em> represent the values of respectively
63   * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
64   * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
65   * <b>FileNamePattern</b>. Then, when rolling over, the file
66   * <code>foo.<em>max</em>.log</code> will be deleted, the file
67   * <code>foo.<em>max-1</em>.log</code> will be renamed as
68   * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
69   * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
70   * the file <code>foo.<em>min+1</em>.log</code> renamed as
71   * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
72   * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
73   * <code>foo.log</code> will be created.
74   * </p>
75   * <p>
76   * Given that this rollover algorithm requires as many file renaming
77   * operations as the window size, large window sizes are discouraged.
78   * </p>
79   */
80  @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
81  public class DefaultRolloverStrategy implements RolloverStrategy {
82  
83      /**
84       * Enumerates over supported file extensions.
85       */
86      private enum FileExtensions {
87          ZIP(".zip") {
88              @Override
89              Action createCompressAction(final String renameTo, final String compressedName,
90                      final boolean deleteSource, final int compressionLevel) {
91                  return new ZipCompressAction(new File(baseName(renameTo)), new File(compressedName), deleteSource,
92                          compressionLevel);
93              }
94          },
95          GZIP(".gz") {
96              @Override
97              Action createCompressAction(final String renameTo, final String compressedName,
98                      final boolean deleteSource, final int compressionLevel) {
99                  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 }