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