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