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", type = "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 public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { 109 if (maxIndex >= 0) { 110 int fileIndex; 111 112 if ((fileIndex = purge(minIndex, maxIndex, manager)) < 0) { 113 return null; 114 } 115 116 final StringBuilder buf = new StringBuilder(); 117 manager.getProcessor().formatFileName(buf, fileIndex); 118 final String currentFileName = manager.getFileName(); 119 120 String renameTo = subst.replace(buf); 121 final String compressedName = renameTo; 122 Action compressAction = null; 123 124 if (renameTo.endsWith(".gz")) { 125 renameTo = renameTo.substring(0, renameTo.length() - 3); 126 compressAction = new GZCompressAction(new File(renameTo), new File(compressedName), true); 127 } else if (renameTo.endsWith(".zip")) { 128 renameTo = renameTo.substring(0, renameTo.length() - 4); 129 compressAction = new ZipCompressAction(new File(renameTo), new File(compressedName), true); 130 } 131 132 final FileRenameAction renameAction = 133 new FileRenameAction(new File(currentFileName), new File(renameTo), false); 134 135 return new RolloverDescriptionImpl(currentFileName, false, renameAction, compressAction); 136 } 137 138 return null; 139 } 140 141 private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) { 142 return useMax ? purgeAscending(lowIndex, highIndex, manager) : 143 purgeDescending(lowIndex, highIndex, manager); 144 } 145 146 /** 147 * Purge and rename old log files in preparation for rollover. The newest file will have the smallest index, the 148 * oldest will have the highest. 149 * 150 * @param lowIndex low index 151 * @param highIndex high index. Log file associated with high index will be deleted if needed. 152 * @param manager The RollingFileManager 153 * @return true if purge was successful and rollover should be attempted. 154 */ 155 private int purgeDescending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 156 int suffixLength = 0; 157 158 final List<FileRenameAction> renames = new ArrayList<FileRenameAction>(); 159 final StringBuilder buf = new StringBuilder(); 160 manager.getProcessor().formatFileName(buf, lowIndex); 161 162 String lowFilename = subst.replace(buf); 163 164 if (lowFilename.endsWith(".gz")) { 165 suffixLength = 3; 166 } else if (lowFilename.endsWith(".zip")) { 167 suffixLength = 4; 168 } 169 170 for (int i = lowIndex; i <= highIndex; i++) { 171 File toRename = new File(lowFilename); 172 boolean isBase = false; 173 174 if (suffixLength > 0) { 175 final File toRenameBase = 176 new File(lowFilename.substring(0, lowFilename.length() - suffixLength)); 177 178 if (toRename.exists()) { 179 if (toRenameBase.exists()) { 180 toRenameBase.delete(); 181 } 182 } else { 183 toRename = toRenameBase; 184 isBase = true; 185 } 186 } 187 188 if (toRename.exists()) { 189 // 190 // if at upper index then 191 // attempt to delete last file 192 // if that fails then abandon purge 193 if (i == highIndex) { 194 if (!toRename.delete()) { 195 return -1; 196 } 197 198 break; 199 } 200 201 // 202 // if intermediate index 203 // add a rename action to the list 204 buf.setLength(0); 205 manager.getProcessor().formatFileName(buf, i + 1); 206 207 final String highFilename = subst.replace(buf); 208 String renameTo = highFilename; 209 210 if (isBase) { 211 renameTo = highFilename.substring(0, highFilename.length() - suffixLength); 212 } 213 214 renames.add(new FileRenameAction(toRename, new File(renameTo), true)); 215 lowFilename = highFilename; 216 } else { 217 break; 218 } 219 } 220 221 // 222 // work renames backwards 223 // 224 for (int i = renames.size() - 1; i >= 0; i--) { 225 final Action action = renames.get(i); 226 227 try { 228 if (!action.execute()) { 229 return -1; 230 } 231 } catch (final Exception ex) { 232 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 233 return -1; 234 } 235 } 236 237 return lowIndex; 238 } 239 240 /** 241 * Purge and rename old log files in preparation for rollover. The oldest file will have the smallest index, 242 * the newest the highest. 243 * 244 * @param lowIndex low index 245 * @param highIndex high index. Log file associated with high index will be deleted if needed. 246 * @param manager The RollingFileManager 247 * @return true if purge was successful and rollover should be attempted. 248 */ 249 private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) { 250 int suffixLength = 0; 251 252 final List<FileRenameAction> renames = new ArrayList<FileRenameAction>(); 253 final StringBuilder buf = new StringBuilder(); 254 manager.getProcessor().formatFileName(buf, highIndex); 255 256 String highFilename = subst.replace(buf); 257 258 if (highFilename.endsWith(".gz")) { 259 suffixLength = 3; 260 } else if (highFilename.endsWith(".zip")) { 261 suffixLength = 4; 262 } 263 264 int maxIndex = 0; 265 266 for (int i = highIndex; i >= lowIndex; i--) { 267 File toRename = new File(highFilename); 268 if (i == highIndex && toRename.exists()) { 269 maxIndex = highIndex; 270 } else if (maxIndex == 0 && toRename.exists()) { 271 maxIndex = i + 1; 272 break; 273 } 274 275 boolean isBase = false; 276 277 if (suffixLength > 0) { 278 final File toRenameBase = 279 new File(highFilename.substring(0, highFilename.length() - suffixLength)); 280 281 if (toRename.exists()) { 282 if (toRenameBase.exists()) { 283 toRenameBase.delete(); 284 } 285 } else { 286 toRename = toRenameBase; 287 isBase = true; 288 } 289 } 290 291 if (toRename.exists()) { 292 // 293 // if at lower index and then all slots full 294 // attempt to delete last file 295 // if that fails then abandon purge 296 if (i == lowIndex) { 297 if (!toRename.delete()) { 298 return -1; 299 } 300 301 break; 302 } 303 304 // 305 // if intermediate index 306 // add a rename action to the list 307 buf.setLength(0); 308 manager.getProcessor().formatFileName(buf, i - 1); 309 310 final String lowFilename = subst.replace(buf); 311 String renameTo = lowFilename; 312 313 if (isBase) { 314 renameTo = lowFilename.substring(0, lowFilename.length() - suffixLength); 315 } 316 317 renames.add(new FileRenameAction(toRename, new File(renameTo), true)); 318 highFilename = lowFilename; 319 } else { 320 buf.setLength(0); 321 manager.getProcessor().formatFileName(buf, i - 1); 322 323 highFilename = subst.replace(buf); 324 } 325 } 326 if (maxIndex == 0) { 327 maxIndex = lowIndex; 328 } 329 330 // 331 // work renames backwards 332 // 333 for (int i = renames.size() - 1; i >= 0; i--) { 334 final Action action = renames.get(i); 335 336 try { 337 if (!action.execute()) { 338 return -1; 339 } 340 } catch (final Exception ex) { 341 LOGGER.warn("Exception during purge in RollingFileAppender", ex); 342 return -1; 343 } 344 } 345 return maxIndex; 346 } 347 348 @Override 349 public String toString() { 350 return "DefaultRolloverStrategy(min=" + minIndex + ", max=" + maxIndex + ")"; 351 } 352 353 /** 354 * Create the DefaultRolloverStrategy. 355 * @param max The maximum number of files to keep. 356 * @param min The minimum number of files to keep. 357 * @param fileIndex If set to "max" (the default), files with a higher index will be newer than files with a 358 * smaller index. If set to "min", file renaming and the counter will follow the Fixed Window strategy. 359 * @param config The Configuration. 360 * @return A DefaultRolloverStrategy. 361 */ 362 @PluginFactory 363 public static DefaultRolloverStrategy createStrategy(@PluginAttr("max") final String max, 364 @PluginAttr("min") final String min, 365 @PluginAttr("fileIndex") final String fileIndex, 366 @PluginConfiguration final Configuration config) { 367 final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max"); 368 int minIndex; 369 if (min != null) { 370 minIndex = Integer.parseInt(min); 371 if (minIndex < 1) { 372 LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE); 373 minIndex = MIN_WINDOW_SIZE; 374 } 375 } else { 376 minIndex = MIN_WINDOW_SIZE; 377 } 378 int maxIndex; 379 if (max != null) { 380 maxIndex = Integer.parseInt(max); 381 if (maxIndex < minIndex) { 382 maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex; 383 LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex); 384 } 385 } else { 386 maxIndex = DEFAULT_WINDOW_SIZE; 387 } 388 return new DefaultRolloverStrategy(minIndex, maxIndex, useMax, config.getSubst()); 389 } 390 391 }