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