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.io.IOException; 021import java.nio.file.Files; 022import java.nio.file.Path; 023import java.util.Arrays; 024import java.util.Collections; 025import java.util.List; 026import java.util.Map; 027import java.util.SortedMap; 028import java.util.concurrent.TimeUnit; 029import java.util.zip.Deflater; 030 031import org.apache.logging.log4j.core.Core; 032import org.apache.logging.log4j.core.appender.rolling.action.Action; 033import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; 034import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction; 035import org.apache.logging.log4j.core.appender.rolling.action.PathCondition; 036import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction; 037import org.apache.logging.log4j.core.config.Configuration; 038import org.apache.logging.log4j.core.config.plugins.Plugin; 039import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 040import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; 041import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; 042import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; 043import org.apache.logging.log4j.core.config.plugins.PluginElement; 044import org.apache.logging.log4j.core.config.plugins.PluginFactory; 045import org.apache.logging.log4j.core.lookup.StrSubstitutor; 046import org.apache.logging.log4j.core.util.Integers; 047 048/** 049 * When rolling over, <code>DefaultRolloverStrategy</code> renames files according to an algorithm as described below. 050 * 051 * <p> 052 * The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When the file name 053 * pattern contains a date format then the rollover time interval will be used to calculate the time to use in the file 054 * pattern. When the file pattern contains an integer replacement token one of the counting techniques will be used. 055 * </p> 056 * <p> 057 * When the ascending attribute is set to true (the default) then the counter will be incremented and the current log 058 * file will be renamed to include the counter value. If the counter hits the maximum value then the oldest file, which 059 * will have the smallest counter, will be deleted, all other files will be renamed to have their counter decremented 060 * and then the current file will be renamed to have the maximum counter value. Note that with this counting strategy 061 * specifying a large maximum value may entirely avoid renaming files. 062 * </p> 063 * <p> 064 * When the ascending attribute is false, then the "normal" fixed-window strategy will be used. 065 * </p> 066 * <p> 067 * Let <em>max</em> and <em>min</em> represent the values of respectively the <b>MaxIndex</b> and <b>MinIndex</b> 068 * options. Let "foo.log" be the value of the <b>ActiveFile</b> option and "foo.%i.log" the value of 069 * <b>FileNamePattern</b>. Then, when rolling over, the file <code>foo.<em>max</em>.log</code> will be deleted, the file 070 * <code>foo.<em>max-1</em>.log</code> will be renamed as <code>foo.<em>max</em>.log</code>, the file 071 * <code>foo.<em>max-2</em>.log</code> renamed as <code>foo.<em>max-1</em>.log</code>, and so on, the file 072 * <code>foo.<em>min+1</em>.log</code> renamed as <code>foo.<em>min+2</em>.log</code>. Lastly, the active file 073 * <code>foo.log</code> will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name 074 * <code>foo.log</code> will be created. 075 * </p> 076 * <p> 077 * Given that this rollover algorithm requires as many file renaming operations as the window size, large window sizes 078 * are discouraged. 079 * </p> 080 */ 081@Plugin(name = "DefaultRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true) 082public class DefaultRolloverStrategy extends AbstractRolloverStrategy { 083 084 private static final int MIN_WINDOW_SIZE = 1; 085 private static final int DEFAULT_WINDOW_SIZE = 7; 086 087 /** 088 * Builds DefaultRolloverStrategy instances. 089 */ 090 public static class Builder implements org.apache.logging.log4j.core.util.Builder<DefaultRolloverStrategy> { 091 @PluginBuilderAttribute("max") 092 private String max; 093 094 @PluginBuilderAttribute("min") 095 private String min; 096 097 @PluginBuilderAttribute("fileIndex") 098 private String fileIndex; 099 100 @PluginBuilderAttribute("compressionLevel") 101 private String compressionLevelStr; 102 103 @PluginElement("Actions") 104 private Action[] customActions; 105 106 @PluginBuilderAttribute(value = "stopCustomActionsOnError") 107 private boolean stopCustomActionsOnError = true; 108 109 @PluginBuilderAttribute(value = "tempCompressedFilePattern") 110 private String tempCompressedFilePattern; 111 112 @PluginConfiguration 113 private Configuration config; 114 115 @Override 116 public DefaultRolloverStrategy build() { 117 int minIndex; 118 int maxIndex; 119 boolean useMax; 120 121 if (fileIndex != null && fileIndex.equalsIgnoreCase("nomax")) { 122 minIndex = Integer.MIN_VALUE; 123 maxIndex = Integer.MAX_VALUE; 124 useMax = false; 125 } else { 126 useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max"); 127 minIndex = MIN_WINDOW_SIZE; 128 if (min != null) { 129 minIndex = Integer.parseInt(min); 130 if (minIndex < 1) { 131 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE); 132 minIndex = MIN_WINDOW_SIZE; 133 } 134 } 135 maxIndex = DEFAULT_WINDOW_SIZE; 136 if (max != null) { 137 maxIndex = Integer.parseInt(max); 138 if (maxIndex < minIndex) { 139 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex; 140 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex); 141 } 142 } 143 } 144 final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION); 145 return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor(), 146 customActions, stopCustomActionsOnError, tempCompressedFilePattern); 147 } 148 149 public String getMax() { 150 return max; 151 } 152 153 /** 154 * Defines the maximum number of files to keep. 155 * 156 * @param max The maximum number of files to keep. 157 * @return This builder for chaining convenience 158 */ 159 public Builder withMax(final String max) { 160 this.max = max; 161 return this; 162 } 163 164 public String getMin() { 165 return min; 166 } 167 168 /** 169 * Defines the minimum number of files to keep. 170 * 171 * @param min The minimum number of files to keep. 172 * @return This builder for chaining convenience 173 */ 174 public Builder withMin(final String min) { 175 this.min = min; 176 return this; 177 } 178 179 public String getFileIndex() { 180 return fileIndex; 181 } 182 183 /** 184 * Defines the file index for rolling strategy. 185 * 186 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller 187 * index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 188 * @return This builder for chaining convenience 189 */ 190 public Builder withFileIndex(final String fileIndex) { 191 this.fileIndex = fileIndex; 192 return this; 193 } 194 195 public String getCompressionLevelStr() { 196 return compressionLevelStr; 197 } 198 199 /** 200 * Defines compression level. 201 * 202 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 203 * @return This builder for chaining convenience 204 */ 205 public Builder withCompressionLevelStr(final String compressionLevelStr) { 206 this.compressionLevelStr = compressionLevelStr; 207 return this; 208 } 209 210 public Action[] getCustomActions() { 211 return customActions; 212 } 213 214 /** 215 * Defines custom actions. 216 * 217 * @param customActions custom actions to perform asynchronously after rollover 218 * @return This builder for chaining convenience 219 */ 220 public Builder withCustomActions(final Action[] customActions) { 221 this.customActions = customActions; 222 return this; 223 } 224 225 public boolean isStopCustomActionsOnError() { 226 return stopCustomActionsOnError; 227 } 228 229 /** 230 * Defines whether to stop executing asynchronous actions if an error occurs. 231 * 232 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 233 * @return This builder for chaining convenience 234 */ 235 public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) { 236 this.stopCustomActionsOnError = stopCustomActionsOnError; 237 return this; 238 } 239 240 public String getTempCompressedFilePattern() { 241 return tempCompressedFilePattern; 242 } 243 244 /** 245 * Defines temporary compression file pattern. 246 * 247 * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used 248 * @return This builder for chaining convenience 249 */ 250 public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) { 251 this.tempCompressedFilePattern = tempCompressedFilePattern; 252 return this; 253 } 254 255 public Configuration getConfig() { 256 return config; 257 } 258 259 /** 260 * Defines configuration. 261 * 262 * @param config The Configuration. 263 * @return This builder for chaining convenience 264 */ 265 public Builder withConfig(final Configuration config) { 266 this.config = config; 267 return this; 268 } 269 } 270 271 @PluginBuilderFactory 272 public static Builder newBuilder() { 273 return new Builder(); 274 } 275 276 /** 277 * Creates the DefaultRolloverStrategy. 278 * 279 * @param max The maximum number of files to keep. 280 * @param min The minimum number of files to keep. 281 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller 282 * index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 283 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 284 * @param customActions custom actions to perform asynchronously after rollover 285 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 286 * @param config The Configuration. 287 * @return A DefaultRolloverStrategy. 288 * @deprecated Since 2.9 Usage of Builder API is preferable 289 */ 290 @PluginFactory 291 @Deprecated 292 public static DefaultRolloverStrategy createStrategy( 293 // @formatter:off 294 @PluginAttribute("max") final String max, 295 @PluginAttribute("min") final String min, 296 @PluginAttribute("fileIndex") final String fileIndex, 297 @PluginAttribute("compressionLevel") final String compressionLevelStr, 298 @PluginElement("Actions") final Action[] customActions, 299 @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true) 300 final boolean stopCustomActionsOnError, 301 @PluginConfiguration final Configuration config) { 302 return DefaultRolloverStrategy.newBuilder() 303 .withMin(min) 304 .withMax(max) 305 .withFileIndex(fileIndex) 306 .withCompressionLevelStr(compressionLevelStr) 307 .withCustomActions(customActions) 308 .withStopCustomActionsOnError(stopCustomActionsOnError) 309 .withConfig(config) 310 .build(); 311 // @formatter:on 312 } 313 314 /** 315 * Index for oldest retained log file. 316 */ 317 private final int maxIndex; 318 319 /** 320 * Index for most recent log file. 321 */ 322 private final int minIndex; 323 private final boolean useMax; 324 private final int compressionLevel; 325 private final List<Action> customActions; 326 private final boolean stopCustomActionsOnError; 327 private final PatternProcessor tempCompressedFilePattern; 328 329 /** 330 * Constructs a new instance. 331 * 332 * @param minIndex The minimum index. 333 * @param maxIndex The maximum index. 334 * @param customActions custom actions to perform asynchronously after rollover 335 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 336 * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter 337 */ 338 @Deprecated 339 protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, 340 final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions, 341 final boolean stopCustomActionsOnError) { 342 this(minIndex, maxIndex, useMax, compressionLevel, 343 strSubstitutor, customActions, stopCustomActionsOnError, null); 344 } 345 346 /** 347 * Constructs a new instance. 348 * 349 * @param minIndex The minimum index. 350 * @param maxIndex The maximum index. 351 * @param customActions custom actions to perform asynchronously after rollover 352 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 353 * @param tempCompressedFilePatternString File pattern of the working file 354 * used during compression, if null no temporary file are used 355 */ 356 protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, 357 final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions, 358 final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) { 359 super(strSubstitutor); 360 this.minIndex = minIndex; 361 this.maxIndex = maxIndex; 362 this.useMax = useMax; 363 this.compressionLevel = compressionLevel; 364 this.stopCustomActionsOnError = stopCustomActionsOnError; 365 this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions); 366 this.tempCompressedFilePattern = 367 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; 368 } 369 370 public int getCompressionLevel() { 371 return this.compressionLevel; 372 } 373 374 public List<Action> getCustomActions() { 375 return customActions; 376 } 377 378 public int getMaxIndex() { 379 return this.maxIndex; 380 } 381 382 public int getMinIndex() { 383 return this.minIndex; 384 } 385 386 public boolean isStopCustomActionsOnError() { 387 return stopCustomActionsOnError; 388 } 389 390 public boolean isUseMax() { 391 return useMax; 392 } 393 394 public PatternProcessor getTempCompressedFilePattern() { 395 return tempCompressedFilePattern; 396 } 397 398 private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) { 399 return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager); 400 } 401 402 /** 403 * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the 404 * newest the highest. 405 * 406 * @param lowIndex low index. Log file associated with low index will be deleted if needed. 407 * @param highIndex high index. 408 * @param manager The RollingFileManager 409 * @return true if purge was successful and rollover should be attempted. 410 */ 411 private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 412 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 413 final int maxFiles = highIndex - lowIndex + 1; 414 415 boolean renameFiles = false; 416 while (eligibleFiles.size() >= maxFiles) { 417 try { 418 LOGGER.debug("Eligible files: {}", eligibleFiles); 419 final Integer key = eligibleFiles.firstKey(); 420 LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath()); 421 Files.delete(eligibleFiles.get(key)); 422 eligibleFiles.remove(key); 423 renameFiles = true; 424 } catch (final IOException ioe) { 425 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe); 426 break; 427 } 428 } 429 final StringBuilder buf = new StringBuilder(); 430 if (renameFiles) { 431 for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) { 432 buf.setLength(0); 433 // LOG4J2-531: directory scan & rollover must use same format 434 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1); 435 final String currentName = entry.getValue().toFile().getName(); 436 String renameTo = buf.toString(); 437 final int suffixLength = suffixLength(renameTo); 438 if (suffixLength > 0 && suffixLength(currentName) == 0) { 439 renameTo = renameTo.substring(0, renameTo.length() - suffixLength); 440 } 441 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true); 442 try { 443 LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action); 444 if (!action.execute()) { 445 return -1; 446 } 447 } catch (final Exception ex) { 448 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 449 return -1; 450 } 451 } 452 } 453 454 return eligibleFiles.size() > 0 ? 455 (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex; 456 } 457 458 /** 459 * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the 460 * oldest will have the highest. 461 * 462 * @param lowIndex low index 463 * @param highIndex high index. Log file associated with high index will be deleted if needed. 464 * @param manager The RollingFileManager 465 * @return true if purge was successful and rollover should be attempted. 466 */ 467 private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 468 // Retrieve the files in descending order, so the highest key will be first. 469 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false); 470 final int maxFiles = highIndex - lowIndex + 1; 471 472 while (eligibleFiles.size() >= maxFiles) { 473 try { 474 final Integer key = eligibleFiles.firstKey(); 475 Files.delete(eligibleFiles.get(key)); 476 eligibleFiles.remove(key); 477 } catch (final IOException ioe) { 478 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe); 479 break; 480 } 481 } 482 final StringBuilder buf = new StringBuilder(); 483 for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) { 484 buf.setLength(0); 485 // LOG4J2-531: directory scan & rollover must use same format 486 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1); 487 final String currentName = entry.getValue().toFile().getName(); 488 String renameTo = buf.toString(); 489 final int suffixLength = suffixLength(renameTo); 490 if (suffixLength > 0 && suffixLength(currentName) == 0) { 491 renameTo = renameTo.substring(0, renameTo.length() - suffixLength); 492 } 493 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true); 494 try { 495 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action); 496 if (!action.execute()) { 497 return -1; 498 } 499 } catch (final Exception ex) { 500 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 501 return -1; 502 } 503 } 504 505 return lowIndex; 506 } 507 508 /** 509 * Performs the rollover. 510 * 511 * @param manager The RollingFileManager name for current active log file. 512 * @return A RolloverDescription. 513 * @throws SecurityException if an error occurs. 514 */ 515 @Override 516 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 517 int fileIndex; 518 if (minIndex == Integer.MIN_VALUE) { 519 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 520 fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1; 521 } else { 522 if (maxIndex < 0) { 523 return null; 524 } 525 final long startNanos = System.nanoTime(); 526 fileIndex = purge(minIndex, maxIndex, manager); 527 if (fileIndex < 0) { 528 return null; 529 } 530 if (LOGGER.isTraceEnabled()) { 531 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); 532 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis); 533 } 534 } 535 final StringBuilder buf = new StringBuilder(255); 536 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex); 537 final String currentFileName = manager.getFileName(); 538 539 String renameTo = buf.toString(); 540 final String compressedName = renameTo; 541 Action compressAction = null; 542 543 final FileExtension fileExtension = manager.getFileExtension(); 544 if (fileExtension != null) { 545 final File renameToFile = new File(renameTo); 546 renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length()); 547 if (tempCompressedFilePattern != null) { 548 buf.delete(0, buf.length()); 549 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex); 550 final String tmpCompressedName = buf.toString(); 551 final File tmpCompressedNameFile = new File(tmpCompressedName); 552 final File parentFile = tmpCompressedNameFile.getParentFile(); 553 if (parentFile != null) { 554 parentFile.mkdirs(); 555 } 556 compressAction = new CompositeAction( 557 Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName, 558 true, compressionLevel), 559 new FileRenameAction(tmpCompressedNameFile, 560 renameToFile, true)), 561 true); 562 } else { 563 compressAction = fileExtension.createCompressAction(renameTo, compressedName, 564 true, compressionLevel); 565 } 566 } 567 568 if (currentFileName.equals(renameTo)) { 569 LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName); 570 return new RolloverDescriptionImpl(currentFileName, false, null, null); 571 } 572 573 if (compressAction != null && manager.isAttributeViewEnabled()) { 574 // Propagate posix attribute view to compressed file 575 // @formatter:off 576 final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder() 577 .withBasePath(compressedName) 578 .withFollowLinks(false) 579 .withMaxDepth(1) 580 .withPathConditions(new PathCondition[0]) 581 .withSubst(getStrSubstitutor()) 582 .withFilePermissions(manager.getFilePermissions()) 583 .withFileOwner(manager.getFileOwner()) 584 .withFileGroup(manager.getFileGroup()) 585 .build(); 586 // @formatter:on 587 compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false); 588 } 589 590 final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), 591 manager.isRenameEmptyFiles()); 592 593 final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); 594 return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); 595 } 596 597 @Override 598 public String toString() { 599 return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")"; 600 } 601 602}