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         XZ(".xz") {
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("xz", source(renameTo), target(compressedName), deleteSource);
131             }
132         };
133 
134         static FileExtensions lookup(final String fileExtension) {
135             for (final FileExtensions ext : values()) {
136                 if (ext.isExtensionFor(fileExtension)) {
137                     return ext;
138                 }
139             }
140             return null;
141         }
142 
143         private final String extension;
144 
145         private FileExtensions(final String extension) {
146             Objects.requireNonNull(extension, "extension");
147             this.extension = extension;
148         }
149 
150         abstract Action createCompressAction(String renameTo, String compressedName, boolean deleteSource,
151                 int compressionLevel);
152 
153         String getExtension() {
154             return extension;
155         }
156 
157         boolean isExtensionFor(final String s) {
158             return s.endsWith(this.extension);
159         }
160 
161         int length() {
162             return extension.length();
163         }
164 
165         File source(final String fileName) {
166             return new File(fileName);
167         }
168 
169         File target(final String fileName) {
170             return new File(fileName);
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      * Create the DefaultRolloverStrategy.
184      *
185      * @param max The maximum number of files to keep.
186      * @param min The minimum number of files to keep.
187      * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
188      *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
189      * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
190      * @param customActions custom actions to perform asynchronously after rollover
191      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
192      * @param config The Configuration.
193      * @return A DefaultRolloverStrategy.
194      */
195     @PluginFactory
196     public static DefaultRolloverStrategy createStrategy(
197             // @formatter:off
198             @PluginAttribute("max") final String max,
199             @PluginAttribute("min") final String min,
200             @PluginAttribute("fileIndex") final String fileIndex,
201             @PluginAttribute("compressionLevel") final String compressionLevelStr,
202             @PluginElement("Actions") final Action[] customActions,
203             @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
204                     final boolean stopCustomActionsOnError,
205             @PluginConfiguration final Configuration config) {
206             // @formatter:on
207         final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
208         int minIndex = MIN_WINDOW_SIZE;
209         if (min != null) {
210             minIndex = Integer.parseInt(min);
211             if (minIndex < 1) {
212                 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
213                 minIndex = MIN_WINDOW_SIZE;
214             }
215         }
216         int maxIndex = DEFAULT_WINDOW_SIZE;
217         if (max != null) {
218             maxIndex = Integer.parseInt(max);
219             if (maxIndex < minIndex) {
220                 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
221                 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
222             }
223         }
224         final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
225         return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
226                 customActions, stopCustomActionsOnError);
227     }
228 
229     /**
230      * Index for oldest retained log file.
231      */
232     private final int maxIndex;
233 
234     /**
235      * Index for most recent log file.
236      */
237     private final int minIndex;
238     private final boolean useMax;
239     private final StrSubstitutor strSubstitutor;
240     private final int compressionLevel;
241     private final List<Action> customActions;
242     private final boolean stopCustomActionsOnError;
243 
244     /**
245      * Constructs a new instance.
246      *
247      * @param minIndex The minimum index.
248      * @param maxIndex The maximum index.
249      * @param customActions custom actions to perform asynchronously after rollover
250      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
251      */
252     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
253             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
254             final boolean stopCustomActionsOnError) {
255         this.minIndex = minIndex;
256         this.maxIndex = maxIndex;
257         this.useMax = useMax;
258         this.compressionLevel = compressionLevel;
259         this.strSubstitutor = strSubstitutor;
260         this.stopCustomActionsOnError = stopCustomActionsOnError;
261         this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
262     }
263 
264     public int getCompressionLevel() {
265         return this.compressionLevel;
266     }
267 
268     public List<Action> getCustomActions() {
269         return customActions;
270     }
271 
272     public int getMaxIndex() {
273         return this.maxIndex;
274     }
275 
276     public int getMinIndex() {
277         return this.minIndex;
278     }
279 
280     public StrSubstitutor getStrSubstitutor() {
281         return strSubstitutor;
282     }
283 
284     public boolean isStopCustomActionsOnError() {
285         return stopCustomActionsOnError;
286     }
287 
288     public boolean isUseMax() {
289         return useMax;
290     }
291 
292     private Action merge(final Action compressAction, final List<Action> custom, final boolean stopOnError) {
293         if (custom.isEmpty()) {
294             return compressAction;
295         }
296         if (compressAction == null) {
297             return new CompositeAction(custom, stopOnError);
298         }
299         final List<Action> all = new ArrayList<>();
300         all.add(compressAction);
301         all.addAll(custom);
302         return new CompositeAction(all, stopOnError);
303     }
304 
305     private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
306         return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
307     }
308 
309     /**
310      * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index, the
311      * newest the highest.
312      *
313      * @param lowIndex low index
314      * @param highIndex high index. Log file associated with high index will be deleted if needed.
315      * @param manager The RollingFileManager
316      * @return true if purge was successful and rollover should be attempted.
317      */
318     private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
319         final List<FileRenameAction> renames = new ArrayList<>();
320         final StringBuilder buf = new StringBuilder();
321 
322         // LOG4J2-531: directory scan & rollover must use same format
323         manager.getPatternProcessor().formatFileName(strSubstitutor, buf, highIndex);
324         String highFilename = strSubstitutor.replace(buf);
325         final int suffixLength = suffixLength(highFilename);
326         int curMaxIndex = 0;
327 
328         for (int i = highIndex; i >= lowIndex; i--) {
329             File toRename = new File(highFilename);
330             if (i == highIndex && toRename.exists()) {
331                 curMaxIndex = highIndex;
332             } else if (curMaxIndex == 0 && toRename.exists()) {
333                 curMaxIndex = i + 1;
334                 break;
335             }
336 
337             boolean isBase = false;
338 
339             if (suffixLength > 0) {
340                 final File toRenameBase = new File(highFilename.substring(0, highFilename.length() - suffixLength));
341 
342                 if (toRename.exists()) {
343                     if (toRenameBase.exists()) {
344                         LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
345                                 toRenameBase, toRename);
346                         toRenameBase.delete();
347                     }
348                 } else {
349                     toRename = toRenameBase;
350                     isBase = true;
351                 }
352             }
353 
354             if (toRename.exists()) {
355                 //
356                 // if at lower index and then all slots full
357                 // attempt to delete last file
358                 // if that fails then abandon purge
359                 if (i == lowIndex) {
360                     LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.",
361                             toRename, i);
362                     if (!toRename.delete()) {
363                         return -1;
364                     }
365 
366                     break;
367                 }
368 
369                 //
370                 // if intermediate index
371                 // add a rename action to the list
372                 buf.setLength(0);
373                 // LOG4J2-531: directory scan & rollover must use same format
374                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
375 
376                 final String lowFilename = strSubstitutor.replace(buf);
377                 String renameTo = lowFilename;
378 
379                 if (isBase) {
380                     renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
381                 }
382 
383                 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
384                 highFilename = lowFilename;
385             } else {
386                 buf.setLength(0);
387                 // LOG4J2-531: directory scan & rollover must use same format
388                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i - 1);
389 
390                 highFilename = strSubstitutor.replace(buf);
391             }
392         }
393         if (curMaxIndex == 0) {
394             curMaxIndex = lowIndex;
395         }
396 
397         //
398         // work renames backwards
399         //
400         for (int i = renames.size() - 1; i >= 0; i--) {
401             final Action action = renames.get(i);
402             try {
403                 LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
404                         i, renames.size(), action);
405                 if (!action.execute()) {
406                     return -1;
407                 }
408             } catch (final Exception ex) {
409                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
410                 return -1;
411             }
412         }
413         return curMaxIndex;
414     }
415 
416     /**
417      * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
418      * oldest will have the highest.
419      *
420      * @param lowIndex low index
421      * @param highIndex high index. Log file associated with high index will be deleted if needed.
422      * @param manager The RollingFileManager
423      * @return true if purge was successful and rollover should be attempted.
424      */
425     private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
426         final List<FileRenameAction> renames = new ArrayList<>();
427         final StringBuilder buf = new StringBuilder();
428 
429         // LOG4J2-531: directory scan & rollover must use same format
430         manager.getPatternProcessor().formatFileName(strSubstitutor, buf, lowIndex);
431 
432         String lowFilename = strSubstitutor.replace(buf);
433         final int suffixLength = suffixLength(lowFilename);
434 
435         for (int i = lowIndex; i <= highIndex; i++) {
436             File toRename = new File(lowFilename);
437             boolean isBase = false;
438 
439             if (suffixLength > 0) {
440                 final File toRenameBase = new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
441 
442                 if (toRename.exists()) {
443                     if (toRenameBase.exists()) {
444                         LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
445                                 toRenameBase, toRename);
446                         toRenameBase.delete();
447                     }
448                 } else {
449                     toRename = toRenameBase;
450                     isBase = true;
451                 }
452             }
453 
454             if (toRename.exists()) {
455                 //
456                 // if at upper index then
457                 // attempt to delete last file
458                 // if that fails then abandon purge
459                 if (i == highIndex) {
460                     LOGGER.debug(
461                             "DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
462                             toRename, i);
463                     if (!toRename.delete()) {
464                         return -1;
465                     }
466 
467                     break;
468                 }
469 
470                 //
471                 // if intermediate index
472                 // add a rename action to the list
473                 buf.setLength(0);
474                 // LOG4J2-531: directory scan & rollover must use same format
475                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, i + 1);
476 
477                 final String highFilename = strSubstitutor.replace(buf);
478                 String renameTo = highFilename;
479 
480                 if (isBase) {
481                     renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
482                 }
483 
484                 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
485                 lowFilename = highFilename;
486             } else {
487                 break;
488             }
489         }
490 
491         //
492         // work renames backwards
493         //
494         for (int i = renames.size() - 1; i >= 0; i--) {
495             final Action action = renames.get(i);
496             try {
497                 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
498                         i, renames.size(), action);
499                 if (!action.execute()) {
500                     return -1;
501                 }
502             } catch (final Exception ex) {
503                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
504                 return -1;
505             }
506         }
507 
508         return lowIndex;
509     }
510 
511     /**
512      * Perform the rollover.
513      *
514      * @param manager The RollingFileManager name for current active log file.
515      * @return A RolloverDescription.
516      * @throws SecurityException if an error occurs.
517      */
518     @Override
519     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
520         if (maxIndex < 0) {
521             return null;
522         }
523         final long startNanos = System.nanoTime();
524         final int fileIndex = purge(minIndex, maxIndex, manager);
525         if (fileIndex < 0) {
526             return null;
527         }
528         if (LOGGER.isTraceEnabled()) {
529             final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
530             LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
531         }
532         final StringBuilder buf = new StringBuilder(255);
533         manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
534         final String currentFileName = manager.getFileName();
535 
536         String renameTo = buf.toString();
537         final String compressedName = renameTo;
538         Action compressAction = null;
539 
540         for (final FileExtensions ext : FileExtensions.values()) { // LOG4J2-1077 support other compression formats
541             if (ext.isExtensionFor(renameTo)) {
542                 renameTo = renameTo.substring(0, renameTo.length() - ext.length()); // LOG4J2-1135 omit extension!
543                 compressAction = ext.createCompressAction(renameTo, compressedName, true, compressionLevel);
544                 break;
545             }
546         }
547 
548         final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
549                 manager.isRenameEmptyFiles());
550 
551         final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
552         return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
553     }
554 
555     private int suffixLength(final String lowFilename) {
556         for (final FileExtensions extension : FileExtensions.values()) {
557             if (extension.isExtensionFor(lowFilename)) {
558                 return extension.length();
559             }
560         }
561         return 0;
562     }
563 
564     @Override
565     public String toString() {
566         return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
567     }
568 
569 }