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.SortedMap; 027import java.util.concurrent.TimeUnit; 028import java.util.zip.Deflater; 029 030import org.apache.logging.log4j.core.Core; 031import org.apache.logging.log4j.core.appender.rolling.action.Action; 032import org.apache.logging.log4j.core.appender.rolling.action.CompositeAction; 033import org.apache.logging.log4j.core.appender.rolling.action.FileRenameAction; 034import org.apache.logging.log4j.core.appender.rolling.action.PathCondition; 035import org.apache.logging.log4j.core.appender.rolling.action.PosixViewAttributeAction; 036import org.apache.logging.log4j.core.config.Configuration; 037import org.apache.logging.log4j.core.config.plugins.Plugin; 038import org.apache.logging.log4j.core.config.plugins.PluginAttribute; 039import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; 040import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; 041import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; 042import org.apache.logging.log4j.core.config.plugins.PluginElement; 043import org.apache.logging.log4j.core.config.plugins.PluginFactory; 044import org.apache.logging.log4j.core.lookup.StrSubstitutor; 045import org.apache.logging.log4j.core.util.Integers; 046 047/** 048 * When rolling over, <code>DirectWriteRolloverStrategy</code> writes directly to the file as resolved by the file 049 * pattern. Files will be renamed files according to an algorithm as described below. 050 * 051 * <p> 052 * The DirectWriteRolloverStrategy uses similar logic as DefaultRolloverStrategy to determine the file name based 053 * on the file pattern, however the DirectWriteRolloverStrategy writes directly to a file and does not rename it 054 * during rollover, except if it is compressed, in which case it will add the appropriate file extension. 055 * </p> 056 * 057 * @since 2.8 058 */ 059@Plugin(name = "DirectWriteRolloverStrategy", category = Core.CATEGORY_NAME, printObject = true) 060public class DirectWriteRolloverStrategy extends AbstractRolloverStrategy implements DirectFileRolloverStrategy { 061 062 private static final int DEFAULT_MAX_FILES = 7; 063 064 /** 065 * Builds DirectWriteRolloverStrategy instances. 066 */ 067 public static class Builder implements org.apache.logging.log4j.core.util.Builder<DirectWriteRolloverStrategy> { 068 @PluginBuilderAttribute("maxFiles") 069 private String maxFiles; 070 071 @PluginBuilderAttribute("compressionLevel") 072 private String compressionLevelStr; 073 074 @PluginElement("Actions") 075 private Action[] customActions; 076 077 @PluginBuilderAttribute(value = "stopCustomActionsOnError") 078 private boolean stopCustomActionsOnError = true; 079 080 @PluginBuilderAttribute(value = "tempCompressedFilePattern") 081 private String tempCompressedFilePattern; 082 083 @PluginConfiguration 084 private Configuration config; 085 086 @Override 087 public DirectWriteRolloverStrategy build() { 088 int maxIndex = Integer.MAX_VALUE; 089 if (maxFiles != null) { 090 maxIndex = Integer.parseInt(maxFiles); 091 if (maxIndex < 0) { 092 maxIndex = Integer.MAX_VALUE; 093 } else if (maxIndex < 2) { 094 LOGGER.error("Maximum files too small. Limited to " + DEFAULT_MAX_FILES); 095 maxIndex = DEFAULT_MAX_FILES; 096 } 097 } 098 final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION); 099 return new DirectWriteRolloverStrategy(maxIndex, compressionLevel, config.getStrSubstitutor(), 100 customActions, stopCustomActionsOnError, tempCompressedFilePattern); 101 } 102 103 public String getMaxFiles() { 104 return maxFiles; 105 } 106 107 /** 108 * Defines the maximum number of files to keep. 109 * 110 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 111 * @return This builder for chaining convenience 112 */ 113 public Builder withMaxFiles(final String maxFiles) { 114 this.maxFiles = maxFiles; 115 return this; 116 } 117 118 public String getCompressionLevelStr() { 119 return compressionLevelStr; 120 } 121 122 /** 123 * Defines compression level. 124 * 125 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 126 * @return This builder for chaining convenience 127 */ 128 public Builder withCompressionLevelStr(final String compressionLevelStr) { 129 this.compressionLevelStr = compressionLevelStr; 130 return this; 131 } 132 133 public Action[] getCustomActions() { 134 return customActions; 135 } 136 137 /** 138 * Defines custom actions. 139 * 140 * @param customActions custom actions to perform asynchronously after rollover 141 * @return This builder for chaining convenience 142 */ 143 public Builder withCustomActions(final Action[] customActions) { 144 this.customActions = customActions; 145 return this; 146 } 147 148 public boolean isStopCustomActionsOnError() { 149 return stopCustomActionsOnError; 150 } 151 152 /** 153 * Defines whether to stop executing asynchronous actions if an error occurs. 154 * 155 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 156 * @return This builder for chaining convenience 157 */ 158 public Builder withStopCustomActionsOnError(final boolean stopCustomActionsOnError) { 159 this.stopCustomActionsOnError = stopCustomActionsOnError; 160 return this; 161 } 162 163 public String getTempCompressedFilePattern() { 164 return tempCompressedFilePattern; 165 } 166 167 /** 168 * Defines temporary compression file pattern. 169 * 170 * @param tempCompressedFilePattern File pattern of the working file pattern used during compression, if null no temporary file are used 171 * @return This builder for chaining convenience 172 */ 173 public Builder withTempCompressedFilePattern(final String tempCompressedFilePattern) { 174 this.tempCompressedFilePattern = tempCompressedFilePattern; 175 return this; 176 } 177 178 public Configuration getConfig() { 179 return config; 180 } 181 182 /** 183 * Defines configuration. 184 * 185 * @param config The Configuration. 186 * @return This builder for chaining convenience 187 */ 188 public Builder withConfig(final Configuration config) { 189 this.config = config; 190 return this; 191 } 192 } 193 194 @PluginBuilderFactory 195 public static Builder newBuilder() { 196 return new Builder(); 197 } 198 199 /** 200 * Creates the DirectWriteRolloverStrategy. 201 * 202 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 203 * @param compressionLevelStr The compression level, 0 (less) through 9 (more); applies only to ZIP files. 204 * @param customActions custom actions to perform asynchronously after rollover 205 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 206 * @param config The Configuration. 207 * @return A DirectWriteRolloverStrategy. 208 * @deprecated Since 2.9 Usage of Builder API is preferable 209 */ 210 @Deprecated 211 @PluginFactory 212 public static DirectWriteRolloverStrategy createStrategy( 213 // @formatter:off 214 @PluginAttribute("maxFiles") final String maxFiles, 215 @PluginAttribute("compressionLevel") final String compressionLevelStr, 216 @PluginElement("Actions") final Action[] customActions, 217 @PluginAttribute(value = "stopCustomActionsOnError", defaultBoolean = true) 218 final boolean stopCustomActionsOnError, 219 @PluginConfiguration final Configuration config) { 220 return newBuilder().withMaxFiles(maxFiles) 221 .withCompressionLevelStr(compressionLevelStr) 222 .withCustomActions(customActions) 223 .withStopCustomActionsOnError(stopCustomActionsOnError) 224 .withConfig(config) 225 .build(); 226 // @formatter:on 227 } 228 229 /** 230 * Index for most recent log file. 231 */ 232 private final int maxFiles; 233 private final int compressionLevel; 234 private final List<Action> customActions; 235 private final boolean stopCustomActionsOnError; 236 private volatile String currentFileName; 237 private int nextIndex = -1; 238 private final PatternProcessor tempCompressedFilePattern; 239 240 /** 241 * Constructs a new instance. 242 * 243 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 244 * @param customActions custom actions to perform asynchronously after rollover 245 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 246 * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter 247 */ 248 @Deprecated 249 protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel, 250 final StrSubstitutor strSubstitutor, final Action[] customActions, 251 final boolean stopCustomActionsOnError) { 252 this(maxFiles, compressionLevel, strSubstitutor, customActions, stopCustomActionsOnError, null); 253 } 254 255 /** 256 * Constructs a new instance. 257 * 258 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 259 * @param customActions custom actions to perform asynchronously after rollover 260 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 261 * @param tempCompressedFilePatternString File pattern of the working file 262 * used during compression, if null no temporary file are used 263 */ 264 protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel, 265 final StrSubstitutor strSubstitutor, final Action[] customActions, 266 final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) { 267 super(strSubstitutor); 268 this.maxFiles = maxFiles; 269 this.compressionLevel = compressionLevel; 270 this.stopCustomActionsOnError = stopCustomActionsOnError; 271 this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions); 272 this.tempCompressedFilePattern = 273 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; 274 } 275 276 public int getCompressionLevel() { 277 return this.compressionLevel; 278 } 279 280 public List<Action> getCustomActions() { 281 return customActions; 282 } 283 284 public int getMaxFiles() { 285 return this.maxFiles; 286 } 287 288 public boolean isStopCustomActionsOnError() { 289 return stopCustomActionsOnError; 290 } 291 292 public PatternProcessor getTempCompressedFilePattern() { 293 return tempCompressedFilePattern; 294 } 295 296 private int purge(final RollingFileManager manager) { 297 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 298 LOGGER.debug("Found {} eligible files, max is {}", eligibleFiles.size(), maxFiles); 299 while (eligibleFiles.size() >= maxFiles) { 300 try { 301 final Integer key = eligibleFiles.firstKey(); 302 Files.delete(eligibleFiles.get(key)); 303 eligibleFiles.remove(key); 304 } catch (final IOException ioe) { 305 LOGGER.error("Unable to delete {}", eligibleFiles.firstKey(), ioe); 306 break; 307 } 308 } 309 return eligibleFiles.size() > 0 ? eligibleFiles.lastKey() : 1; 310 } 311 312 @Override 313 public String getCurrentFileName(final RollingFileManager manager) { 314 if (currentFileName == null) { 315 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 316 final int fileIndex = eligibleFiles.size() > 0 ? (nextIndex > 0 ? nextIndex : eligibleFiles.size()) : 1; 317 final StringBuilder buf = new StringBuilder(255); 318 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, true, fileIndex); 319 final int suffixLength = suffixLength(buf.toString()); 320 final String name = suffixLength > 0 ? buf.substring(0, buf.length() - suffixLength) : buf.toString(); 321 currentFileName = name; 322 } 323 return currentFileName; 324 } 325 326 /** 327 * Performs the rollover. 328 * 329 * @param manager The RollingFileManager name for current active log file. 330 * @return A RolloverDescription. 331 * @throws SecurityException if an error occurs. 332 */ 333 @Override 334 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 335 LOGGER.debug("Rolling " + currentFileName); 336 if (maxFiles < 0) { 337 return null; 338 } 339 final long startNanos = System.nanoTime(); 340 final int fileIndex = purge(manager); 341 if (LOGGER.isTraceEnabled()) { 342 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); 343 LOGGER.trace("DirectWriteRolloverStrategy.purge() took {} milliseconds", durationMillis); 344 } 345 Action compressAction = null; 346 final String sourceName = getCurrentFileName(manager); 347 String compressedName = sourceName; 348 currentFileName = null; 349 nextIndex = fileIndex + 1; 350 final FileExtension fileExtension = manager.getFileExtension(); 351 if (fileExtension != null) { 352 compressedName += fileExtension.getExtension(); 353 if (tempCompressedFilePattern != null) { 354 final StringBuilder buf = new StringBuilder(); 355 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex); 356 final String tmpCompressedName = buf.toString(); 357 final File tmpCompressedNameFile = new File(tmpCompressedName); 358 final File parentFile = tmpCompressedNameFile.getParentFile(); 359 if (parentFile != null) { 360 parentFile.mkdirs(); 361 } 362 compressAction = new CompositeAction( 363 Arrays.asList(fileExtension.createCompressAction(sourceName, tmpCompressedName, 364 true, compressionLevel), 365 new FileRenameAction(tmpCompressedNameFile, 366 new File(compressedName), true)), 367 true); 368 } else { 369 compressAction = fileExtension.createCompressAction(sourceName, compressedName, 370 true, compressionLevel); 371 } 372 } 373 374 if (compressAction != null && manager.isAttributeViewEnabled()) { 375 // Propagate posix attribute view to compressed file 376 // @formatter:off 377 final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder() 378 .withBasePath(compressedName) 379 .withFollowLinks(false) 380 .withMaxDepth(1) 381 .withPathConditions(new PathCondition[0]) 382 .withSubst(getStrSubstitutor()) 383 .withFilePermissions(manager.getFilePermissions()) 384 .withFileOwner(manager.getFileOwner()) 385 .withFileGroup(manager.getFileGroup()) 386 .build(); 387 // @formatter:on 388 compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false); 389 } 390 391 final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); 392 return new RolloverDescriptionImpl(sourceName, false, null, asyncAction); 393 } 394 395 @Override 396 public String toString() { 397 return "DirectWriteRolloverStrategy(maxFiles=" + maxFiles + ')'; 398 } 399 400}