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 private volatile boolean usePrevTime = false; 240 241 /** 242 * Constructs a new instance. 243 * 244 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 245 * @param customActions custom actions to perform asynchronously after rollover 246 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 247 * @deprecated Since 2.9 Added tempCompressedFilePatternString parameter 248 */ 249 @Deprecated 250 protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel, 251 final StrSubstitutor strSubstitutor, final Action[] customActions, 252 final boolean stopCustomActionsOnError) { 253 this(maxFiles, compressionLevel, strSubstitutor, customActions, stopCustomActionsOnError, null); 254 } 255 256 /** 257 * Constructs a new instance. 258 * 259 * @param maxFiles The maximum number of files that match the date portion of the pattern to keep. 260 * @param customActions custom actions to perform asynchronously after rollover 261 * @param stopCustomActionsOnError whether to stop executing asynchronous actions if an error occurs 262 * @param tempCompressedFilePatternString File pattern of the working file 263 * used during compression, if null no temporary file are used 264 */ 265 protected DirectWriteRolloverStrategy(final int maxFiles, final int compressionLevel, 266 final StrSubstitutor strSubstitutor, final Action[] customActions, 267 final boolean stopCustomActionsOnError, final String tempCompressedFilePatternString) { 268 super(strSubstitutor); 269 this.maxFiles = maxFiles; 270 this.compressionLevel = compressionLevel; 271 this.stopCustomActionsOnError = stopCustomActionsOnError; 272 this.customActions = customActions == null ? Collections.<Action> emptyList() : Arrays.asList(customActions); 273 this.tempCompressedFilePattern = 274 tempCompressedFilePatternString != null ? new PatternProcessor(tempCompressedFilePatternString) : null; 275 } 276 277 public int getCompressionLevel() { 278 return this.compressionLevel; 279 } 280 281 public List<Action> getCustomActions() { 282 return customActions; 283 } 284 285 public int getMaxFiles() { 286 return this.maxFiles; 287 } 288 289 public boolean isStopCustomActionsOnError() { 290 return stopCustomActionsOnError; 291 } 292 293 public PatternProcessor getTempCompressedFilePattern() { 294 return tempCompressedFilePattern; 295 } 296 297 private int purge(final RollingFileManager manager) { 298 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 299 LOGGER.debug("Found {} eligible files, max is {}", eligibleFiles.size(), maxFiles); 300 while (eligibleFiles.size() >= maxFiles) { 301 try { 302 final Integer key = eligibleFiles.firstKey(); 303 Files.delete(eligibleFiles.get(key)); 304 eligibleFiles.remove(key); 305 } catch (final IOException ioe) { 306 LOGGER.error("Unable to delete {}", eligibleFiles.firstKey(), ioe); 307 break; 308 } 309 } 310 return eligibleFiles.size() > 0 ? eligibleFiles.lastKey() : 1; 311 } 312 313 @Override 314 public String getCurrentFileName(final RollingFileManager manager) { 315 if (currentFileName == null) { 316 final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager); 317 final int fileIndex = eligibleFiles.size() > 0 ? (nextIndex > 0 ? nextIndex : eligibleFiles.size()) : 1; 318 final StringBuilder buf = new StringBuilder(255); 319 manager.getPatternProcessor().formatFileName(strSubstitutor, buf, true, fileIndex); 320 final int suffixLength = suffixLength(buf.toString()); 321 final String name = suffixLength > 0 ? buf.substring(0, buf.length() - suffixLength) : buf.toString(); 322 currentFileName = name; 323 } 324 return currentFileName; 325 } 326 327 @Override 328 public void clearCurrentFileName() { 329 currentFileName = null; 330 } 331 332 /** 333 * Performs the rollover. 334 * 335 * @param manager The RollingFileManager name for current active log file. 336 * @return A RolloverDescription. 337 * @throws SecurityException if an error occurs. 338 */ 339 @Override 340 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 341 LOGGER.debug("Rolling " + currentFileName); 342 if (maxFiles < 0) { 343 return null; 344 } 345 final long startNanos = System.nanoTime(); 346 final int fileIndex = purge(manager); 347 if (LOGGER.isTraceEnabled()) { 348 final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); 349 LOGGER.trace("DirectWriteRolloverStrategy.purge() took {} milliseconds", durationMillis); 350 } 351 Action compressAction = null; 352 final String sourceName = getCurrentFileName(manager); 353 String compressedName = sourceName; 354 currentFileName = null; 355 nextIndex = fileIndex + 1; 356 final FileExtension fileExtension = manager.getFileExtension(); 357 if (fileExtension != null) { 358 compressedName += fileExtension.getExtension(); 359 if (tempCompressedFilePattern != null) { 360 final StringBuilder buf = new StringBuilder(); 361 tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex); 362 final String tmpCompressedName = buf.toString(); 363 final File tmpCompressedNameFile = new File(tmpCompressedName); 364 final File parentFile = tmpCompressedNameFile.getParentFile(); 365 if (parentFile != null) { 366 parentFile.mkdirs(); 367 } 368 compressAction = new CompositeAction( 369 Arrays.asList(fileExtension.createCompressAction(sourceName, tmpCompressedName, 370 true, compressionLevel), 371 new FileRenameAction(tmpCompressedNameFile, 372 new File(compressedName), true)), 373 true); 374 } else { 375 compressAction = fileExtension.createCompressAction(sourceName, compressedName, 376 true, compressionLevel); 377 } 378 } 379 380 if (compressAction != null && manager.isAttributeViewEnabled()) { 381 // Propagate posix attribute view to compressed file 382 // @formatter:off 383 final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder() 384 .withBasePath(compressedName) 385 .withFollowLinks(false) 386 .withMaxDepth(1) 387 .withPathConditions(PathCondition.EMPTY_ARRAY) 388 .withSubst(getStrSubstitutor()) 389 .withFilePermissions(manager.getFilePermissions()) 390 .withFileOwner(manager.getFileOwner()) 391 .withFileGroup(manager.getFileGroup()) 392 .build(); 393 // @formatter:on 394 compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false); 395 } 396 397 final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError); 398 return new RolloverDescriptionImpl(sourceName, false, null, asyncAction); 399 } 400 401 @Override 402 public String toString() { 403 return "DirectWriteRolloverStrategy(maxFiles=" + maxFiles + ')'; 404 } 405 406}