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