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 // The config object can be null when this object is built programmatically. 146 final StrSubstitutor nonNullStrSubstitutor = config != null ? config.getStrSubstitutor() : new StrSubstitutor(); 147 return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, nonNullStrSubstitutor, 148 customActions, stopCustomActionsOnError, tempCompressedFilePattern); 149 } 150 151 public String getMax() { 152 return max; 153 } 154 155 /** 156 * Defines the maximum number of files to keep. 157 * 158 * @param max The maximum number of files to keep. 159 * @return This builder for chaining convenience 160 */ 161 public Builder withMax(final String max) { 162 this.max = max; 163 return this; 164 } 165 166 public String getMin() { 167 return min; 168 } 169 170 /** 171 * Defines the minimum number of files to keep. 172 * 173 * @param min The minimum number of files to keep. 174 * @return This builder for chaining convenience 175 */ 176 public Builder withMin(final String min) { 177 this.min = min; 178 return this; 179 } 180 181 public String getFileIndex() { 182 return fileIndex; 183 } 184 185 /** 186 * Defines the file index for rolling strategy. 187 * 188 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller 189 * index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 190 * @return This builder for chaining convenience 191 */ 192 public Builder withFileIndex(final String fileIndex) { 193 this.fileIndex = fileIndex; 194 return this; 195 } 196 197 public String getCompressionLevelStr() { 198 return compressionLevelStr; 199 } 200 201 /** 202 * Defines compression level. 203 * 204 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 205 * @return This builder for chaining convenience 206 */ 207 public Builder withCompressionLevelStr(final String compressionLevelStr) { 208 this.compressionLevelStr = compressionLevelStr; 209 return this; 210 } 211 212 public Action[] getCustomActions() { 213 return customActions; 214 } 215 216 /** 217 * Defines custom actions. 218 * 219 * @param customActions custom actions to perform asynchronously after rollover 220 * @return This builder for chaining convenience 221 */ 222 public Builder withCustomActions(final Action[] customActions) { 223 this.customActions = customActions; 224 return this; 225 } 226 227 public boolean isStopCustomActionsOnError() { 228 return stopCustomActionsOnError; 229 } 230 231 /** 232 * Defines whether to stop executing asynchronous actions if an error occurs. 233 * 234 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 235 * @return This builder for chaining convenience 236 */ 237 public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) { 238 this.stopCustomActionsOnError = stopCustomActionsOnError; 239 return this; 240 } 241 242 public String getTempCompressedFilePattern() { 243 return tempCompressedFilePattern; 244 } 245 246 /** 247 * Defines temporary compression file pattern. 248 * 249 * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used 250 * @return This builder for chaining convenience 251 */ 252 public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) { 253 this.tempCompressedFilePattern = tempCompressedFilePattern; 254 return this; 255 } 256 257 public Configuration getConfig() { 258 return config; 259 } 260 261 /** 262 * Defines configuration. 263 * 264 * @param config The Configuration. 265 * @return This builder for chaining convenience 266 */ 267 public Builder withConfig(final Configuration config) { 268 this.config = config; 269 return this; 270 } 271 } 272 273 @PluginBuilderFactory 274 public static Builder newBuilder() { 275 return new Builder(); 276 } 277 278 /** 279 * Creates the DefaultRolloverStrategy. 280 * 281 * @param max The maximum number of files to keep. 282 * @param min The minimum number of files to keep. 283 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a smaller 284 * index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 285 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 286 * @param customActions custom actions to perform asynchronously after rollover 287 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 288 * @param config The Configuration. 289 * @return A DefaultRolloverStrategy. 290 * @deprecated Since 2.9 Usage of Builder API is preferable 291 */ 292 @PluginFactory 293 @Deprecated 294 public static DefaultRolloverStrategy createStrategy( 295 // @formatter:off 296 @PluginAttribute("max") final String max, 297 @PluginAttribute("min") final String min, 298 @PluginAttribute("fileIndex") final String fileIndex, 299 @PluginAttribute("compressionLevel") final String compressionLevelStr, 300 @PluginElement("Actions") final Action[] customActions, 301 @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true) 302 final boolean stopCustomActionsOnError, 303 @PluginConfiguration final Configuration config) { 304 return DefaultRolloverStrategy.newBuilder() 305 .withMin(min) 306 .withMax(max) 307 .withFileIndex(fileIndex) 308 .withCompressionLevelStr(compressionLevelStr) 309 .withCustomActions(customActions) 310 .withStopCustomActionsOnError(stopCustomActionsOnError) 311 .withConfig(config) 312 .build(); 313 // @formatter:on 314 } 315 316 /** 317 * Index for oldest retained log file. 318 */ 319 private final int maxIndex; 320 321 /** 322 * Index for most recent log file. 323 */ 324 private final int minIndex; 325 private final boolean useMax; 326 private final int compressionLevel; 327 private final List<Action> customActions; 328 private final boolean stopCustomActionsOnError; 329 private final PatternProcessor tempCompressedFilePattern; 330 331 /** 332 * Constructs a new instance. 333 * 334 * @param minIndex The minimum index. 335 * @param maxIndex The maximum index. 336 * @param customActions custom actions to perform asynchronously after rollover 337 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 338 * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter 339 */ 340 @Deprecated 341 protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, 342 final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions, 343 final boolean stopCustomActionsOnError) { 344 this(minIndex, maxIndex, useMax, compressionLevel, 345 strSubstitutor, customActions, stopCustomActionsOnError, null); 346 } 347 348 /** 349 * Constructs a new instance. 350 * 351 * @param minIndex The minimum index. 352 * @param maxIndex The maximum index. 353 * @param customActions custom actions to perform asynchronously after rollover 354 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 355 * @param tempCompressedFilePatternString File pattern of the working file 356 * used during compression, if null no temporary file are used 357 */ 358 protected DefaultRolloverStrategy(final int minIndex, final int maxIndex, final boolean useMax, 359 final int compressionLevel, final StrSubstitutor strSubstitutor, final Action[] customActions, 360 final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) { 361 super(strSubstitutor); 362 this.minIndex = minIndex; 363 this.maxIndex = maxIndex; 364 this.useMax = useMax; 365 this.compressionLevel = compressionLevel; 366 this.stopCustomActionsOnError = stopCustomActionsOnError; 367 this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions); 368 this.tempCompressedFilePattern = 369 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; 370 } 371 372 public int getCompressionLevel() { 373 return this.compressionLevel; 374 } 375 376 public List<Action> getCustomActions() { 377 return customActions; 378 } 379 380 public int getMaxIndex() { 381 return this.maxIndex; 382 } 383 384 public int getMinIndex() { 385 return this.minIndex; 386 } 387 388 public boolean isStopCustomActionsOnError() { 389 return stopCustomActionsOnError; 390 } 391 392 public boolean isUseMax() { 393 return useMax; 394 } 395 396 public PatternProcessor getTempCompressedFilePattern() { 397 return tempCompressedFilePattern; 398 } 399 400 private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) { 401 return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager); 402 } 403 404 /** 405 * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the 406 * newest the highest. 407 * 408 * @param lowIndex low index. Log file associated with low index will be deleted if needed. 409 * @param highIndex high index. 410 * @param manager The RollingFileManager 411 * @return true if purge was successful and rollover should be attempted. 412 */ 413 private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 414 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 415 final int maxFiles = highIndex - lowIndex + 1; 416 417 boolean renameFiles = !eligibleFiles.isEmpty() && eligibleFiles.lastKey() >= maxIndex; 418 while (eligibleFiles.size() >= maxFiles) { 419 try { 420 LOGGER.debug("Eligible files: {}", eligibleFiles); 421 final Integer key = eligibleFiles.firstKey(); 422 LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath()); 423 Files.delete(eligibleFiles.get(key)); 424 eligibleFiles.remove(key); 425 renameFiles = true; 426 } catch (final IOException ioe) { 427 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe); 428 break; 429 } 430 } 431 final StringBuilder buf = new StringBuilder(); 432 if (renameFiles) { 433 for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) { 434 buf.setLength(0); 435 // LOG4J2-531: directory scan & rollover must use same format 436 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1); 437 final String currentName = entry.getValue().toFile().getName(); 438 String renameTo = buf.toString(); 439 final int suffixLength = suffixLength(renameTo); 440 if (suffixLength > 0 && suffixLength(currentName) == 0) { 441 renameTo = renameTo.substring(0, renameTo.length() - suffixLength); 442 } 443 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true); 444 try { 445 LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action); 446 if (!action.execute()) { 447 return -1; 448 } 449 } catch (final Exception ex) { 450 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 451 return -1; 452 } 453 } 454 } 455 456 return eligibleFiles.size() > 0 ? 457 (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex; 458 } 459 460 /** 461 * Purges and renames old log files in preparation for rollover. The newest file will have the smallest index, the 462 * oldest will have the highest. 463 * 464 * @param lowIndex low index 465 * @param highIndex high index. Log file associated with high index will be deleted if needed. 466 * @param manager The RollingFileManager 467 * @return true if purge was successful and rollover should be attempted. 468 */ 469 private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 470 // Retrieve the files in descending order, so the highest key will be first. 471 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager, false); 472 final int maxFiles = highIndex - lowIndex + 1; 473 474 while (eligibleFiles.size() >= maxFiles) { 475 try { 476 final Integer key = eligibleFiles.firstKey(); 477 Files.delete(eligibleFiles.get(key)); 478 eligibleFiles.remove(key); 479 } catch (final IOException ioe) { 480 LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe); 481 break; 482 } 483 } 484 final StringBuilder buf = new StringBuilder(); 485 for (final Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) { 486 buf.setLength(0); 487 // LOG4J2-531: directory scan & rollover must use same format 488 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() + 1); 489 final String currentName = entry.getValue().toFile().getName(); 490 String renameTo = buf.toString(); 491 final int suffixLength = suffixLength(renameTo); 492 if (suffixLength > 0 && suffixLength(currentName) == 0) { 493 renameTo = renameTo.substring(0, renameTo.length() - suffixLength); 494 } 495 final Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true); 496 try { 497 LOGGER.debug("DefaultRolloverStrategy.purgeDescending executing {}", action); 498 if (!action.execute()) { 499 return -1; 500 } 501 } catch (final Exception ex) { 502 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 503 return -1; 504 } 505 } 506 507 return lowIndex; 508 } 509 510 /** 511 * Performs the rollover. 512 * 513 * @param manager The RollingFileManager name for current active log file. 514 * @return A RolloverDescription. 515 * @throws SecurityException if an error occurs. 516 */ 517 @Override 518 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 519 int fileIndex; 520 final StringBuilder buf = new StringBuilder(255); 521 if (minIndex == Integer.MIN_VALUE) { 522 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 523 fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1; 524 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex); 525 } else { 526 if (maxIndex < 0) { 527 return null; 528 } 529 final long startNanos = System.nanoTime(); 530 fileIndex = purge(minIndex, maxIndex, manager); 531 if (fileIndex < 0) { 532 return null; 533 } 534 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex); 535 if (LOGGER.isTraceEnabled()) { 536 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); 537 LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis); 538 } 539 } 540 541 final String currentFileName = manager.getFileName(); 542 543 String renameTo = buf.toString(); 544 final String compressedName = renameTo; 545 Action compressAction = null; 546 547 final FileExtension fileExtension = manager.getFileExtension(); 548 if (fileExtension != null) { 549 final File renameToFile = new File(renameTo); 550 renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length()); 551 if (tempCompressedFilePattern != null) { 552 buf.delete(0, buf.length()); 553 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex); 554 final String tmpCompressedName = buf.toString(); 555 final File tmpCompressedNameFile = new File(tmpCompressedName); 556 final File parentFile = tmpCompressedNameFile.getParentFile(); 557 if (parentFile != null) { 558 parentFile.mkdirs(); 559 } 560 compressAction = new CompositeAction( 561 Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName, 562 true, compressionLevel), 563 new FileRenameAction(tmpCompressedNameFile, 564 renameToFile, true)), 565 true); 566 } else { 567 compressAction = fileExtension.createCompressAction(renameTo, compressedName, 568 true, compressionLevel); 569 } 570 } 571 572 if (currentFileName.equals(renameTo)) { 573 LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName); 574 return new RolloverDescriptionImpl(currentFileName, false, null, null); 575 } 576 577 if (compressAction != null && manager.isAttributeViewEnabled()) { 578 // Propagate posix attribute view to compressed file 579 // @formatter:off 580 final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder() 581 .withBasePath(compressedName) 582 .withFollowLinks(false) 583 .withMaxDepth(1) 584 .withPathConditions(new PathCondition[0]) 585 .withSubst(getStrSubstitutor()) 586 .withFilePermissions(manager.getFilePermissions()) 587 .withFileOwner(manager.getFileOwner()) 588 .withFileGroup(manager.getFileGroup()) 589 .build(); 590 // @formatter:on 591 compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false); 592 } 593 594 final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo), 595 manager.isRenameEmptyFiles()); 596 597 final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); 598 return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction); 599 } 600 601 @Override 602 public String toString() { 603 return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ", useMax=" + useMax + ")"; 604 } 605 606}