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