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.lookup.StrSubstitutor; 035import org.apache.logging.log4j.core.util.Integers; 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 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}