View Javadoc
1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v1.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  package ch.qos.logback.core.rolling.helper;
15  
16  import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
17  
18  import java.io.File;
19  import java.util.Date;
20  import java.util.concurrent.ExecutorService;
21  import java.util.concurrent.Future;
22  
23  import ch.qos.logback.core.CoreConstants;
24  import ch.qos.logback.core.pattern.Converter;
25  import ch.qos.logback.core.pattern.LiteralConverter;
26  import ch.qos.logback.core.spi.ContextAwareBase;
27  import ch.qos.logback.core.util.FileSize;
28  
29  public class TimeBasedArchiveRemover extends ContextAwareBase implements ArchiveRemover {
30  
31      static protected final long UNINITIALIZED = -1;
32      // aim for 32 days, except in case of hourly rollover
33      static protected final long INACTIVITY_TOLERANCE_IN_MILLIS = 32L * (long) CoreConstants.MILLIS_IN_ONE_DAY;
34      static final int MAX_VALUE_FOR_INACTIVITY_PERIODS = 14 * 24; // 14 days in case of hourly rollover
35  
36      final FileNamePattern fileNamePattern;
37      final RollingCalendar rc;
38      private int maxHistory = CoreConstants.UNBOUND_HISTORY;
39      private long totalSizeCap = CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP;
40      final boolean parentClean;
41      long lastHeartBeat = UNINITIALIZED;
42  
43      public TimeBasedArchiveRemover(FileNamePattern fileNamePattern, RollingCalendar rc) {
44          this.fileNamePattern = fileNamePattern;
45          this.rc = rc;
46          this.parentClean = computeParentCleaningFlag(fileNamePattern);
47      }
48  
49      int callCount = 0;
50      public void clean(Date now) {
51   
52          long nowInMillis = now.getTime();
53          // for a live appender periodsElapsed is expected to be 1
54          int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
55          lastHeartBeat = nowInMillis;
56          if (periodsElapsed > 1) {
57              addInfo("Multiple periods, i.e. " + periodsElapsed + " periods, seem to have elapsed. This is expected at application start.");
58          }
59          for (int i = 0; i < periodsElapsed; i++) {
60              int offset = getPeriodOffsetForDeletionTarget() - i;
61              Date dateOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
62              cleanPeriod(dateOfPeriodToClean);
63          }
64      }
65  
66      protected File[] getFilesInPeriod(Date dateOfPeriodToClean) {
67          String filenameToDelete = fileNamePattern.convert(dateOfPeriodToClean);
68          File file2Delete = new File(filenameToDelete);
69  
70          if (fileExistsAndIsFile(file2Delete)) {
71              return new File[] { file2Delete };
72          } else {
73              return new File[0];
74          }
75      }
76  
77      private boolean fileExistsAndIsFile(File file2Delete) {
78          return file2Delete.exists() && file2Delete.isFile();
79      }
80  
81      public void cleanPeriod(Date dateOfPeriodToClean) {
82          File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean);
83  
84          for (File f : matchingFileArray) {
85              addInfo("deleting " + f);
86              f.delete();
87          }
88  
89          if (parentClean && matchingFileArray.length > 0) {
90              File parentDir = getParentDir(matchingFileArray[0]);
91              removeFolderIfEmpty(parentDir);
92          }
93      }
94  
95      void capTotalSize(Date now) {
96          long totalSize = 0;
97          long totalRemoved = 0;
98          for (int offset = 0; offset < maxHistory; offset++) {
99              Date date = rc.getEndOfNextNthPeriod(now, -offset);
100             File[] matchingFileArray = getFilesInPeriod(date);
101             descendingSort(matchingFileArray, date);
102             for (File f : matchingFileArray) {
103                 long size = f.length();
104                 if (totalSize + size > totalSizeCap) {
105                     addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size));
106                     totalRemoved += size;
107                     f.delete();
108                 }
109                 totalSize += size;
110             }
111         }
112         addInfo("Removed  " + new FileSize(totalRemoved) + " of files");
113     }
114 
115     
116     protected void descendingSort(File[] matchingFileArray, Date date) {
117         // nothing to do in super class
118     }
119 
120     File getParentDir(File file) {
121         File absolute = file.getAbsoluteFile();
122         File parentDir = absolute.getParentFile();
123         return parentDir;
124     }
125 
126     int computeElapsedPeriodsSinceLastClean(long nowInMillis) {
127         long periodsElapsed = 0;
128         if (lastHeartBeat == UNINITIALIZED) {
129             addInfo("first clean up after appender initialization");
130             periodsElapsed = rc.periodBarriersCrossed(nowInMillis, nowInMillis + INACTIVITY_TOLERANCE_IN_MILLIS);
131             periodsElapsed = Math.min(periodsElapsed, MAX_VALUE_FOR_INACTIVITY_PERIODS);
132         } else {
133             periodsElapsed = rc.periodBarriersCrossed(lastHeartBeat, nowInMillis);
134             // periodsElapsed of zero is possible for size and time based policies
135         }
136         return (int) periodsElapsed;
137     }
138 
139     /**
140      * Computes whether the fileNamePattern may create sub-folders.
141      * @param fileNamePattern
142      * @return
143      */
144     boolean computeParentCleaningFlag(FileNamePattern fileNamePattern) {
145         DateTokenConverter<Object> dtc = fileNamePattern.getPrimaryDateTokenConverter();
146         // if the date pattern has a /, then we need parent cleaning
147         if (dtc.getDatePattern().indexOf('/') != -1) {
148             return true;
149         }
150         // if the literal string subsequent to the dtc contains a /, we also
151         // need parent cleaning
152 
153         Converter<Object> p = fileNamePattern.headTokenConverter;
154 
155         // find the date converter
156         while (p != null) {
157             if (p instanceof DateTokenConverter) {
158                 break;
159             }
160             p = p.getNext();
161         }
162 
163         while (p != null) {
164             if (p instanceof LiteralConverter) {
165                 String s = p.convert(null);
166                 if (s.indexOf('/') != -1) {
167                     return true;
168                 }
169             }
170             p = p.getNext();
171         }
172 
173         // no '/', so we don't need parent cleaning
174         return false;
175     }
176 
177     void removeFolderIfEmpty(File dir) {
178         removeFolderIfEmpty(dir, 0);
179     }
180 
181     /**
182      * Will remove the directory passed as parameter if empty. After that, if the
183      * parent is also becomes empty, remove the parent dir as well but at most 3
184      * times.
185      *
186      * @param dir
187      * @param depth
188      */
189     private void removeFolderIfEmpty(File dir, int depth) {
190         // we should never go more than 3 levels higher
191         if (depth >= 3) {
192             return;
193         }
194         if (dir.isDirectory() && FileFilterUtil.isEmptyDirectory(dir)) {
195             addInfo("deleting folder [" + dir + "]");
196             dir.delete();
197             removeFolderIfEmpty(dir.getParentFile(), depth + 1);
198         }
199     }
200 
201     public void setMaxHistory(int maxHistory) {
202         this.maxHistory = maxHistory;
203     }
204 
205     protected int getPeriodOffsetForDeletionTarget() {
206         return -maxHistory - 1;
207     }
208 
209     public void setTotalSizeCap(long totalSizeCap) {
210         this.totalSizeCap = totalSizeCap;
211     }
212 
213     public String toString() {
214         return "c.q.l.core.rolling.helper.TimeBasedArchiveRemover";
215     }
216 
217     public Future<?> cleanAsynchronously(Date now) {
218         ArhiveRemoverRunnable runnable = new ArhiveRemoverRunnable(now);
219         ExecutorService executorService = context.getScheduledExecutorService();
220         Future<?> future = executorService.submit(runnable);
221         return future;
222     }
223 
224     public class ArhiveRemoverRunnable implements Runnable {
225         Date now;
226 
227         ArhiveRemoverRunnable(Date now) {
228             this.now = now;
229         }
230 
231         @Override
232         public void run() {
233             clean(now);
234             if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
235                 capTotalSize(now);
236             }
237         }
238     }
239 
240 }