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 */ 017 package org.apache.logging.log4j.core.appender.rolling; 018 019 import org.apache.logging.log4j.Logger; 020 import org.apache.logging.log4j.core.appender.rolling.helper.Action; 021 import org.apache.logging.log4j.core.appender.rolling.helper.FileRenameAction; 022 import org.apache.logging.log4j.core.appender.rolling.helper.GZCompressAction; 023 import org.apache.logging.log4j.core.appender.rolling.helper.ZipCompressAction; 024 import org.apache.logging.log4j.core.config.Configuration; 025 import org.apache.logging.log4j.core.config.plugins.Plugin; 026 import org.apache.logging.log4j.core.config.plugins.PluginAttr; 027 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; 028 import org.apache.logging.log4j.core.config.plugins.PluginFactory; 029 import org.apache.logging.log4j.core.lookup.StrSubstitutor; 030 import org.apache.logging.log4j.status.StatusLogger; 031 032 import java.io.File; 033 import java.util.ArrayList; 034 import java.util.List; 035 036 /** 037 * When rolling over, <code>DefaultRolloverStrategy</code> renames files 038 * according to an algorithm as described below. 039 * <p/> 040 * <p>The DefaultRolloverStrategy is a combination of a time-based policy and a fixed-window policy. When 041 * the file name pattern contains a date format then the rollover time interval will be used to calculate the 042 * time to use in the file pattern. When the file pattern contains an integer replacement token one of the 043 * counting techniques will be used.</p> 044 * <p>When the ascending attribute is set to true (the default) then the counter will be incremented and the 045 * current log file will be renamed to include the counter value. If the counter hits the maximum value then 046 * the oldest file, which will have the smallest counter, will be deleted, all other files will be renamed to 047 * have their counter decremented and then the current file will be renamed to have the maximum counter value. 048 * Note that with this counting strategy specifying a large maximum value may entirely avoid renaming files.</p> 049 * <p>When the ascending attribute is false, then the "normal" fixed-window strategy will be used.</p> 050 * Let <em>max</em> and <em>min</em> represent the values of respectively 051 * the <b>MaxIndex</b> and <b>MinIndex</b> options. Let "foo.log" be the value 052 * of the <b>ActiveFile</b> option and "foo.%i.log" the value of 053 * <b>FileNamePattern</b>. Then, when rolling over, the file 054 * <code>foo.<em>max</em>.log</code> will be deleted, the file 055 * <code>foo.<em>max-1</em>.log</code> will be renamed as 056 * <code>foo.<em>max</em>.log</code>, the file <code>foo.<em>max-2</em>.log</code> 057 * renamed as <code>foo.<em>max-1</em>.log</code>, and so on, 058 * the file <code>foo.<em>min+1</em>.log</code> renamed as 059 * <code>foo.<em>min+2</em>.log</code>. Lastly, the active file <code>foo.log</code> 060 * will be renamed as <code>foo.<em>min</em>.log</code> and a new active file name 061 * <code>foo.log</code> will be created. 062 * <p/> 063 * <p>Given that this rollover algorithm requires as many file renaming 064 * operations as the window size, large window sizes are discouraged. 065 */ 066 @Plugin(name = "DefaultRolloverStrategy", category = "Core", printObject = true) 067 public class DefaultRolloverStrategy implements RolloverStrategy { 068 /** 069 * Allow subclasses access to the status logger without creating another instance. 070 */ 071 protected static final Logger LOGGER = StatusLogger.getLogger(); 072 073 private static final int MIN_WINDOW_SIZE = 1; 074 private static final int DEFAULT_WINDOW_SIZE = 7; 075 076 /** 077 * Index for oldest retained log file. 078 */ 079 private final int maxIndex; 080 081 /** 082 * Index for most recent log file. 083 */ 084 private final int minIndex; 085 086 private final boolean useMax; 087 088 private final StrSubstitutor subst; 089 090 /** 091 * Constructs a new instance. 092 * @param min The minimum index. 093 * @param max The maximum index. 094 */ 095 protected DefaultRolloverStrategy(final int min, final int max, final boolean useMax, final StrSubstitutor subst) { 096 minIndex = min; 097 maxIndex = max; 098 this.subst = subst; 099 this.useMax = useMax; 100 } 101 102 /** 103 * Perform the rollover. 104 * @param manager The RollingFileManager name for current active log file. 105 * @return A RolloverDescription. 106 * @throws SecurityException if an error occurs. 107 */ 108 @Override 109 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 110 if (maxIndex >= 0) { 111 int fileIndex; 112 113 if ((fileIndex = purge(minIndex, maxIndex, manager)) < 0) { 114 return null; 115 } 116 117 final StringBuilder buf = new StringBuilder(); 118 manager.getProcessor().formatFileName(buf, fileIndex); 119 final String currentFileName = manager.getFileName(); 120 121 String renameTo = subst.replace(buf); 122 final String compressedName = renameTo; 123 Action compressAction = null; 124 125 if (renameTo.endsWith(".gz")) { 126 renameTo = renameTo.substring(0, renameTo.length() - 3); 127 compressAction = new GZCompressAction(new File(renameTo), new File(compressedName), true); 128 } else if (renameTo.endsWith(".zip")) { 129 renameTo = renameTo.substring(0, renameTo.length() - 4); 130 compressAction = new ZipCompressAction(new File(renameTo), new File(compressedName), true); 131 } 132 133 final FileRenameAction renameAction = 134 new FileRenameAction(new File(currentFileName), new File(renameTo), false); 135 136 return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction); 137 } 138 139 return null; 140 } 141 142 private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) { 143 return useMax ? purgeAscending(lowIndex, highIndex, manager) : 144 purgeDescending(lowIndex, highIndex, manager); 145 } 146 147 /** 148 * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the 149 * oldest will have the highest. 150 * 151 * @param lowIndex low index 152 * @param highIndex high index. Log file associated with high index will be deleted if needed. 153 * @param manager The RollingFileManager 154 * @return true if purge was successful and rollover should be attempted. 155 */ 156 private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 157 int suffixLength = 0; 158 159 final List<FileRenameAction> renames = new ArrayList<FileRenameAction>(); 160 final StringBuilder buf = new StringBuilder(); 161 manager.getProcessor().formatFileName(buf, lowIndex); 162 163 String lowFilename = subst.replace(buf); 164 165 if (lowFilename.endsWith(".gz")) { 166 suffixLength = 3; 167 } else if (lowFilename.endsWith(".zip")) { 168 suffixLength = 4; 169 } 170 171 for (int i = lowIndex; i <= highIndex; i++) { 172 File toRename = new File(lowFilename); 173 boolean isBase = false; 174 175 if (suffixLength > 0) { 176 final File toRenameBase = 177 new File(lowFilename.substring(0, lowFilename.length() - suffixLength)); 178 179 if (toRename.exists()) { 180 if (toRenameBase.exists()) { 181 toRenameBase.delete(); 182 } 183 } else { 184 toRename = toRenameBase; 185 isBase = true; 186 } 187 } 188 189 if (toRename.exists()) { 190 // 191 // if at upper index then 192 // attempt to delete last file 193 // if that fails then abandon purge 194 if (i == highIndex) { 195 if (!toRename.delete()) { 196 return -1; 197 } 198 199 break; 200 } 201 202 // 203 // if intermediate index 204 // add a rename action to the list 205 buf.setLength(0); 206 manager.getProcessor().formatFileName(buf, i + 1); 207 208 final String highFilename = subst.replace(buf); 209 String renameTo = highFilename; 210 211 if (isBase) { 212 renameTo = highFilename.substring(0, highFilename.length() - suffixLength); 213 } 214 215 renames.add(new FileRenameAction(toRename, new File(renameTo), true)); 216 lowFilename = highFilename; 217 } else { 218 break; 219 } 220 } 221 222 // 223 // work renames backwards 224 // 225 for (int i = renames.size() - 1; i >= 0; i--) { 226 final Action action = renames.get(i); 227 228 try { 229 if (!action.execute()) { 230 return -1; 231 } 232 } catch (final Exception ex) { 233 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 234 return -1; 235 } 236 } 237 238 return lowIndex; 239 } 240 241 /** 242 * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index, 243 * the newest the highest. 244 * 245 * @param lowIndex low index 246 * @param highIndex high index. Log file associated with high index will be deleted if needed. 247 * @param manager The RollingFileManager 248 * @return true if purge was successful and rollover should be attempted. 249 */ 250 private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 251 int suffixLength = 0; 252 253 final List<FileRenameAction> renames = new ArrayList<FileRenameAction>(); 254 final StringBuilder buf = new StringBuilder(); 255 manager.getProcessor().formatFileName(buf, highIndex); 256 257 String highFilename = subst.replace(buf); 258 259 if (highFilename.endsWith(".gz")) { 260 suffixLength = 3; 261 } else if (highFilename.endsWith(".zip")) { 262 suffixLength = 4; 263 } 264 265 int maxIndex = 0; 266 267 for (int i = highIndex; i >= lowIndex; i--) { 268 File toRename = new File(highFilename); 269 if (i == highIndex && toRename.exists()) { 270 maxIndex = highIndex; 271 } else if (maxIndex == 0 && toRename.exists()) { 272 maxIndex = i + 1; 273 break; 274 } 275 276 boolean isBase = false; 277 278 if (suffixLength > 0) { 279 final File toRenameBase = 280 new File(highFilename.substring(0, highFilename.length() - suffixLength)); 281 282 if (toRename.exists()) { 283 if (toRenameBase.exists()) { 284 toRenameBase.delete(); 285 } 286 } else { 287 toRename = toRenameBase; 288 isBase = true; 289 } 290 } 291 292 if (toRename.exists()) { 293 // 294 // if at lower index and then all slots full 295 // attempt to delete last file 296 // if that fails then abandon purge 297 if (i == lowIndex) { 298 if (!toRename.delete()) { 299 return -1; 300 } 301 302 break; 303 } 304 305 // 306 // if intermediate index 307 // add a rename action to the list 308 buf.setLength(0); 309 manager.getProcessor().formatFileName(buf, i - 1); 310 311 final String lowFilename = subst.replace(buf); 312 String renameTo = lowFilename; 313 314 if (isBase) { 315 renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength); 316 } 317 318 renames.add(new FileRenameAction(toRename, new File(renameTo), true)); 319 highFilename = lowFilename; 320 } else { 321 buf.setLength(0); 322 manager.getProcessor().formatFileName(buf, i - 1); 323 324 highFilename = subst.replace(buf); 325 } 326 } 327 if (maxIndex == 0) { 328 maxIndex = lowIndex; 329 } 330 331 // 332 // work renames backwards 333 // 334 for (int i = renames.size() - 1; i >= 0; i--) { 335 final Action action = renames.get(i); 336 337 try { 338 if (!action.execute()) { 339 return -1; 340 } 341 } catch (final Exception ex) { 342 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 343 return -1; 344 } 345 } 346 return maxIndex; 347 } 348 349 @Override 350 public String toString() { 351 return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ")"; 352 } 353 354 /** 355 * Create the DefaultRolloverStrategy. 356 * @param max The maximum number of files to keep. 357 * @param min The minimum number of files to keep. 358 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a 359 * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 360 * @param config The Configuration. 361 * @return A DefaultRolloverStrategy. 362 */ 363 @PluginFactory 364 public static DefaultRolloverStrategy createStrategy(@PluginAttr("max") final String max, 365 @PluginAttr("min") final String min, 366 @PluginAttr("fileIndex") final String fileIndex, 367 @PluginConfiguration final Configuration config) { 368 final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max"); 369 int minIndex; 370 if (min != null) { 371 minIndex = Integer.parseInt(min); 372 if (minIndex < 1) { 373 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE); 374 minIndex = MIN_WINDOW_SIZE; 375 } 376 } else { 377 minIndex = MIN_WINDOW_SIZE; 378 } 379 int maxIndex; 380 if (max != null) { 381 maxIndex = Integer.parseInt(max); 382 if (maxIndex < minIndex) { 383 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex; 384 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex); 385 } 386 } else { 387 maxIndex = DEFAULT_WINDOW_SIZE; 388 } 389 return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, config.getSubst()); 390 } 391 392 }