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 is expected 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 " + 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)); 140 141 boolean success = checkAndDeleteFile(f); 142 if (success) { 143 successfulDeletions++; 144 totalRemoved += size; 145 } else { 146 failedDeletions++; 147 } 148 } 149 } 150 } 151 if ((successfulDeletions + failedDeletions) == 0) { 152 addInfo("No removal attempts were made."); 153 } else { 154 addInfo("Removed " + new FileSize(totalRemoved) + " of files in " + successfulDeletions + " files."); 155 if (failedDeletions != 0) { 156 addInfo("There were " + failedDeletions + " failed deletion attempts."); 157 } 158 } 159 } 160 161 protected void descendingSort(File[] matchingFileArray, Instant instant) { 162 // nothing to do in super class 163 } 164 165 File getParentDir(File file) { 166 File absolute = file.getAbsoluteFile(); 167 File parentDir = absolute.getParentFile(); 168 return parentDir; 169 } 170 171 int computeElapsedPeriodsSinceLastClean(long nowInMillis) { 172 long periodsElapsed = 0; 173 if (lastHeartBeat == UNINITIALIZED) { 174 addInfo("first clean up after appender initialization"); 175 periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS); 176 periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS); 177 } else { 178 periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis); 179 // periodsElapsed of zero is possible for size and time based policies 180 } 181 return (int) periodsElapsed; 182 } 183 184 /** 185 * Computes whether the fileNamePattern may create sub-folders. 186 * 187 * @param fileNamePattern 188 * @return 189 */ 190 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) { 191 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter(); 192 // if the date pattern has a /, then we need parent cleaning 193 if (dtc.getDatePattern().indexOf('/') != -1) { 194 return true; 195 } 196 // if the literal string after the dtc contains a /, we also 197 // need parent cleaning 198 199 Converter<Object> p = fileNamePattern.headTokenConverter; 200 201 // find the date converter 202 while (p != null) { 203 if (p instanceof DateTokenConverter) { 204 break; 205 } 206 p = p.getNext(); 207 } 208 209 while (p != null) { 210 if (p instanceof LiteralConverter) { 211 String s = p.convert(null); 212 if (s.indexOf('/') != -1) { 213 return true; 214 } 215 } 216 p = p.getNext(); 217 } 218 219 // no '/', so we don't need parent cleaning 220 return false; 221 } 222 223 void removeFolderIfEmpty(File dir) { 224 removeFolderIfEmpty(dir, 0); 225 } 226 227 /** 228 * Will remove the directory passed as parameter if empty. After that, if the parent is also becomes empty, remove 229 * the parent dir as well but at most 3 times. 230 * 231 * @param dir 232 * @param depth 233 */ 234 private void removeFolderIfEmpty(File dir, int depth) { 235 // we should never go more than 3 levels higher 236 if (depth >= 3) { 237 return; 238 } 239 if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) { 240 addInfo("deleting folder [" + dir + "]"); 241 checkAndDeleteFile(dir); 242 removeFolderIfEmpty(dir.getParentFile(), depth + 1); 243 } 244 } 245 246 public void setMaxHistory(int maxHistory) { 247 this.maxHistory = maxHistory; 248 } 249 250 protected int getPeriodOffsetForDeletionTarget() { 251 return -maxHistory - 1; 252 } 253 254 public void setTotalSizeCap(long totalSizeCap) { 255 this.totalSizeCap = totalSizeCap; 256 } 257 258 public String toString() { 259 return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover"; 260 } 261 262 public class ArchiveRemoverRunnable implements Runnable { 263 Instant now; 264 265 ArchiveRemoverRunnable(Instant now) { 266 this.now = now; 267 } 268 269 @Override 270 public void run() { 271 clean(now); 272 if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) { 273 capTotalSize(now); 274 } 275 } 276 } 277 278}