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.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.SortedMap;
29  import java.util.concurrent.TimeUnit;
30  import java.util.zip.Deflater;
31  
32  import org.apache.logging.log4j.core.Core;
33  import org.apache.logging.log4j.core.appender.rolling.action.Action;
34  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
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  
44  /**
45   * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below.
46   *
47   * <p>
48   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name
49   * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file
50   * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used.
51   * </p>
52   * <p>
53   * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log
54   * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which
55   * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented
56   * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy
57   * specifying a large maximum value may entirely avoid renaming files.
58   * </p>
59   * <p>
60   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
61   * </p>
62   * <p>
63   * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b>
64   * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of
65   * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file
66   * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file
67   * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file
68   * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file
69   * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
70   * <code>foo.log</code> will be created.
71   * </p>
72   * <p>
73   * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes
74   * are discouraged.
75   * </p>
76   */
77  @Plugin(name = "DefaultRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true)
78  public class DefaultRolloverStrategy extends AbstractRolloverStrategy {
79  
80      private static final int MIN_WINDOW_SIZE = 1;
81      private static final int DEFAULT_WINDOW_SIZE = 7;
82  
83      /**
84       * Creates the DefaultRolloverStrategy.
85       *
86       * @param max The maximum number of files to keep.
87       * @param min The minimum number of files to keep.
88       * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller
89       *            index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
90       * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
91       * @param customActions custom actions to perform asynchronously after rollover
92       * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
93       * @param config The Configuration.
94       * @return A DefaultRolloverStrategy.
95       */
96      @PluginFactory
97      public static DefaultRolloverStrategy createStrategy(
98              // @formatter:off
99              @PluginAttribute("max") final String max,
100             @PluginAttribute("min") final String min,
101             @PluginAttribute("fileIndex") final String fileIndex,
102             @PluginAttribute("compressionLevel") final String compressionLevelStr,
103             @PluginElement("Actions") final Action[] customActions,
104             @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true)
105                     final boolean stopCustomActionsOnError,
106             @PluginConfiguration final Configuration config) {
107             // @formatter:on
108         int minIndex;
109         int maxIndex;
110         boolean useMax;
111 
112         if (fileIndex != null && fileIndex.equalsIgnoreCase("nomax")) {
113             minIndex = Integer.MIN_VALUE;
114             maxIndex = Integer.MAX_VALUE;
115             useMax = false;
116         } else {
117             useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
118             minIndex = MIN_WINDOW_SIZE;
119             if (min != null) {
120                 minIndex = Integer.parseInt(min);
121                 if (minIndex < 1) {
122                     LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
123                     minIndex = MIN_WINDOW_SIZE;
124                 }
125             }
126             maxIndex = DEFAULT_WINDOW_SIZE;
127             if (max != null) {
128                 maxIndex = Integer.parseInt(max);
129                 if (maxIndex < minIndex) {
130                     maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
131                     LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
132                 }
133             }
134         }
135         final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
136         return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(),
137                 customActions, stopCustomActionsOnError);
138     }
139 
140     /**
141      * Index for oldest retained log file.
142      */
143     private final int maxIndex;
144 
145     /**
146      * Index for most recent log file.
147      */
148     private final int minIndex;
149     private final boolean useMax;
150     private final int compressionLevel;
151     private final List<Action> customActions;
152     private final boolean stopCustomActionsOnError;
153 
154     /**
155      * Constructs a new instance.
156      *
157      * @param minIndex The minimum index.
158      * @param maxIndex The maximum index.
159      * @param customActions custom actions to perform asynchronously after rollover
160      * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs
161      */
162     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax,
163             final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions,
164             final boolean stopCustomActionsOnError) {
165         super(strSubstitutor);
166         this.minIndex = minIndex;
167         this.maxIndex = maxIndex;
168         this.useMax = useMax;
169         this.compressionLevel = compressionLevel;
170         this.stopCustomActionsOnError = stopCustomActionsOnError;
171         this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions);
172     }
173 
174     public int getCompressionLevel() {
175         return this.compressionLevel;
176     }
177 
178     public List<Action> getCustomActions() {
179         return customActions;
180     }
181 
182     public int getMaxIndex() {
183         return this.maxIndex;
184     }
185 
186     public int getMinIndex() {
187         return this.minIndex;
188     }
189 
190     public boolean isStopCustomActionsOnError() {
191         return stopCustomActionsOnError;
192     }
193 
194     public boolean isUseMax() {
195         return useMax;
196     }
197 
198     private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
199         return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
200     }
201 
202     /**
203      * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
204      * newest the highest.
205      *
206      * @param lowIndex low index. Log file associated with low index will be deleted if needed.
207      * @param highIndex high index.
208      * @param manager The RollingFileManager
209      * @return true if purge was successful and rollover should be attempted.
210      */
211     private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
212         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
213         final int maxFiles = highIndex - lowIndex + 1;
214 
215         boolean renameFiles = false;
216         while (eligibleFiles.size() >= maxFiles) {
217             try {
218                 LOGGER.debug("Eligible files: {}", eligibleFiles);
219                 Integer key = eligibleFiles.firstKey();
220                 LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
221                 Files.delete(eligibleFiles.get(key));
222                 eligibleFiles.remove(key);
223                 renameFiles = true;
224             } catch (IOException ioe) {
225                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
226                 break;
227             }
228         }
229         final StringBuilder buf = new StringBuilder();
230         if (renameFiles) {
231             for (Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
232                 buf.setLength(0);
233                 // LOG4J2-531: directory scan & rollover must use same format
234                 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
235                 String currentName = entry.getValue().toFile().getName();
236                 String renameTo = buf.toString();
237                 int suffixLength = suffixLength(renameTo);
238                 if (suffixLength > 0 && suffixLength(currentName) == 0) {
239                    renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
240                 }
241                 Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
242                 try {
243                     LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
244                     if (!action.execute()) {
245                         return -1;
246                     }
247                 } catch (final Exception ex) {
248                     LOGGER.warn("Exception during purge in RollingFileAppender", ex);
249                     return -1;
250                 }
251             }
252         }
253 
254         return eligibleFiles.size() > 0 ?
255                 (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
256     }
257 
258     /**
259      * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the
260      * oldest will have the highest.
261      *
262      * @param lowIndex low index
263      * @param highIndex high index. Log file associated with high index will be deleted if needed.
264      * @param manager The RollingFileManager
265      * @return true if purge was successful and rollover should be attempted.
266      */
267     private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
268         // Retrieve the files in descending order, so the highest key will be first.
269         final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false);
270         final int maxFiles = highIndex - lowIndex + 1;
271 
272         while (eligibleFiles.size() >= maxFiles) {
273             try {
274                 Integer key = eligibleFiles.firstKey();
275                 Files.delete(eligibleFiles.get(key));
276                 eligibleFiles.remove(key);
277             } catch (IOException ioe) {
278                 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
279                 break;
280             }
281         }
282         final StringBuilder buf = new StringBuilder();
283         for (Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
284             buf.setLength(0);
285             // LOG4J2-531: directory scan & rollover must use same format
286             manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1);
287             String currentName = entry.getValue().toFile().getName();
288             String renameTo = buf.toString();
289             int suffixLength = suffixLength(renameTo);
290             if (suffixLength > 0 && suffixLength(currentName) == 0) {
291                 renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
292             }
293             Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
294             try {
295                 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action);
296                 if (!action.execute()) {
297                     return -1;
298                 }
299             } catch (final Exception ex) {
300                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
301                 return -1;
302             }
303         }
304 
305         return lowIndex;
306     }
307 
308     /**
309      * Performs the rollover.
310      *
311      * @param manager The RollingFileManager name for current active log file.
312      * @return A RolloverDescription.
313      * @throws SecurityException if an error occurs.
314      */
315     @Override
316     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
317         int fileIndex;
318         if (minIndex == Integer.MIN_VALUE) {
319             final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
320             fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
321         } else {
322             if (maxIndex < 0) {
323                 return null;
324             }
325             final long startNanos = System.nanoTime();
326             fileIndex = purge(minIndex, maxIndex, manager);
327             if (fileIndex < 0) {
328                 return null;
329             }
330             if (LOGGER.isTraceEnabled()) {
331                 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
332                 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
333             }
334         }
335         final StringBuilder buf = new StringBuilder(255);
336         manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
337         final String currentFileName = manager.getFileName();
338 
339         String renameTo = buf.toString();
340         final String compressedName = renameTo;
341         Action compressAction = null;
342 
343         FileExtension fileExtension = manager.getFileExtension();
344         if (fileExtension != null) {
345             renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
346             compressAction = fileExtension.createCompressAction(renameTo, compressedName,
347                     true, compressionLevel);
348         }
349 
350         if (currentFileName.equals(renameTo)) {
351             LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
352             return new RolloverDescriptionImpl(currentFileName, false, null, null);
353         }
354 
355         final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
356                     manager.isRenameEmptyFiles());
357 
358         final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
359         return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
360     }
361 
362     @Override
363     public String toString() {
364         return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")";
365     }
366 
367 }