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