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 java.io.File;
020    import java.util.ArrayList;
021    import java.util.List;
022    import java.util.zip.Deflater;
023    
024    import org.apache.logging.log4j.Logger;
025    import org.apache.logging.log4j.core.appender.rolling.action.Action;
026    import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
027    import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
028    import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
029    import org.apache.logging.log4j.core.config.Configuration;
030    import org.apache.logging.log4j.core.config.plugins.Plugin;
031    import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
032    import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
033    import org.apache.logging.log4j.core.config.plugins.PluginFactory;
034    import org.apache.logging.log4j.core.lookup.StrSubstitutor;
035    import org.apache.logging.log4j.core.util.Integers;
036    import org.apache.logging.log4j.status.StatusLogger;
037    
038    /**
039     * When rolling over, <code>DefaultRolloverStrategy</code> renames files
040     * according to an algorithm as described below.
041     *
042     * <p>
043     * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When
044     * the file name pattern contains a date format then the rollover time interval will be used to calculate the
045     * time to use in the file pattern. When the file pattern contains an integer replacement token one of the
046     * counting techniques will be used.
047     * </p>
048     * <p>
049     * When the ascending attribute is set to true (the default) then the counter will be incremented and the
050     * current log file will be renamed to include the counter value. If the counter hits the maximum value then
051     * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to
052     * have their counter decremented and then the current file will be renamed to have the maximum counter value.
053     * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.
054     * </p>
055     * <p>
056     * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
057     * </p>
058     * <p>
059     * Let <em>max</em> and <em>min</em> represent the values of respectively
060     * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
061     * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
062     * <b>FileNamePattern</b>. Then, when rolling over, the file
063     * <code>foo.<em>max</em>.log</code> will be deleted, the file
064     * <code>foo.<em>max-1</em>.log</code> will be renamed as
065     * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
066     * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
067     * the file <code>foo.<em>min+1</em>.log</code> renamed as
068     * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
069     * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
070     * <code>foo.log</code> will be created.
071     * </p>
072     * <p>
073     * Given that this rollover algorithm requires as many file renaming
074     * operations as the window size, large window sizes are discouraged.
075     * </p>
076     */
077    @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
078    public class DefaultRolloverStrategy implements RolloverStrategy {
079    
080        private static final String EXT_ZIP = ".zip";
081        private static final String EXT_GZIP = ".gz";
082    
083        /**
084         * Allow subclasses access to the status logger without creating another instance.
085         */
086        protected static final Logger LOGGER = StatusLogger.getLogger();
087    
088        private static final int MIN_WINDOW_SIZE = 1;
089        private static final int DEFAULT_WINDOW_SIZE = 7;
090    
091        /**
092         * Create the DefaultRolloverStrategy.
093         * @param max The maximum number of files to keep.
094         * @param min The minimum number of files to keep.
095         * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a
096         * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
097         * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
098         * @param config The Configuration.
099         * @return A DefaultRolloverStrategy.
100         */
101        @PluginFactory
102        public static DefaultRolloverStrategy createStrategy(
103                @PluginAttribute("max") final String max,
104                @PluginAttribute("min") final String min,
105                @PluginAttribute("fileIndex") final String fileIndex,
106                @PluginAttribute("compressionLevel") final String compressionLevelStr,
107                @PluginConfiguration final Configuration config) {
108            final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
109            int minIndex = MIN_WINDOW_SIZE;
110            if (min != null) {
111                minIndex = Integer.parseInt(min);
112                if (minIndex < 1) {
113                    LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
114                    minIndex = MIN_WINDOW_SIZE;
115                }
116            }
117            int maxIndex = DEFAULT_WINDOW_SIZE;
118            if (max != null) {
119                maxIndex = Integer.parseInt(max);
120                if (maxIndex < minIndex) {
121                    maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
122                    LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
123                }
124            }
125            final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
126            return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor());
127        }
128    
129        /**
130         * Index for oldest retained log file.
131         */
132        private final int maxIndex;
133    
134        /**
135         * Index for most recent log file.
136         */
137        private final int minIndex;
138        private final boolean useMax;
139        private final StrSubstitutor subst;
140        private final int compressionLevel;
141    
142        /**
143         * Constructs a new instance.
144         * @param minIndex The minimum index.
145         * @param maxIndex The maximum index.
146         */
147        protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, final int compressionLevel, final StrSubstitutor subst) {
148            this.minIndex = minIndex;
149            this.maxIndex = maxIndex;
150            this.useMax = useMax;
151            this.compressionLevel = compressionLevel;
152            this.subst = subst;
153        }
154    
155        public int getCompressionLevel() {
156            return this.compressionLevel;
157        }
158    
159        public int getMaxIndex() {
160            return this.maxIndex;
161        }
162    
163        public int getMinIndex() {
164            return this.minIndex;
165        }
166    
167        private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
168            return useMax ? purgeAscending(lowIndex, highIndex, manager) :
169                purgeDescending(lowIndex, highIndex, manager);
170        }
171    
172        /**
173         * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index,
174         * the newest the highest.
175         *
176         * @param lowIndex  low index
177         * @param highIndex high index.  Log file associated with high index will be deleted if needed.
178         * @param manager The RollingFileManager
179         * @return true if purge was successful and rollover should be attempted.
180         */
181        private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
182            int suffixLength = 0;
183    
184            final List<FileRenameAction> renames = new ArrayList<FileRenameAction>();
185            final StringBuilder buf = new StringBuilder();
186    
187            // LOG4J2-531: directory scan & rollover must use same format
188            manager.getPatternProcessor().formatFileName(subst, buf, highIndex);
189    
190            String highFilename = subst.replace(buf);
191    
192            if (highFilename.endsWith(EXT_GZIP)) {
193                suffixLength = EXT_GZIP.length();
194            } else if (highFilename.endsWith(EXT_ZIP)) {
195                suffixLength = EXT_ZIP.length();
196            }
197    
198            int maxIndex = 0;
199    
200            for (int i = highIndex; i >= lowIndex; i--) {
201                File toRename = new File(highFilename);
202                if (i == highIndex && toRename.exists()) {
203                    maxIndex = highIndex;
204                } else if (maxIndex == 0 && toRename.exists()) {
205                    maxIndex = i + 1;
206                    break;
207                }
208    
209                boolean isBase = false;
210    
211                if (suffixLength > 0) {
212                    final File toRenameBase =
213                        new File(highFilename.substring(0, highFilename.length() - suffixLength));
214    
215                    if (toRename.exists()) {
216                        if (toRenameBase.exists()) {
217                            LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} base of {}.", //
218                                    toRenameBase, toRename);
219                            toRenameBase.delete();
220                        }
221                    } else {
222                        toRename = toRenameBase;
223                        isBase = true;
224                    }
225                }
226    
227                if (toRename.exists()) {
228                    //
229                    //    if at lower index and then all slots full
230                    //        attempt to delete last file
231                    //        if that fails then abandon purge
232                    if (i == lowIndex) {
233                        LOGGER.debug("DefaultRolloverStrategy.purgeAscending deleting {} at low index {}: all slots full.", //
234                                toRename, i);
235                        if (!toRename.delete()) {
236                            return -1;
237                        }
238    
239                        break;
240                    }
241    
242                    //
243                    //   if intermediate index
244                    //     add a rename action to the list
245                    buf.setLength(0);
246                    // LOG4J2-531: directory scan & rollover must use same format
247                    manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
248    
249                    final String lowFilename = subst.replace(buf);
250                    String renameTo = lowFilename;
251    
252                    if (isBase) {
253                        renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength);
254                    }
255    
256                    renames.add(new FileRenameAction(toRename, new File(renameTo), true));
257                    highFilename = lowFilename;
258                } else {
259                    buf.setLength(0);
260                    // LOG4J2-531: directory scan & rollover must use same format
261                    manager.getPatternProcessor().formatFileName(subst, buf, i - 1);
262    
263                    highFilename = subst.replace(buf);
264                }
265            }
266            if (maxIndex == 0) {
267                maxIndex = lowIndex;
268            }
269    
270            //
271            //   work renames backwards
272            //
273            for (int i = renames.size() - 1; i >= 0; i--) {
274                final Action action = renames.get(i);
275                try {
276                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {} of {}: {}", //
277                            i, renames.size(), action);
278                    if (!action.execute()) {
279                        return -1;
280                    }
281                } catch (final Exception ex) {
282                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
283                    return -1;
284                }
285            }
286            return maxIndex;
287        }
288    
289        /**
290         * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the
291         * oldest will have the highest.
292         *
293         * @param lowIndex  low index
294         * @param highIndex high index.  Log file associated with high index will be deleted if needed.
295         * @param manager The RollingFileManager
296         * @return true if purge was successful and rollover should be attempted.
297         */
298        private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
299            int suffixLength = 0;
300    
301            final List<FileRenameAction> renames = new ArrayList<FileRenameAction>();
302            final StringBuilder buf = new StringBuilder();
303    
304            // LOG4J2-531: directory scan & rollover must use same format
305            manager.getPatternProcessor().formatFileName(subst, buf, lowIndex);
306    
307            String lowFilename = subst.replace(buf);
308    
309            if (lowFilename.endsWith(EXT_GZIP)) {
310                suffixLength = EXT_GZIP.length();
311            } else if (lowFilename.endsWith(EXT_ZIP)) {
312                suffixLength = EXT_ZIP.length();
313            }
314    
315            for (int i = lowIndex; i <= highIndex; i++) {
316                File toRename = new File(lowFilename);
317                boolean isBase = false;
318    
319                if (suffixLength > 0) {
320                    final File toRenameBase =
321                        new File(lowFilename.substring(0, lowFilename.length() - suffixLength));
322    
323                    if (toRename.exists()) {
324                        if (toRenameBase.exists()) {
325                            LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} base of {}.", //
326                                    toRenameBase, toRename);
327                            toRenameBase.delete();
328                        }
329                    } else {
330                        toRename = toRenameBase;
331                        isBase = true;
332                    }
333                }
334    
335                if (toRename.exists()) {
336                    //
337                    //    if at upper index then
338                    //        attempt to delete last file
339                    //        if that fails then abandon purge
340                    if (i == highIndex) {
341                        LOGGER.debug("DefaultRolloverStrategy.purgeDescending deleting {} at high index {}: all slots full.", //
342                                toRename, i);
343                        if (!toRename.delete()) {
344                            return -1;
345                        }
346    
347                        break;
348                    }
349    
350                    //
351                    //   if intermediate index
352                    //     add a rename action to the list
353                    buf.setLength(0);
354                    // LOG4J2-531: directory scan & rollover must use same format
355                    manager.getPatternProcessor().formatFileName(subst, buf, i + 1);
356    
357                    final String highFilename = subst.replace(buf);
358                    String renameTo = highFilename;
359    
360                    if (isBase) {
361                        renameTo = highFilename.substring(0, highFilename.length() - suffixLength);
362                    }
363    
364                    renames.add(new FileRenameAction(toRename, new File(renameTo), true));
365                    lowFilename = highFilename;
366                } else {
367                    break;
368                }
369            }
370    
371            //
372            //   work renames backwards
373            //
374            for (int i = renames.size() - 1; i >= 0; i--) {
375                final Action action = renames.get(i);
376                try {
377                    LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {} of {}: {}", //
378                            i, renames.size(), action);
379                    if (!action.execute()) {
380                        return -1;
381                    }
382                } catch (final Exception ex) {
383                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
384                    return -1;
385                }
386            }
387    
388            return lowIndex;
389        }
390    
391        /**
392         * Perform the rollover.
393         * @param manager The RollingFileManager name for current active log file.
394         * @return A RolloverDescription.
395         * @throws SecurityException if an error occurs.
396         */
397        @Override
398        public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
399            if (maxIndex < 0) {
400                return null;
401            }
402            final long start = System.nanoTime();
403            final int fileIndex = purge(minIndex, maxIndex, manager);
404            if (fileIndex < 0) {
405                return null;
406            }
407            if (LOGGER.isTraceEnabled()) {
408                final double duration = (System.nanoTime() - start) / (1000.0 * 1000.0 * 1000.0);
409                LOGGER.trace("DefaultRolloverStrategy.purge() took {} seconds", duration);
410            }
411            final StringBuilder buf = new StringBuilder(255);
412            manager.getPatternProcessor().formatFileName(subst, buf, fileIndex);
413            final String currentFileName = manager.getFileName();
414    
415            String renameTo = buf.toString();
416            final String compressedName = renameTo;
417            Action compressAction = null;
418    
419            if (renameTo.endsWith(EXT_GZIP)) {
420                renameTo = renameTo.substring(0, renameTo.length() - EXT_GZIP.length());
421                compressAction = new GzCompressAction(new File(renameTo), new File(compressedName), true);
422            } else if (renameTo.endsWith(EXT_ZIP)) {
423                renameTo = renameTo.substring(0, renameTo.length() - EXT_ZIP.length());
424                compressAction = new ZipCompressAction(new File(renameTo), new File(compressedName), true,
425                        compressionLevel);
426            }
427    
428            final FileRenameAction renameAction =
429                new FileRenameAction(new File(currentFileName), new File(renameTo), false);
430    
431            return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction);
432        }
433    
434        @Override
435        public String toString() {
436            return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ')';
437        }
438    
439    }