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.io.IOException;
21  import java.nio.file.Files;
22  import java.nio.file.Path;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.SortedMap;
28  import java.util.concurrent.TimeUnit;
29  import java.util.zip.Deflater;
30  
31  import org.apache.logging.log4j.core.Core;
32  import org.apache.logging.log4j.core.appender.rolling.action.Action;
33  import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction;
34  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
35  import org.apache.logging.log4j.core.appender.rolling.action.PathCondition;
36  import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction;
37  import org.apache.logging.log4j.core.config.Configuration;
38  import org.apache.logging.log4j.core.config.plugins.Plugin;
39  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
40  import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
41  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
42  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
43  import org.apache.logging.log4j.core.config.plugins.PluginElement;
44  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
45  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
46  import org.apache.logging.log4j.core.util.Integers;
47  
48  /**
49   * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
50   *
51   * <p>
52   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
53   * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
54   * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
55   * </p>
56   * <p>
57   * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
58   * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
59   * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
60   * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
61   * specifying a large maximum value may entirely avoid renaming files.
62   * </p>
63   * <p>
64   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
65   * </p>
66   * <p>
67   * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b>
68   * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of
69   * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file
70   * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file
71   * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file
72   * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file
73   * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
74   * <code>foo.log</code> will be created.
75   * </p>
76   * <p>
77   * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
78   * are discouraged.
79   * </p>
80   */
81  @Plugin(name = "DefaultRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
82  public class DefaultRolloverStrategy extends AbstractRolloverStrategy {
83  
84      private static final int MIN_WINDOW_SIZE = 1;
85      private static final int DEFAULT_WINDOW_SIZE = 7;
86  
87      /**
88       * Builds DefaultRolloverStrategy instances.
89       */
90      public static class Builder implements org.apache.logging.log4j.core.util.Builder<DefaultRolloverStrategy> {
91          @PluginBuilderAttribute("max")
92          private String max;
93          
94          @PluginBuilderAttribute("min")
95          private String min;
96          
97          @PluginBuilderAttribute("fileIndex")
98          private String fileIndex;
99  
100         @PluginBuilderAttribute("compressionLevel")
101         private String compressionLevelStr;
102 
103         @PluginElement("Actions")
104         private Action[] customActions;
105 
106         @PluginBuilderAttribute(value = "stopCustomActionsOnError")
107         private boolean stopCustomActionsOnError = true;
108 
109         @PluginBuilderAttribute(value = "tempCompressedFilePattern")
110         private String tempCompressedFilePattern;
111 
112         @PluginConfiguration
113         private Configuration config;
114 
115         @Override
116         public DefaultRolloverStrategy build() {
117             int minIndex;
118             int maxIndex;
119             boolean useMax;
120 
121             if (fileIndex != null && fileIndex.equalsIgnoreCase("nomax")) {
122                 minIndex = Integer.MIN_VALUE;
123                 maxIndex = Integer.MAX_VALUE;
124                 useMax = false;
125             } else {
126                 useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
127                 minIndex = MIN_WINDOW_SIZE;
128                 if (min != null) {
129                     minIndex = Integer.parseInt(min);
130                     if (minIndex < 1) {
131                         LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
132                         minIndex = MIN_WINDOW_SIZE;
133                     }
134                 }
135                 maxIndex = DEFAULT_WINDOW_SIZE;
136                 if (max != null) {
137                     maxIndex = Integer.parseInt(max);
138                     if (maxIndex < minIndex) {
139                         maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
140                         LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
141                     }
142                 }
143             }
144             final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
145             return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
146                     customActions, stopCustomActionsOnError, tempCompressedFilePattern);
147         }
148 
149         public String getMax() {
150             return max;
151         }
152 
153         /**
154          * Defines the maximum number of files to keep.
155          *
156          * @param max The maximum number of files to keep.
157          * @return This builder for chaining convenience
158          */
159         public Builder withMax(final String max) {
160             this.max = max;
161             return this;
162         }
163 
164         public String getMin() {
165             return min;
166         }
167 
168         /**
169          * Defines the minimum number of files to keep.
170          *
171          * @param min The minimum number of files to keep.
172          * @return This builder for chaining convenience
173          */
174         public Builder withMin(final String min) {
175             this.min = min;
176             return this;
177         }
178 
179         public String getFileIndex() {
180             return fileIndex;
181         }
182 
183         /**
184          * Defines the file index for rolling strategy.
185          *
186          * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
187          *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
188          * @return This builder for chaining convenience
189          */
190         public Builder withFileIndex(final String fileIndex) {
191             this.fileIndex = fileIndex;
192             return this;
193         }
194 
195         public String getCompressionLevelStr() {
196             return compressionLevelStr;
197         }
198 
199         /**
200          * Defines compression level.
201          *
202          * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
203          * @return This builder for chaining convenience
204          */
205         public Builder withCompressionLevelStr(final String compressionLevelStr) {
206             this.compressionLevelStr = compressionLevelStr;
207             return this;
208         }
209 
210         public Action[] getCustomActions() {
211             return customActions;
212         }
213 
214         /**
215          * Defines custom actions.
216          *
217          * @param customActions custom actions to perform asynchronously after rollover
218          * @return This builder for chaining convenience
219          */
220         public Builder withCustomActions(final Action[] customActions) {
221             this.customActions = customActions;
222             return this;
223         }
224 
225         public boolean isStopCustomActionsOnError() {
226             return stopCustomActionsOnError;
227         }
228 
229         /**
230          * Defines whether to stop executing asynchronous actions if an error occurs.
231          *
232          * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
233          * @return This builder for chaining convenience
234          */
235         public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) {
236             this.stopCustomActionsOnError = stopCustomActionsOnError;
237             return this;
238         }
239 
240         public String getTempCompressedFilePattern() {
241             return tempCompressedFilePattern;
242         }
243 
244         /**
245          * Defines temporary compression file pattern.
246          *
247          * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used
248          * @return This builder for chaining convenience
249          */
250         public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) {
251             this.tempCompressedFilePattern = tempCompressedFilePattern;
252             return this;
253         }
254 
255         public Configuration getConfig() {
256             return config;
257         }
258 
259         /**
260          * Defines configuration.
261          * 
262          * @param config The Configuration.
263          * @return This builder for chaining convenience
264          */
265         public Builder withConfig(final Configuration config) {
266             this.config = config;
267             return this;
268         }
269     }
270 
271     @PluginBuilderFactory
272     public static Builder newBuilder() {
273         return new Builder();
274     }
275 
276     /**
277      * Creates the DefaultRolloverStrategy.
278      *
279      * @param max The maximum number of files to keep.
280      * @param min The minimum number of files to keep.
281      * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
282      *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
283      * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
284      * @param customActions custom actions to perform asynchronously after rollover
285      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
286      * @param config The Configuration.
287      * @return A DefaultRolloverStrategy.
288      * @deprecated Since 2.9 Usage of Builder API is preferable
289      */
290     @PluginFactory
291     @Deprecated
292     public static DefaultRolloverStrategy createStrategy(
293             // @formatter:off
294             @PluginAttribute("max") final String max,
295             @PluginAttribute("min") final String min,
296             @PluginAttribute("fileIndex") final String fileIndex,
297             @PluginAttribute("compressionLevel") final String compressionLevelStr,
298             @PluginElement("Actions") final Action[] customActions,
299             @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
300                     final boolean stopCustomActionsOnError,
301             @PluginConfiguration final Configuration config) {
302         return DefaultRolloverStrategy.newBuilder()
303                     .withMin(min)
304                     .withMax(max)
305                     .withFileIndex(fileIndex)
306                     .withCompressionLevelStr(compressionLevelStr)
307                     .withCustomActions(customActions)
308                     .withStopCustomActionsOnError(stopCustomActionsOnError)
309                     .withConfig(config)
310                 .build();
311             // @formatter:on
312     }
313 
314     /**
315      * Index for oldest retained log file.
316      */
317     private final int maxIndex;
318 
319     /**
320      * Index for most recent log file.
321      */
322     private final int minIndex;
323     private final boolean useMax;
324     private final int compressionLevel;
325     private final List<Action> customActions;
326     private final boolean stopCustomActionsOnError;
327     private final PatternProcessor tempCompressedFilePattern;
328 
329     /**
330      * Constructs a new instance.
331      *
332      * @param minIndex The minimum index.
333      * @param maxIndex The maximum index.
334      * @param customActions custom actions to perform asynchronously after rollover
335      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
336      * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter
337      */
338     @Deprecated
339     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
340             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
341             final boolean stopCustomActionsOnError) {
342         this(minIndex, maxIndex, useMax, compressionLevel,
343                        strSubstitutor, customActions, stopCustomActionsOnError, null);
344     }
345 
346     /**
347      * Constructs a new instance.
348      *
349      * @param minIndex The minimum index.
350      * @param maxIndex The maximum index.
351      * @param customActions custom actions to perform asynchronously after rollover
352      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
353      * @param tempCompressedFilePatternString File pattern of the working file
354      *                                     used during compression, if null no temporary file are used
355      */
356     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
357             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
358             final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) {
359         super(strSubstitutor);
360         this.minIndex = minIndex;
361         this.maxIndex = maxIndex;
362         this.useMax = useMax;
363         this.compressionLevel = compressionLevel;
364         this.stopCustomActionsOnError = stopCustomActionsOnError;
365         this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
366         this.tempCompressedFilePattern =
367                 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null;
368     }
369 
370     public int getCompressionLevel() {
371         return this.compressionLevel;
372     }
373 
374     public List<Action> getCustomActions() {
375         return customActions;
376     }
377 
378     public int getMaxIndex() {
379         return this.maxIndex;
380     }
381 
382     public int getMinIndex() {
383         return this.minIndex;
384     }
385 
386     public boolean isStopCustomActionsOnError() {
387         return stopCustomActionsOnError;
388     }
389 
390     public boolean isUseMax() {
391         return useMax;
392     }
393 
394     public PatternProcessor getTempCompressedFilePattern() {
395         return tempCompressedFilePattern;
396     }
397 
398     private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
399         return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
400     }
401 
402     /**
403      * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
404      * newest the highest.
405      *
406      * @param lowIndex low index. Log file associated with low index will be deleted if needed.
407      * @param highIndex high index.
408      * @param manager The RollingFileManager
409      * @return true if purge was successful and rollover should be attempted.
410      */
411     private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
412         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
413         final int maxFiles = highIndex - lowIndex + 1;
414 
415         boolean renameFiles = false;
416         while (eligibleFiles.size() >= maxFiles) {
417             try {
418                 LOGGER.debug("Eligible files: {}", eligibleFiles);
419                 final Integer key = eligibleFiles.firstKey();
420                 LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
421                 Files.delete(eligibleFiles.get(key));
422                 eligibleFiles.remove(key);
423                 renameFiles = true;
424             } catch (final IOException ioe) {
425                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
426                 break;
427             }
428         }
429         final StringBuilder buf = new StringBuilder();
430         if (renameFiles) {
431             for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
432                 buf.setLength(0);
433                 // LOG4J2-531: directory scan & rollover must use same format
434                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
435                 final String currentName = entry.getValue().toFile().getName();
436                 String renameTo = buf.toString();
437                 final int suffixLength = suffixLength(renameTo);
438                 if (suffixLength > 0 && suffixLength(currentName) == 0) {
439                    renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
440                 }
441                 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
442                 try {
443                     LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
444                     if (!action.execute()) {
445                         return -1;
446                     }
447                 } catch (final Exception ex) {
448                     LOGGER.warn("Exception during purge in RollingFileAppender", ex);
449                     return -1;
450                 }
451             }
452         }
453 
454         return eligibleFiles.size() > 0 ?
455                 (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
456     }
457 
458     /**
459      * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the
460      * oldest will have the highest.
461      *
462      * @param lowIndex low index
463      * @param highIndex high index. Log file associated with high index will be deleted if needed.
464      * @param manager The RollingFileManager
465      * @return true if purge was successful and rollover should be attempted.
466      */
467     private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
468         // Retrieve the files in descending order, so the highest key will be first.
469         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false);
470         final int maxFiles = highIndex - lowIndex + 1;
471 
472         while (eligibleFiles.size() >= maxFiles) {
473             try {
474                 final Integer key = eligibleFiles.firstKey();
475                 Files.delete(eligibleFiles.get(key));
476                 eligibleFiles.remove(key);
477             } catch (final IOException ioe) {
478                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
479                 break;
480             }
481         }
482         final StringBuilder buf = new StringBuilder();
483         for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
484             buf.setLength(0);
485             // LOG4J2-531: directory scan & rollover must use same format
486             manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1);
487             final String currentName = entry.getValue().toFile().getName();
488             String renameTo = buf.toString();
489             final int suffixLength = suffixLength(renameTo);
490             if (suffixLength > 0 && suffixLength(currentName) == 0) {
491                 renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
492             }
493             final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
494             try {
495                 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action);
496                 if (!action.execute()) {
497                     return -1;
498                 }
499             } catch (final Exception ex) {
500                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
501                 return -1;
502             }
503         }
504 
505         return lowIndex;
506     }
507 
508     /**
509      * Performs the rollover.
510      *
511      * @param manager The RollingFileManager name for current active log file.
512      * @return A RolloverDescription.
513      * @throws SecurityException if an error occurs.
514      */
515     @Override
516     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
517         int fileIndex;
518         if (minIndex == Integer.MIN_VALUE) {
519             final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
520             fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
521         } else {
522             if (maxIndex < 0) {
523                 return null;
524             }
525             final long startNanos = System.nanoTime();
526             fileIndex = purge(minIndex, maxIndex, manager);
527             if (fileIndex < 0) {
528                 return null;
529             }
530             if (LOGGER.isTraceEnabled()) {
531                 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
532                 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
533             }
534         }
535         final StringBuilder buf = new StringBuilder(255);
536         manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
537         final String currentFileName = manager.getFileName();
538 
539         String renameTo = buf.toString();
540         final String compressedName = renameTo;
541         Action compressAction = null;
542 
543         final FileExtension fileExtension = manager.getFileExtension();
544         if (fileExtension != null) {
545             final File renameToFile = new File(renameTo);
546             renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
547             if (tempCompressedFilePattern != null) {
548                 buf.delete(0, buf.length());
549                 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
550                 final String tmpCompressedName = buf.toString();
551                 final File tmpCompressedNameFile = new File(tmpCompressedName);
552                 final File parentFile = tmpCompressedNameFile.getParentFile();
553                 if (parentFile != null) {
554                     parentFile.mkdirs();
555                 }
556                 compressAction = new CompositeAction(
557                         Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
558                                 true, compressionLevel),
559                                 new FileRenameAction(tmpCompressedNameFile,
560                                         renameToFile, true)),
561                         true);
562             } else {
563                 compressAction = fileExtension.createCompressAction(renameTo, compressedName,
564                         true, compressionLevel);
565             }
566         }
567 
568         if (currentFileName.equals(renameTo)) {
569             LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
570             return new RolloverDescriptionImpl(currentFileName, false, null, null);
571         }
572 
573         if (compressAction != null && manager.isAttributeViewEnabled()) {
574             // Propagate posix attribute view to compressed file
575             // @formatter:off
576             final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
577                                                         .withBasePath(compressedName)
578                                                         .withFollowLinks(false)
579                                                         .withMaxDepth(1)
580                                                         .withPathConditions(new PathCondition[0])
581                                                         .withSubst(getStrSubstitutor())
582                                                         .withFilePermissions(manager.getFilePermissions())
583                                                         .withFileOwner(manager.getFileOwner())
584                                                         .withFileGroup(manager.getFileGroup())
585                                                         .build();
586             // @formatter:on
587             compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
588         }
589 
590         final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
591                     manager.isRenameEmptyFiles());
592 
593         final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
594         return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
595     }
596 
597     @Override
598     public String toString() {
599         return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
600     }
601 
602 }