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