/** * Logback: the reliable, generic, fast and flexible logging framework. * Copyright (C) 1999-2015, QOS.ch. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation * * or (per the licensee's choosing) * * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. */ package ch.qos.logback.core.rolling.helper; import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; import java.io.File; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.pattern.Converter; import ch.qos.logback.core.pattern.LiteralConverter; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.util.FileSize; public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover { static protected final long UNINITIALIZED = -1; // aim for 32 days, except in case of hourly rollover static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY; static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover final FileNamePattern fileNamePattern; final RollingCalendar rc; private int maxHistory = CoreConstants.UNBOUND_HISTORY; private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; final boolean parentClean; long lastHeartBeat = UNINITIALIZED; public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) { this.fileNamePattern = fileNamePattern; this.rc = rc; this.parentClean = computeParentCleaningFlag(fileNamePattern); } int callCount = 0; public void clean(Date now) { long nowInMillis = now.getTime(); // for a live appender periodsElapsed is expected to be 1 int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis); lastHeartBeat = nowInMillis; if (periodsElapsed > 1) { addInfo("Multiple periods, i.e. " + periodsElapsed + " periods, seem to have elapsed. This is expected at application start."); } for (int i = 0; i < periodsElapsed; i++) { int offset = getPeriodOffsetForDeletionTarget() - i; Date dateOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset); cleanPeriod(dateOfPeriodToClean); } } protected File[] getFilesInPeriod(Date dateOfPeriodToClean) { String filenameToDelete = fileNamePattern.convert(dateOfPeriodToClean); File file2Delete = new File(filenameToDelete); if (fileExistsAndIsFile(file2Delete)) { return new File[] { file2Delete }; } else { return new File[0]; } } private boolean fileExistsAndIsFile(File file2Delete) { return file2Delete.exists() && file2Delete.isFile(); } public void cleanPeriod(Date dateOfPeriodToClean) { File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean); for (File f : matchingFileArray) { addInfo("deleting " + f); f.delete(); } if (parentClean && matchingFileArray.length > 0) { File parentDir = getParentDir(matchingFileArray[0]); removeFolderIfEmpty(parentDir); } } void capTotalSize(Date now) { long totalSize = 0; long totalRemoved = 0; for (int offset = 0; offset < maxHistory; offset++) { Date date = rc.getEndOfNextNthPeriod(now, -offset); File[] matchingFileArray = getFilesInPeriod(date); descendingSortByLastModified(matchingFileArray); for (File f : matchingFileArray) { long size = f.length(); if (totalSize + size > totalSizeCap) { addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size)); totalRemoved += size; f.delete(); } totalSize += size; } } addInfo("Removed " + new FileSize(totalRemoved) + " of files"); } private void descendingSortByLastModified(File[] matchingFileArray) { Arrays.sort(matchingFileArray, new Comparator<File>() { @Override public int compare(final File f1, final File f2) { long l1 = f1.lastModified(); long l2 = f2.lastModified(); if (l1 == l2) return 0; // descending sort, i.e. newest files first if (l2 < l1) return -1; else return 1; } }); } File getParentDir(File file) { File absolute = file.getAbsoluteFile(); File parentDir = absolute.getParentFile(); return parentDir; } int computeElapsedPeriodsSinceLastClean(long nowInMillis) { long periodsElapsed = 0; if (lastHeartBeat == UNINITIALIZED) { addInfo("first clean up after appender initialization"); periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS); periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS); } else { periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis); // periodsElapsed of zero is possible for size and time based policies } return (int) periodsElapsed; } boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) { DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter(); // if the date pattern has a /, then we need parent cleaning if (dtc.getDatePattern().indexOf('/') != -1) { return true; } // if the literal string subsequent to the dtc contains a /, we also // need parent cleaning Converter<Object> p = fileNamePattern.headTokenConverter; // find the date converter while (p != null) { if (p instanceof DateTokenConverter) { break; } p = p.getNext(); } while (p != null) { if (p instanceof LiteralConverter) { String s = p.convert(null); if (s.indexOf('/') != -1) { return true; } } p = p.getNext(); } // no /, so we don't need parent cleaning return false; } void removeFolderIfEmpty(File dir) { removeFolderIfEmpty(dir, 0); } /** * Will remove the directory passed as parameter if empty. After that, if the * parent is also becomes empty, remove the parent dir as well but at most 3 * times. * * @param dir * @param depth */ private void removeFolderIfEmpty(File dir, int depth) { // we should never go more than 3 levels higher if (depth >= 3) { return; } if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) { addInfo("deleting folder [" + dir + "]"); dir.delete(); removeFolderIfEmpty(dir.getParentFile(), depth + 1); } } public void setMaxHistory(int maxHistory) { this.maxHistory = maxHistory; } protected int getPeriodOffsetForDeletionTarget() { return -maxHistory - 1; } public void setTotalSizeCap(long totalSizeCap) { this.totalSizeCap = totalSizeCap; } public String toString() { return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover"; } public Future<?> cleanAsynchronously(Date now) { ArhiveRemoverRunnable runnable = new ArhiveRemoverRunnable(now); ExecutorService executorService = context.getScheduledExecutorService(); Future<?> future = executorService.submit(runnable); return future; } public class ArhiveRemoverRunnable implements Runnable { Date now; ArhiveRemoverRunnable(Date now) { this.now = now; } @Override public void run() { clean(now); if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) { capTotalSize(now); } } } }