    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    }