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 final RollingCalendar rc; 037 private int maxHistory = CoreConstants.UNBOUNDED_HISTORY; 038 private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 039 final boolean parentClean; 040 long lastHeartBeat = UNINITIALIZED; 041 042 public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) { 043 this.fileNamePattern = fileNamePattern; 044 this.rc = rc; 045 this.parentClean = computeParentCleaningFlag(fileNamePattern); 046 } 047 048 int callCount = 0; 049 050 public Future<?> cleanAsynchronously(Instant now) { 051 ArchiveRemoverRunnable runnable = new ArchiveRemoverRunnable(now); 052 ExecutorService alternateExecutorService = context.getAlternateExecutorService(); 053 Future<?> future = alternateExecutorService.submit(runnable); 054 return future; 055 } 056 057 /** 058 * Called from the cleaning thread. 059 * 060 * @param now 061 */ 062 @Override 063 public void clean(Instant now) { 064 065 long nowInMillis = now.toEpochMilli(); 066 // for a live appender periodsElapsed is expected to be 1 067 int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis); 068 lastHeartBeat = nowInMillis; 069 if (periodsElapsed > 1) { 070 addInfo("Multiple periods, i.e. " + periodsElapsed 071 + " periods, seem to have elapsed. This can happen at application start."); 072 } 073 for (int i = 0; i < periodsElapsed; i++) { 074 int offset = getPeriodOffsetForDeletionTarget() - i; 075 Instant instantOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset); 076 cleanPeriod(instantOfPeriodToClean); 077 } 078 } 079 080 protected File[] getFilesInPeriod(Instant instantOfPeriodToClean) { 081 String filenameToDelete = fileNamePattern.convert(instantOfPeriodToClean); 082 File file2Delete = new File(filenameToDelete); 083 084 if (fileExistsAndIsFile(file2Delete)) { 085 return new File[] { file2Delete }; 086 } else { 087 return new File[0]; 088 } 089 } 090 091 private boolean fileExistsAndIsFile(File file2Delete) { 092 return file2Delete.exists() && file2Delete.isFile(); 093 } 094 095 public void cleanPeriod(Instant instantOfPeriodToClean) { 096 File[] matchingFileArray = getFilesInPeriod(instantOfPeriodToClean); 097 098 for (File f : matchingFileArray) { 099 checkAndDeleteFile(f); 100 } 101 102 if (parentClean && matchingFileArray.length > 0) { 103 File parentDir = getParentDir(matchingFileArray[0]); 104 removeFolderIfEmpty(parentDir); 105 } 106 } 107 108 private boolean checkAndDeleteFile(File f) { 109 addInfo("deleting historically stale " + f); 110 if (f == null) { 111 addWarn("Cannot delete empty file"); 112 return false; 113 } else if (!f.exists()) { 114 addWarn("Cannot delete non existent file"); 115 return false; 116 } 117 118 boolean result = f.delete(); 119 if (!result) { 120 addWarn("Failed to delete file " + f.toString()); 121 } 122 return result; 123 } 124 125 void capTotalSize(Instant now) { 126 long totalSize = 0; 127 long totalRemoved = 0; 128 int successfulDeletions = 0; 129 int failedDeletions = 0; 130 131 for (int offset = 0; offset < maxHistory; offset++) { 132 Instant instant = rc.getEndOfNextNthPeriod(now, -offset); 133 File[] matchingFileArray = getFilesInPeriod(instant); 134 descendingSort(matchingFileArray, instant); 135 for (File f : matchingFileArray) { 136 long size = f.length(); 137 totalSize += size; 138 if (totalSize > totalSizeCap) { 139 addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size) + " on account of totalSizeCap " + totalSizeCap); 140 141 boolean success = checkAndDeleteFile(f); 142 143 if (success) { 144 successfulDeletions++; 145 totalRemoved += size; 146 } else { 147 failedDeletions++; 148 } 149 } 150 } 151 } 152 if ((successfulDeletions + failedDeletions) == 0) { 153 addInfo("No removal attempts were made on account of totalSizeCap="+totalSizeCap); 154 } else { 155 addInfo("Removed " + new FileSize(totalRemoved) + " of files in " + successfulDeletions + " files on account of totalSizeCap=" + totalSizeCap); 156 if (failedDeletions != 0) { 157 addInfo("There were " + failedDeletions + " failed deletion attempts."); 158 } 159 } 160 } 161 162 protected void descendingSort(File[] matchingFileArray, Instant instant) { 163 // nothing to do in super class 164 } 165 166 File getParentDir(File file) { 167 File absolute = file.getAbsoluteFile(); 168 File parentDir = absolute.getParentFile(); 169 return parentDir; 170 } 171 172 int computeElapsedPeriodsSinceLastClean(long nowInMillis) { 173 long periodsElapsed = 0; 174 if (lastHeartBeat == UNINITIALIZED) { 175 addInfo("first clean up after appender initialization"); 176 periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS); 177 periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS); 178 } else { 179 periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis); 180 // periodsElapsed of zero is possible for size and time based policies 181 } 182 return (int) periodsElapsed; 183 } 184 185 /** 186 * Computes whether the fileNamePattern may create sub-folders. 187 * 188 * @param fileNamePattern 189 * @return 190 */ 191 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) { 192 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter(); 193 // if the date pattern has a /, then we need parent cleaning 194 if (dtc.getDatePattern().indexOf('/') != -1) { 195 return true; 196 } 197 // if the literal string after the dtc contains a /, we also 198 // need parent cleaning 199 200 Converter<Object> p = fileNamePattern.headTokenConverter; 201 202 // find the date converter 203 while (p != null) { 204 if (p instanceof DateTokenConverter) { 205 break; 206 } 207 p = p.getNext(); 208 } 209 210 while (p != null) { 211 if (p instanceof LiteralConverter) { 212 String s = p.convert(null); 213 if (s.indexOf('/') != -1) { 214 return true; 215 } 216 } 217 p = p.getNext(); 218 } 219 220 // no '/', so we don't need parent cleaning 221 return false; 222 } 223 224 void removeFolderIfEmpty(File dir) { 225 removeFolderIfEmpty(dir, 0); 226 } 227 228 /** 229 * Will remove the directory passed as parameter if empty. After that, if the parent is also becomes empty, remove 230 * the parent dir as well but at most 3 times. 231 * 232 * @param dir 233 * @param depth 234 */ 235 private void removeFolderIfEmpty(File dir, int depth) { 236 // we should never go more than 3 levels higher 237 if (depth >= 3) { 238 return; 239 } 240 if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) { 241 addInfo("deleting folder [" + dir + "]"); 242 checkAndDeleteFile(dir); 243 removeFolderIfEmpty(dir.getParentFile(), depth + 1); 244 } 245 } 246 247 public void setMaxHistory(int maxHistory) { 248 this.maxHistory = maxHistory; 249 } 250 251 protected int getPeriodOffsetForDeletionTarget() { 252 return -maxHistory - 1; 253 } 254 255 public void setTotalSizeCap(long totalSizeCap) { 256 this.totalSizeCap = totalSizeCap; 257 } 258 259 public String toString() { 260 return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover"; 261 } 262 263 public class ArchiveRemoverRunnable implements Runnable { 264 Instant now; 265 266 ArchiveRemoverRunnable(Instant now) { 267 this.now = now; 268 } 269 270 @Override 271 public void run() { 272 clean(now); 273 if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) { 274 capTotalSize(now); 275 } 276 } 277 } 278 279}