001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache license, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License. You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the license for the specific language governing permissions and
015     * limitations under the license.
016     */
017    package org.apache.logging.log4j.core.appender.rolling;
018    
019    import org.apache.logging.log4j.Logger;
020    import org.apache.logging.log4j.core.appender.rolling.helper.Action;
021    import org.apache.logging.log4j.core.appender.rolling.helper.FileRenameAction;
022    import org.apache.logging.log4j.core.appender.rolling.helper.GZCompressAction;
023    import org.apache.logging.log4j.core.appender.rolling.helper.ZipCompressAction;
024    import org.apache.logging.log4j.core.config.Configuration;
025    import org.apache.logging.log4j.core.config.plugins.Plugin;
026    import org.apache.logging.log4j.core.config.plugins.PluginAttr;
027    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
028    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
029    import org.apache.logging.log4j.core.lookup.StrSubstitutor;
030    import org.apache.logging.log4j.status.StatusLogger;
031    
032    import java.io.File;
033    import java.util.ArrayList;
034    import java.util.List;
035    
036    /**
037     * When rolling over, <code>DefaultRolloverStrategy</code> renames files
038     * according to an algorithm as described below.
039     * <p/>
040     * <p>The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When
041     * the file name pattern contains a date format then the rollover time interval will be used to calculate the
042     * time to use in the file pattern. When the file pattern contains an integer replacement token one of the
043     * counting techniques will be used.</p>
044     * <p>When the ascending attribute is set to true (the default) then the counter will be incremented and the
045     * current log file will be renamed to include the counter value. If the counter hits the maximum value then
046     * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to
047     * have their counter decremented and then the current file will be renamed to have the maximum counter value.
048     * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.</p>
049     * <p>When the ascending attribute is false, then the "normal" fixed-window strategy will be used.</p>
050     * Let <em>max</em> and <em>min</em> represent the values of respectively
051     * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
052     * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
053     * <b>FileNamePattern</b>. Then, when rolling over, the file
054     * <code>foo.<em>max</em>.log</code> will be deleted, the file
055     * <code>foo.<em>max-1</em>.log</code> will be renamed as
056     * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
057     * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
058     * the file <code>foo.<em>min+1</em>.log</code> renamed as
059     * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
060     * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
061     * <code>foo.log</code> will be created.
062     * <p/>
063     * <p>Given that this rollover algorithm requires as many file renaming
064     * operations as the window size, large window sizes are discouraged.
065     */
066    @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
067    public class DefaultRolloverStrategy implements RolloverStrategy {
068        /**
069         * Allow subclasses access to the status logger without creating another instance.
070         */
071        protected static final Logger LOGGER = StatusLogger.getLogger();
072    
073        private static final int MIN_WINDOW_SIZE = 1;
074        private static final int DEFAULT_WINDOW_SIZE = 7;
075    
076        /**
077         * Index for oldest retained log file.
078         */
079        private final int maxIndex;
080    
081        /**
082         * Index for most recent log file.
083         */
084        private final int minIndex;
085    
086        private final boolean useMax;
087    
088        private final StrSubstitutor subst;
089    
090        /**
091         * Constructs a new instance.
092         * @param min The minimum index.
093         * @param max The maximum index.
094         */
095        protected DefaultRolloverStrategy(final int min, final int max, final boolean useMax, final StrSubstitutor subst) {
096            minIndex = min;
097            maxIndex = max;
098            this.subst = subst;
099            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    }