001/** 002 * Logback: the reliable, generic, fast and flexible logging framework. Copyright (C) 1999-2015, QOS.ch. All rights 003 * reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under either the terms of the Eclipse Public License 006 * v1.0 as published by the Eclipse Foundation 007 * 008 * or (per the licensee's choosing) 009 * 010 * under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. 011 */ 012package ch.qos.logback.core.rolling.helper; 013 014import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 015 016import java.io.File; 017import java.time.Instant; 018import java.util.concurrent.ExecutorService; 019import java.util.concurrent.Future; 020 021import ch.qos.logback.core.CoreConstants; 022import ch.qos.logback.core.pattern.Converter; 023import ch.qos.logback.core.pattern.LiteralConverter; 024import ch.qos.logback.core.spi.ContextAwareBase; 025import ch.qos.logback.core.util.FileSize; 026 027public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover { 028 029 static protected final long UNINITIALIZED = -1; 030 // aim for 32 days, except in case of hourly rollover, see 031 // MAX_VALUE_FOR_INACTIVITY_PERIODS 032 static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY; 033 static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover 034 035 final FileNamePattern fileNamePattern; 036 // Pattern of archives before compression. Null if there is no compression. Used 037 // to find files left uncompressed, e.g. when the application was down at rollover. 038 final FileNamePattern fileNamePatternWithoutCompSuffix; 039 final RollingCalendar rc; 040 private int maxHistory = CoreConstants.UNBOUNDED_HISTORY; 041 private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 042 final boolean parentClean; 043 long lastHeartBeat = UNINITIALIZED; 044 045 public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) { 046 this(fileNamePattern, null, rc); 047 } 048 049 /** 050 * @param fileNamePattern the pattern of archived files 051 * @param fileNamePatternWithoutCompSuffix the same pattern without the compression suffix, may be null. 052 * Uncompressed files matching this pattern are removed 053 * along with archived files. 054 * @param rc the rolling calendar 055 * @since 1.7.0 056 */ 057 public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, FileNamePattern fileNamePatternWithoutCompSuffix, 058 RollingCalendar rc) { 059 this.fileNamePattern = fileNamePattern; 060 this.fileNamePatternWithoutCompSuffix = differentOrNull(fileNamePattern, fileNamePatternWithoutCompSuffix); 061 this.rc = rc; 062 this.parentClean = computeParentCleaningFlag(fileNamePattern); 063 } 064 065 // Without compression both patterns are the same. Keeping only one avoids finding the same file twice. 066 private static FileNamePattern differentOrNull(FileNamePattern fileNamePattern, 067 FileNamePattern fileNamePatternWithoutCompSuffix) { 068 if (fileNamePatternWithoutCompSuffix == null) { 069 return null; 070 } 071 if (fileNamePatternWithoutCompSuffix.getPattern().equals(fileNamePattern.getPattern())) { 072 return null; 073 } 074 return fileNamePatternWithoutCompSuffix; 075 } 076 077 int callCount = 0; 078 079 public Future<?> cleanAsynchronously(Instant now) { 080 ArchiveRemoverRunnable runnable = new ArchiveRemoverRunnable(now); 081 ExecutorService alternateExecutorService = context.getAlternateExecutorService(); 082 Future<?> future = alternateExecutorService.submit(runnable); 083 return future; 084 } 085 086 /** 087 * Called from the cleaning thread. 088 * 089 * @param now 090 */ 091 @Override 092 public void clean(Instant now) { 093 094 long nowInMillis = now.toEpochMilli(); 095 // for a live appender periodsElapsed is expected to be 1 096 int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis); 097 lastHeartBeat = nowInMillis; 098 if (periodsElapsed > 1) { 099 addInfo("Multiple periods, i.e. " + periodsElapsed 100 + " periods, seem to have elapsed. This can happen at application start."); 101 } 102 for (int i = 0; i < periodsElapsed; i++) { 103 int offset = getPeriodOffsetForDeletionTarget() - i; 104 Instant instantOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset); 105 cleanPeriod(instantOfPeriodToClean); 106 } 107 } 108 109 protected File[] getFilesInPeriod(Instant instantOfPeriodToClean) { 110 String filenameToDelete = fileNamePattern.convert(instantOfPeriodToClean); 111 File file2Delete = new File(filenameToDelete); 112 113 if (fileExistsAndIsFile(file2Delete)) { 114 return new File[] { file2Delete }; 115 } else { 116 return new File[0]; 117 } 118 } 119 120 /** 121 * Returns files of the given period which were not compressed, e.g. because the 122 * application was down at rollover time. Returns an empty array if there is no 123 * compression. 124 * 125 * <p>Only called for periods older than maxHistory. Such files are neither the active 126 * file nor being compressed.</p> 127 * 128 * @since 1.7.0 129 */ 130 protected File[] getUncompressedFilesInPeriod(Instant instantOfPeriodToClean) { 131 if (fileNamePatternWithoutCompSuffix == null) { 132 return new File[0]; 133 } 134 File file2Delete = new File(fileNamePatternWithoutCompSuffix.convert(instantOfPeriodToClean)); 135 if (fileExistsAndIsFile(file2Delete)) { 136 return new File[] { file2Delete }; 137 } else { 138 return new File[0]; 139 } 140 } 141 142 private boolean fileExistsAndIsFile(File file2Delete) { 143 return file2Delete.exists() && file2Delete.isFile(); 144 } 145 146 public void cleanPeriod(Instant instantOfPeriodToClean) { 147 File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean); 148 149 File[] uncompressedFileArray = getUncompressedFilesInPeriod(instantOfPeriodToClean); 150 151 for (File f : matchingFileArray) { 152 addInfo("deleting historically stale " + f); 153 checkAndDeleteFile(f); 154 } 155 for (File f : uncompressedFileArray) { 156 addInfo("deleting historically stale uncompressed file " + f); 157 checkAndDeleteFile(f); 158 } 159 160 if (parentClean) { 161 File anyFile = matchingFileArray.length > 0 ? matchingFileArray[0] 162 : (uncompressedFileArray.length > 0 ? uncompressedFileArray[0] : null); 163 if (anyFile != null) { 164 removeFolderIfEmpty(getParentDir(anyFile)); 165 } 166 } 167 } 168 169 private boolean checkAndDeleteFile(File f) { 170 171 if (f == null) { 172 addWarn("Cannot delete empty file"); 173 return false; 174 } else if (!f.exists()) { 175 addWarn("Cannot delete non existent file"); 176 return false; 177 } 178 179 boolean result = f.delete(); 180 if (!result) { 181 addWarn("Failed to delete file " + f.toString()); 182 } 183 return result; 184 } 185 186 void capTotalSize(Instant now) { 187 long totalSize = 0; 188 long totalRemoved = 0; 189 int successfulDeletions = 0; 190 int failedDeletions = 0; 191 192 for (int offset = 0; offset < maxHistory; offset++) { 193 Instant instant = rc.getEndOfNextNthPeriod(now, -offset); 194 File[] matchingFileArray = getFilesInPeriod(instant); 195 descendingSort(matchingFileArray, instant); 196 for (File f : matchingFileArray) { 197 long size = f.length(); 198 //System.out.println("File: " + f + " size=" + size); 199 totalSize += size; 200 if (totalSize > totalSizeCap) { 201 //addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size) + " on account of totalSizeCap " + totalSizeCap); 202 addInfo("Deleting [" + f + "]" + " of size " + size + " on account of totalSizeCap " + totalSizeCap); 203 204 boolean success = checkAndDeleteFile(f); 205 206 if (success) { 207 successfulDeletions++; 208 totalRemoved += size; 209 } else { 210 failedDeletions++; 211 } 212 } 213 } 214 } 215 if ((successfulDeletions + failedDeletions) == 0) { 216 addInfo("No removal attempts were made on account of totalSizeCap="+totalSizeCap); 217 } else { 218 addInfo("Removed " + new FileSize(totalRemoved) + " of files in " + successfulDeletions + " files on account of totalSizeCap=" + totalSizeCap); 219 if (failedDeletions != 0) { 220 addInfo("There were " + failedDeletions + " failed deletion attempts."); 221 } 222 } 223 } 224 225 protected void descendingSort(File[] matchingFileArray, Instant instant) { 226 // nothing to do in super class 227 } 228 229 File getParentDir(File file) { 230 File absolute = file.getAbsoluteFile(); 231 File parentDir = absolute.getParentFile(); 232 return parentDir; 233 } 234 235 int computeElapsedPeriodsSinceLastClean(long nowInMillis) { 236 long periodsElapsed = 0; 237 if (lastHeartBeat == UNINITIALIZED) { 238 addInfo("first clean up after appender initialization"); 239 periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS); 240 periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS); 241 } else { 242 periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis); 243 // periodsElapsed of zero is possible for size and time based policies 244 } 245 return (int) periodsElapsed; 246 } 247 248 /** 249 * Computes whether the fileNamePattern may create sub-folders. 250 * 251 * @param fileNamePattern 252 * @return 253 */ 254 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) { 255 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter(); 256 // if the date pattern has a /, then we need parent cleaning 257 if (dtc.getDatePattern().indexOf('/') != -1) { 258 return true; 259 } 260 // if the literal string after the dtc contains a /, we also 261 // need parent cleaning 262 263 Converter<Object> p = fileNamePattern.headTokenConverter; 264 265 // find the date converter 266 while (p != null) { 267 if (p instanceof DateTokenConverter) { 268 break; 269 } 270 p = p.getNext(); 271 } 272 273 while (p != null) { 274 if (p instanceof LiteralConverter) { 275 String s = p.convert(null); 276 if (s.indexOf('/') != -1) { 277 return true; 278 } 279 } 280 p = p.getNext(); 281 } 282 283 // no '/', so we don't need parent cleaning 284 return false; 285 } 286 287 void removeFolderIfEmpty(File dir) { 288 removeFolderIfEmpty(dir, 0); 289 } 290 291 /** 292 * Will remove the directory passed as parameter if empty. After that, if the parent is also becomes empty, remove 293 * the parent dir as well but at most 3 times. 294 * 295 * @param dir 296 * @param depth 297 */ 298 private void removeFolderIfEmpty(File dir, int depth) { 299 // we should never go more than 3 levels higher 300 if (depth >= 3) { 301 return; 302 } 303 if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) { 304 addInfo("deleting folder [" + dir + "]"); 305 checkAndDeleteFile(dir); 306 removeFolderIfEmpty(dir.getParentFile(), depth + 1); 307 } 308 } 309 310 public void setMaxHistory(int maxHistory) { 311 this.maxHistory = maxHistory; 312 } 313 314 protected int getPeriodOffsetForDeletionTarget() { 315 return -maxHistory - 1; 316 } 317 318 public void setTotalSizeCap(long totalSizeCap) { 319 this.totalSizeCap = totalSizeCap; 320 } 321 322 public String toString() { 323 return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover"; 324 } 325 326 public class ArchiveRemoverRunnable implements Runnable { 327 Instant now; 328 329 ArchiveRemoverRunnable(Instant now) { 330 this.now = now; 331 } 332 333 @Override 334 public void run() { 335 clean(now); 336 if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) { 337 capTotalSize(now); 338 } 339 } 340 } 341 342}