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