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       * <p>
86       * Package-protected for unit tests.
87       */
88      enum FileExtensions {
89          ZIP(".zip") {
90              @Override
91              Action createCompressAction(final String renameTo, final String compressedName,
92                      final boolean deleteSource, final int compressionLevel) {
93                  return new ZipCompressAction(source(renameTo), target(compressedName), deleteSource, compressionLevel);
94              }
95          },
96          GZ(".gz") {
97              @Override
98              Action createCompressAction(final String renameTo, final String compressedName,
99                      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 }