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