View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements. See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache license, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License. You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the license for the specific language governing permissions and
    15   * limitations under the license.
    16   */
    17  package org.apache.logging.log4j.core.appender.rolling;
    18
    19  import java.io.File;
    20  import java.util.ArrayList;
    21  import java.util.List;
    22  import java.util.zip.Deflater;
    23
    24  import org.apache.logging.log4j.Logger;
    25  import org.apache.logging.log4j.core.appender.rolling.action.Action;
    26  import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction;
    27  import org.apache.logging.log4j.core.appender.rolling.action.GzCompressAction;
    28  import org.apache.logging.log4j.core.appender.rolling.action.ZipCompressAction;
    29  import org.apache.logging.log4j.core.config.Configuration;
    30  import org.apache.logging.log4j.core.config.plugins.Plugin;
    31  import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
    32  import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
    33  import org.apache.logging.log4j.core.config.plugins.PluginFactory;
    34  import org.apache.logging.log4j.core.lookup.StrSubstitutor;
    35  import org.apache.logging.log4j.core.util.Integers;
    36  import org.apache.logging.log4j.status.StatusLogger;
    37
    38  /**
    39   * When rolling over, <code>DefaultRolloverStrategy</code> renames files
    40   * according to an algorithm as described below.
    41   *
    42   * <p>
    43   * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When
    44   * the file name pattern contains a date format then the rollover time interval will be used to calculate the
    45   * time to use in the file pattern. When the file pattern contains an integer replacement token one of the
    46   * counting techniques will be used.
    47   * </p>
    48   * <p>
    49   * When the ascending attribute is set to true (the default) then the counter will be incremented and the
    50   * current log file will be renamed to include the counter value. If the counter hits the maximum value then
    51   * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to
    52   * have their counter decremented and then the current file will be renamed to have the maximum counter value.
    53   * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.
    54   * </p>
    55   * <p>
    56   * When the ascending attribute is false, then the "normal" fixed-window strategy will be used.
    57   * </p>
    58   * <p>
    59   * Let <em>max</em> and <em>min</em> represent the values of respectively
    60   * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value
    61   * of the <b>ActiveFile</b> option and "foo.%i.log" the value of
    62   * <b>FileNamePattern</b>. Then, when rolling over, the file
    63   * <code>foo.<em>max</em>.log</code> will be deleted, the file
    64   * <code>foo.<em>max-1</em>.log</code> will be renamed as
    65   * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code>
    66   * renamed as <code>foo.<em>max-1</em>.log</code>, and so on,
    67   * the file <code>foo.<em>min+1</em>.log</code> renamed as
    68   * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code>
    69   * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name
    70   * <code>foo.log</code> will be created.
    71   * </p>
    72   * <p>
    73   * Given that this rollover algorithm requires as many file renaming
    74   * operations as the window size, large window sizes are discouraged.
    75   * </p>
    76   */
    77  @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true)
    78  public class DefaultRolloverStrategy implements RolloverStrategy {
    79
    80      private static final String EXT_ZIP = ".zip";
    81      private static final String EXT_GZIP = ".gz";
    82
    83      /**
    84       * Allow subclasses access to the status logger without creating another instance.
    85       */
    86      protected static final Logger LOGGER = StatusLogger.getLogger();
    87
    88      private static final int MIN_WINDOW_SIZE = 1;
    89      private static final int DEFAULT_WINDOW_SIZE = 7;
    90
    91      /**
    92       * Create the DefaultRolloverStrategy.
    93       * @param max The maximum number of files to keep.
    94       * @param min The minimum number of files to keep.
    95       * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a
    96       * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy.
    97       * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files.
    98       * @param config The Configuration.
    99       * @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 }