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", type = "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        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    }