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&uuml;lc&uuml;
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}