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.List;
22  import java.util.zip.Deflater;
23  
24  import org.apache.logging.log4j.Logger;
25  import org.apache.logging.log4j.core.appender.rolling.action.Action;
26  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
27  import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
28  import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
29  import org.apache.logging.log4j.core.config.Configuration;
30  import org.apache.logging.log4j.core.config.plugins.Plugin;
31  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
32  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
33  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
34  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
35  import org.apache.logging.log4j.core.util.Integers;
36  import org.apache.logging.log4j.status.StatusLogger;
37  
38  /**
39   * When rolling over, <code>DefaultRolloverStrategy</code> renames files
40   * according to an algorithm as described below.
41   *
42   * <p>
43   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When
44   * the file name pattern contains a date format then the rollover time interval will be used to calculate the
45   * time to use in the file pattern. When the file pattern contains an integer replacement token one of the
46   * counting techniques will be used.
47   * </p>
48   * <p>
49   * When the ascending attribute is set to true (the default) then the counter will be incremented and the
50   * current log file will be renamed to include the counter value. If the counter hits the maximum value then
51   * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to
52   * have their counter decremented and then the current file will be renamed to have the maximum counter value.
53   * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.
54   * </p>
55   * <p>
56   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
57   * </p>
58   * <p>
59   * Let <em>max</em> and <em>min</em> represent the values of respectively
60   * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
61   * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
62   * <b>FileNamePattern</b>. Then, when rolling over, the file
63   * <code>foo.<em>max</em>.log</code> will be deleted, the file
64   * <code>foo.<em>max-1</em>.log</code> will be renamed as
65   * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
66   * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
67   * the file <code>foo.<em>min+1</em>.log</code> renamed as
68   * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
69   * 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
74   * operations as the window size, large window sizes are discouraged.
75   * </p>
76   */
77  @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
78  public class DefaultRolloverStrategy implements RolloverStrategy {
79  
80      private static final String EXT_ZIP = ".zip";
81      private static final String EXT_GZIP = ".gz";
82  
83      /**
84       * Allow subclasses access to the status logger without creating another instance.
85       */
86      protected static final Logger LOGGER = StatusLogger.getLogger();
87  
88      private static final int MIN_WINDOW_SIZE = 1;
89      private static final int DEFAULT_WINDOW_SIZE = 7;
90  
91      /**
92       * Create the DefaultRolloverStrategy.
93       * @param max The maximum number of files to keep.
94       * @param min The minimum number of files to keep.
95       * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a
96       * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
97       * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
98       * @param config The Configuration.
99       * @return A DefaultRolloverStrategy.
100      */
101     @PluginFactory
102     public static DefaultRolloverStrategy createStrategy(
103             @PluginAttribute("max") final String max,
104             @PluginAttribute("min") final String min,
105             @PluginAttribute("fileIndex") final String fileIndex,
106             @PluginAttribute("compressionLevel") final String compressionLevelStr,
107             @PluginConfiguration final Configuration config) {
108         final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
109         int minIndex = MIN_WINDOW_SIZE;
110         if (min != null) {
111             minIndex = Integer.parseInt(min);
112             if (minIndex < 1) {
113                 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
114                 minIndex = MIN_WINDOW_SIZE;
115             }
116         }
117         int maxIndex = DEFAULT_WINDOW_SIZE;
118         if (max != null) {
119             maxIndex = Integer.parseInt(max);
120             if (maxIndex < minIndex) {
121                 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
122                 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
123             }
124         }
125         final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
126         return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor());
127     }
128 
129     /**
130      * Index for oldest retained log file.
131      */
132     private final int maxIndex;
133 
134     /**
135      * Index for most recent log file.
136      */
137     private final int minIndex;
138     private final boolean useMax;
139     private final StrSubstitutor subst;
140     private final int compressionLevel;
141 
142     /**
143      * Constructs a new instance.
144      * @param minIndex The minimum index.
145      * @param maxIndex The maximum index.
146      */
147     protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, final int compressionLevel, final StrSubstitutor subst) {
148         this.minIndex = minIndex;
149         this.maxIndex = maxIndex;
150         this.useMax = useMax;
151         this.compressionLevel = compressionLevel;
152         this.subst = subst;
153     }
154 
155     public int getCompressionLevel() {
156         return this.compressionLevel;
157     }
158 
159     public int getMaxIndex() {
160         return this.maxIndex;
161     }
162 
163     public int getMinIndex() {
164         return this.minIndex;
165     }
166 
167     private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
168         return useMax ? purgeAscending(lowIndex, highIndex, manager) :
169             purgeDescending(lowIndex, highIndex, manager);
170     }
171 
172     /**
173      * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index,
174      * the newest the highest.
175      *
176      * @param lowIndex  low index
177      * @param highIndex high index.  Log file associated with high index will be deleted if needed.
178      * @param manager The RollingFileManager
179      * @return true if purge was successful and rollover should be attempted.
180      */
181     private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
182         int suffixLength = 0;
183 
184         final List<FileRenameAction> renames = new ArrayList<FileRenameAction>();
185         final StringBuilder buf = new StringBuilder();
186 
187         // LOG4J2-531: directory scan & rollover must use same format
188         manager.getPatternProcessor().formatFileName(subst, buf, highIndex);
189 
190         String highFilename = subst.replace(buf);
191 
192         if (highFilename.endsWith(EXT_GZIP)) {
193             suffixLength = EXT_GZIP.length();
194         } else if (highFilename.endsWith(EXT_ZIP)) {
195             suffixLength = EXT_ZIP.length();
196         }
197 
198         int maxIndex = 0;
199 
200         for (int i = highIndex; i >= lowIndex; i--) {
201             File toRename = new File(highFilename);
202             if (i == highIndex && toRename.exists()) {
203                 maxIndex = highIndex;
204             } else if (maxIndex == 0 && toRename.exists()) {
205                 maxIndex = i + 1;
206                 break;
207             }
208 
209             boolean isBase = false;
210 
211             if (suffixLength > 0) {
212                 final File toRenameBase =
213                     new File(highFilename.substring(0, highFilename.length() - suffixLength));
214 
215                 if (toRename.exists()) {
216                     if (toRenameBase.exists()) {
217                         LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
218                                 toRenameBase, toRename);
219                         toRenameBase.delete();
220                     }
221                 } else {
222                     toRename = toRenameBase;
223                     isBase = true;
224                 }
225             }
226 
227             if (toRename.exists()) {
228                 //
229                 //    if at lower index and then all slots full
230                 //        attempt to delete last file
231                 //        if that fails then abandon purge
232                 if (i == lowIndex) {
233                     LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.", //
234                             toRename, i);
235                     if (!toRename.delete()) {
236                         return -1;
237                     }
238 
239                     break;
240                 }
241 
242                 //
243                 //   if intermediate index
244                 //     add a rename action to the list
245                 buf.setLength(0);
246                 // LOG4J2-531: directory scan & rollover must use same format
247                 manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
248 
249                 final String lowFilename = subst.replace(buf);
250                 String renameTo = lowFilename;
251 
252                 if (isBase) {
253                     renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
254                 }
255 
256                 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
257                 highFilename = lowFilename;
258             } else {
259                 buf.setLength(0);
260                 // LOG4J2-531: directory scan & rollover must use same format
261                 manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
262 
263                 highFilename = subst.replace(buf);
264             }
265         }
266         if (maxIndex == 0) {
267             maxIndex = lowIndex;
268         }
269 
270         //
271         //   work renames backwards
272         //
273         for (int i = renames.size() - 1; i >= 0; i--) {
274             final Action action = renames.get(i);
275             try {
276                 LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
277                         i, renames.size(), action);
278                 if (!action.execute()) {
279                     return -1;
280                 }
281             } catch (final Exception ex) {
282                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
283                 return -1;
284             }
285         }
286         return maxIndex;
287     }
288 
289     /**
290      * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
291      * oldest will have the highest.
292      *
293      * @param lowIndex  low index
294      * @param highIndex high index.  Log file associated with high index will be deleted if needed.
295      * @param manager The RollingFileManager
296      * @return true if purge was successful and rollover should be attempted.
297      */
298     private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
299         int suffixLength = 0;
300 
301         final List<FileRenameAction> renames = new ArrayList<FileRenameAction>();
302         final StringBuilder buf = new StringBuilder();
303 
304         // LOG4J2-531: directory scan & rollover must use same format
305         manager.getPatternProcessor().formatFileName(subst, buf, lowIndex);
306 
307         String lowFilename = subst.replace(buf);
308 
309         if (lowFilename.endsWith(EXT_GZIP)) {
310             suffixLength = EXT_GZIP.length();
311         } else if (lowFilename.endsWith(EXT_ZIP)) {
312             suffixLength = EXT_ZIP.length();
313         }
314 
315         for (int i = lowIndex; i <= highIndex; i++) {
316             File toRename = new File(lowFilename);
317             boolean isBase = false;
318 
319             if (suffixLength > 0) {
320                 final File toRenameBase =
321                     new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
322 
323                 if (toRename.exists()) {
324                     if (toRenameBase.exists()) {
325                         LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
326                                 toRenameBase, toRename);
327                         toRenameBase.delete();
328                     }
329                 } else {
330                     toRename = toRenameBase;
331                     isBase = true;
332                 }
333             }
334 
335             if (toRename.exists()) {
336                 //
337                 //    if at upper index then
338                 //        attempt to delete last file
339                 //        if that fails then abandon purge
340                 if (i == highIndex) {
341                     LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
342                             toRename, i);
343                     if (!toRename.delete()) {
344                         return -1;
345                     }
346 
347                     break;
348                 }
349 
350                 //
351                 //   if intermediate index
352                 //     add a rename action to the list
353                 buf.setLength(0);
354                 // LOG4J2-531: directory scan & rollover must use same format
355                 manager.getPatternProcessor().formatFileName(subst, buf, i + 1);
356 
357                 final String highFilename = subst.replace(buf);
358                 String renameTo = highFilename;
359 
360                 if (isBase) {
361                     renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
362                 }
363 
364                 renames.add(new FileRenameAction(toRename, new File(renameTo), true));
365                 lowFilename = highFilename;
366             } else {
367                 break;
368             }
369         }
370 
371         //
372         //   work renames backwards
373         //
374         for (int i = renames.size() - 1; i >= 0; i--) {
375             final Action action = renames.get(i);
376             try {
377                 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
378                         i, renames.size(), action);
379                 if (!action.execute()) {
380                     return -1;
381                 }
382             } catch (final Exception ex) {
383                 LOGGER.warn("Exception during purge in RollingFileAppender", ex);
384                 return -1;
385             }
386         }
387 
388         return lowIndex;
389     }
390 
391     /**
392      * Perform the rollover.
393      * @param manager The RollingFileManager name for current active log file.
394      * @return A RolloverDescription.
395      * @throws SecurityException if an error occurs.
396      */
397     @Override
398     public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
399         if (maxIndex < 0) {
400             return null;
401         }
402         final long start = System.nanoTime();
403         final int fileIndex = purge(minIndex, maxIndex, manager);
404         if (fileIndex < 0) {
405             return null;
406         }
407         if (LOGGER.isTraceEnabled()) {
408             final double duration = (System.nanoTime() - start) / (1000.0 * 1000.0 * 1000.0);
409             LOGGER.trace("DefaultRolloverStrategy.purge() took {} seconds", duration);
410         }
411         final StringBuilder buf = new StringBuilder(255);
412         manager.getPatternProcessor().formatFileName(subst, buf, fileIndex);
413         final String currentFileName = manager.getFileName();
414 
415         String renameTo = buf.toString();
416         final String compressedName = renameTo;
417         Action compressAction = null;
418 
419         if (renameTo.endsWith(EXT_GZIP)) {
420             renameTo = renameTo.substring(0, renameTo.length() - EXT_GZIP.length());
421             compressAction = new GzCompressAction(new File(renameTo), new File(compressedName), true);
422         } else if (renameTo.endsWith(EXT_ZIP)) {
423             renameTo = renameTo.substring(0, renameTo.length() - EXT_ZIP.length());
424             compressAction = new ZipCompressAction(new File(renameTo), new File(compressedName), true,
425                     compressionLevel);
426         }
427 
428         final FileRenameAction renameAction =
429             new FileRenameAction(new File(currentFileName), new File(renameTo), false);
430 
431         return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction);
432     }
433 
434     @Override
435     public String toString() {
436         return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
437     }
438 
439 }