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