001/** 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2022, 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; 015 016import static ch.qos.logback.core.CoreConstants.UNBOUNDED_HISTORY; 017import static ch.qos.logback.core.CoreConstants.UNBOUNDED_TOTAL_SIZE_CAP; 018 019import java.io.File; 020import java.time.Instant; 021import java.util.Date; 022import java.util.concurrent.Future; 023import java.util.concurrent.TimeUnit; 024import java.util.concurrent.TimeoutException; 025 026import ch.qos.logback.core.CoreConstants; 027import ch.qos.logback.core.rolling.helper.ArchiveRemover; 028import ch.qos.logback.core.rolling.helper.CompressionMode; 029import ch.qos.logback.core.rolling.helper.Compressor; 030import ch.qos.logback.core.rolling.helper.FileFilterUtil; 031import ch.qos.logback.core.rolling.helper.FileNamePattern; 032import ch.qos.logback.core.rolling.helper.RenameUtil; 033import ch.qos.logback.core.util.FileSize; 034 035/** 036 * <code>TimeBasedRollingPolicy</code> is both easy to configure and quite 037 * powerful. It allows the rollover to be made based on time. It is possible to 038 * specify that the rollover occur once per day, per week or per month. 039 * 040 * <p> 041 * For more information, please refer to the online manual at 042 * http://logback.qos.ch/manual/appenders.html#TimeBasedRollingPolicy 043 * 044 * @author Ceki Gülcü 045 */ 046public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> { 047 static final String FNP_NOT_SET = "The FileNamePattern option must be set before using TimeBasedRollingPolicy. "; 048 // WCS: without compression suffix 049 FileNamePattern fileNamePatternWithoutCompSuffix; 050 051 private Compressor compressor; 052 private RenameUtil renameUtil = new RenameUtil(); 053 Future<?> compressionFuture; 054 Future<?> cleanUpFuture; 055 056 private int maxHistory = UNBOUNDED_HISTORY; 057 protected FileSize totalSizeCap = new FileSize(UNBOUNDED_TOTAL_SIZE_CAP); 058 059 private ArchiveRemover archiveRemover; 060 061 TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedFileNamingAndTriggeringPolicy; 062 063 boolean cleanHistoryOnStart = false; 064 065 public void start() { 066 // set the LR for our utility object 067 renameUtil.setContext(this.context); 068 069 // find out period from the filename pattern 070 if (fileNamePatternStr != null) { 071 fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context); 072 determineCompressionMode(); 073 } else { 074 addWarn(FNP_NOT_SET); 075 addWarn(CoreConstants.SEE_FNP_NOT_SET); 076 throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET); 077 } 078 079 compressor = new Compressor(compressionMode); 080 compressor.setContext(context); 081 082 // wcs : without compression suffix 083 fileNamePatternWithoutCompSuffix = new FileNamePattern( 084 Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context); 085 086 addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file"); 087 088 if (compressionMode == CompressionMode.ZIP) { 089 String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr); 090 zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context); 091 } 092 093 if (timeBasedFileNamingAndTriggeringPolicy == null) { 094 timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>(); 095 } 096 timeBasedFileNamingAndTriggeringPolicy.setContext(context); 097 timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this); 098 timeBasedFileNamingAndTriggeringPolicy.start(); 099 100 if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) { 101 addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start."); 102 return; 103 } 104 105 // the maxHistory property is given to TimeBasedRollingPolicy instead of to 106 // the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient 107 // for the user at the cost of inconsistency here. 108 if (maxHistory != UNBOUNDED_HISTORY) { 109 archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover(); 110 archiveRemover.setMaxHistory(maxHistory); 111 archiveRemover.setTotalSizeCap(totalSizeCap.getSize()); 112 if (cleanHistoryOnStart) { 113 addInfo("Cleaning on start up"); 114 Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime()); 115 cleanUpFuture = archiveRemover.cleanAsynchronously(now); 116 } 117 } else if (!isUnboundedTotalSizeCap()) { 118 addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value [" + totalSizeCap + "]"); 119 } 120 121 super.start(); 122 } 123 124 protected boolean isUnboundedTotalSizeCap() { 125 return totalSizeCap.getSize() == UNBOUNDED_TOTAL_SIZE_CAP; 126 } 127 128 @Override 129 public void stop() { 130 if (!isStarted()) 131 return; 132 waitForAsynchronousJobToStop(compressionFuture, "compression"); 133 waitForAsynchronousJobToStop(cleanUpFuture, "clean-up"); 134 super.stop(); 135 } 136 137 private void waitForAsynchronousJobToStop(Future<?> aFuture, String jobDescription) { 138 if (aFuture != null) { 139 try { 140 aFuture.get(CoreConstants.SECONDS_TO_WAIT_FOR_COMPRESSION_JOBS, TimeUnit.SECONDS); 141 } catch (TimeoutException e) { 142 addError("Timeout while waiting for " + jobDescription + " job to finish", e); 143 } catch (Exception e) { 144 addError("Unexpected exception while waiting for " + jobDescription + " job to finish", e); 145 } 146 } 147 } 148 149 private String transformFileNamePattern2ZipEntry(String fileNamePatternStr) { 150 String slashified = FileFilterUtil.slashify(fileNamePatternStr); 151 return FileFilterUtil.afterLastSlash(slashified); 152 } 153 154 public void setTimeBasedFileNamingAndTriggeringPolicy( 155 TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedTriggering) { 156 this.timeBasedFileNamingAndTriggeringPolicy = timeBasedTriggering; 157 } 158 159 public TimeBasedFileNamingAndTriggeringPolicy<E> getTimeBasedFileNamingAndTriggeringPolicy() { 160 return timeBasedFileNamingAndTriggeringPolicy; 161 } 162 163 public void rollover() throws RolloverFailure { 164 165 // when rollover is called the elapsed period's file has 166 // been already closed. This is a working assumption of this method. 167 168 String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName(); 169 170 String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName); 171 172 if (compressionMode == CompressionMode.NONE) { 173 if (getParentsRawFileProperty() != null) { 174 renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName); 175 } // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == 176 // null } 177 } else { 178 if (getParentsRawFileProperty() == null) { 179 compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, 180 elapsedPeriodStem); 181 } else { 182 compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem); 183 } 184 } 185 186 if (archiveRemover != null) { 187 Instant now = Instant.ofEpochMilli(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime()); 188 this.cleanUpFuture = archiveRemover.cleanAsynchronously(now); 189 } 190 } 191 192 Future<?> renameRawAndAsyncCompress(String nameOfCompressedFile, String innerEntryName) throws RolloverFailure { 193 String parentsRawFile = getParentsRawFileProperty(); 194 String tmpTarget = nameOfCompressedFile + System.nanoTime() + ".tmp"; 195 renameUtil.rename(parentsRawFile, tmpTarget); 196 return compressor.asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName); 197 } 198 199 /** 200 * 201 * The active log file is determined by the value of the parent's filename 202 * option. However, in case the file name is left blank, then, the active log 203 * file equals the file name for the current period as computed by the 204 * <b>FileNamePattern</b> option. 205 * 206 * <p> 207 * The RollingPolicy must know whether it is responsible for changing the name 208 * of the active file or not. If the active file name is set by the user via the 209 * configuration file, then the RollingPolicy must let it like it is. If the 210 * user does not specify an active file name, then the RollingPolicy generates 211 * one. 212 * 213 * <p> 214 * To be sure that the file name used by the parent class has been generated by 215 * the RollingPolicy and not specified by the user, we keep track of the last 216 * generated name object and compare its reference to the parent file name. If 217 * they match, then the RollingPolicy knows it's responsible for the change of 218 * the file name. 219 * 220 */ 221 public String getActiveFileName() { 222 String parentsRawFileProperty = getParentsRawFileProperty(); 223 if (parentsRawFileProperty != null) { 224 return parentsRawFileProperty; 225 } else { 226 return timeBasedFileNamingAndTriggeringPolicy.getCurrentPeriodsFileNameWithoutCompressionSuffix(); 227 } 228 } 229 230 /** 231 * Delegates to the underlying timeBasedFileNamingAndTriggeringPolicy. 232 * 233 * @param activeFile A reference to the currently active log file. 234 * @param event A reference to the current event. 235 * @return 236 */ 237 public boolean isTriggeringEvent(File activeFile, final E event) { 238 return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event); 239 } 240 241 /** 242 * Get the number of archive files to keep. 243 * 244 * @return number of archive files to keep 245 */ 246 public int getMaxHistory() { 247 return maxHistory; 248 } 249 250 /** 251 * Set the maximum number of archive files to keep. 252 * 253 * @param maxHistory number of archive files to keep 254 */ 255 public void setMaxHistory(int maxHistory) { 256 this.maxHistory = maxHistory; 257 } 258 259 public boolean isCleanHistoryOnStart() { 260 return cleanHistoryOnStart; 261 } 262 263 /** 264 * Should archive removal be attempted on application start up? Default is 265 * false. 266 * 267 * @since 1.0.1 268 * @param cleanHistoryOnStart 269 */ 270 public void setCleanHistoryOnStart(boolean cleanHistoryOnStart) { 271 this.cleanHistoryOnStart = cleanHistoryOnStart; 272 } 273 274 @Override 275 public String toString() { 276 return "c.q.l.core.rolling.TimeBasedRollingPolicy@" + this.hashCode(); 277 } 278 279 public void setTotalSizeCap(FileSize totalSizeCap) { 280 addInfo("setting totalSizeCap to " + totalSizeCap.toString()); 281 this.totalSizeCap = totalSizeCap; 282 } 283}