001/** 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v1.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.core.rolling.helper; 015 016import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 017 018import java.io.File; 019import java.util.Date; 020import java.util.concurrent.ExecutorService; 021import java.util.concurrent.Future; 022 023import ch.qos.logback.core.CoreConstants; 024import ch.qos.logback.core.pattern.Converter; 025import ch.qos.logback.core.pattern.LiteralConverter; 026import ch.qos.logback.core.spi.ContextAwareBase; 027import ch.qos.logback.core.util.FileSize; 028 029public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover { 030 031 static protected final long UNINITIALIZED = -1; 032 // aim for 32 days, except in case of hourly rollover, see 033 // MAX_VALUE_FOR_INACTIVITY_PERIODS 034 static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY; 035 static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover 036 037 final FileNamePattern fileNamePattern; 038 final RollingCalendar rc; 039 private int maxHistory = CoreConstants.UNBOUNDED_HISTORY; 040 private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 041 final boolean parentClean; 042 long lastHeartBeat = UNINITIALIZED; 043 044 public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) { 045 this.fileNamePattern = fileNamePattern; 046 this.rc = rc; 047 this.parentClean = computeParentCleaningFlag(fileNamePattern); 048 } 049 050 int callCount = 0; 051 052 public void clean(Date now) { 053 054 long nowInMillis = now.getTime(); 055 // for a live appender periodsElapsed is expected to be 1 056 int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis); 057 lastHeartBeat = nowInMillis; 058 if (periodsElapsed > 1) { 059 addInfo("Multiple periods, i.e. " + periodsElapsed 060 + " periods, seem to have elapsed. This is expected at application start."); 061 } 062 for (int i = 0; i < periodsElapsed; i++) { 063 int offset = getPeriodOffsetForDeletionTarget() - i; 064 Date dateOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset); 065 cleanPeriod(dateOfPeriodToClean); 066 } 067 } 068 069 protected File[] getFilesInPeriod(Date dateOfPeriodToClean) { 070 String filenameToDelete = fileNamePattern.convert(dateOfPeriodToClean); 071 File file2Delete = new File(filenameToDelete); 072 073 if (fileExistsAndIsFile(file2Delete)) { 074 return new File[] { file2Delete }; 075 } else { 076 return new File[0]; 077 } 078 } 079 080 private boolean fileExistsAndIsFile(File file2Delete) { 081 return file2Delete.exists() && file2Delete.isFile(); 082 } 083 084 public void cleanPeriod(Date dateOfPeriodToClean) { 085 File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean); 086 087 for (File f : matchingFileArray) { 088 addInfo("deleting " + f); 089 f.delete(); 090 } 091 092 if (parentClean && matchingFileArray.length > 0) { 093 File parentDir = getParentDir(matchingFileArray[0]); 094 removeFolderIfEmpty(parentDir); 095 } 096 } 097 098 void capTotalSize(Date now) { 099 long totalSize = 0; 100 long totalRemoved = 0; 101 for (int offset = 0; offset < maxHistory; offset++) { 102 Date date = rc.getEndOfNextNthPeriod(now, -offset); 103 File[] matchingFileArray = getFilesInPeriod(date); 104 descendingSort(matchingFileArray, date); 105 for (File f : matchingFileArray) { 106 long size = f.length(); 107 if (totalSize + size > totalSizeCap) { 108 addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size)); 109 totalRemoved += size; 110 f.delete(); 111 } 112 totalSize += size; 113 } 114 } 115 addInfo("Removed " + new FileSize(totalRemoved) + " of files"); 116 } 117 118 protected void descendingSort(File[] matchingFileArray, Date date) { 119 // nothing to do in super class 120 } 121 122 File getParentDir(File file) { 123 File absolute = file.getAbsoluteFile(); 124 File parentDir = absolute.getParentFile(); 125 return parentDir; 126 } 127 128 int computeElapsedPeriodsSinceLastClean(long nowInMillis) { 129 long periodsElapsed = 0; 130 if (lastHeartBeat == UNINITIALIZED) { 131 addInfo("first clean up after appender initialization"); 132 periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS); 133 periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS); 134 } else { 135 periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis); 136 // periodsElapsed of zero is possible for size and time based policies 137 } 138 return (int) periodsElapsed; 139 } 140 141 /** 142 * Computes whether the fileNamePattern may create sub-folders. 143 * 144 * @param fileNamePattern 145 * @return 146 */ 147 boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) { 148 DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter(); 149 // if the date pattern has a /, then we need parent cleaning 150 if (dtc.getDatePattern().indexOf('/') != -1) { 151 return true; 152 } 153 // if the literal string after the dtc contains a /, we also 154 // need parent cleaning 155 156 Converter<Object> p = fileNamePattern.headTokenConverter; 157 158 // find the date converter 159 while (p != null) { 160 if (p instanceof DateTokenConverter) { 161 break; 162 } 163 p = p.getNext(); 164 } 165 166 while (p != null) { 167 if (p instanceof LiteralConverter) { 168 String s = p.convert(null); 169 if (s.indexOf('/') != -1) { 170 return true; 171 } 172 } 173 p = p.getNext(); 174 } 175 176 // no '/', so we don't need parent cleaning 177 return false; 178 } 179 180 void removeFolderIfEmpty(File dir) { 181 removeFolderIfEmpty(dir, 0); 182 } 183 184 /** 185 * Will remove the directory passed as parameter if empty. After that, if the 186 * parent is also becomes empty, remove the parent dir as well but at most 3 187 * times. 188 * 189 * @param dir 190 * @param depth 191 */ 192 private void removeFolderIfEmpty(File dir, int depth) { 193 // we should never go more than 3 levels higher 194 if (depth >= 3) { 195 return; 196 } 197 if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) { 198 addInfo("deleting folder [" + dir + "]"); 199 dir.delete(); 200 removeFolderIfEmpty(dir.getParentFile(), depth + 1); 201 } 202 } 203 204 public void setMaxHistory(int maxHistory) { 205 this.maxHistory = maxHistory; 206 } 207 208 protected int getPeriodOffsetForDeletionTarget() { 209 return -maxHistory - 1; 210 } 211 212 public void setTotalSizeCap(long totalSizeCap) { 213 this.totalSizeCap = totalSizeCap; 214 } 215 216 public String toString() { 217 return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover"; 218 } 219 220 public Future<?> cleanAsynchronously(Date now) { 221 ArhiveRemoverRunnable runnable = new ArhiveRemoverRunnable(now); 222 ExecutorService executorService = context.getScheduledExecutorService(); 223 Future<?> future = executorService.submit(runnable); 224 return future; 225 } 226 227 public class ArhiveRemoverRunnable implements Runnable { 228 Date now; 229 230 ArhiveRemoverRunnable(Date now) { 231 this.now = now; 232 } 233 234 @Override 235 public void run() { 236 clean(now); 237 if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) { 238 capTotalSize(now); 239 } 240 } 241 } 242 243}