001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, 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.util.Date;
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            fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
071            determineCompressionMode();
072        } else {
073            addWarn(FNP_NOT_SET);
074            addWarn(CoreConstants.SEE_FNP_NOT_SET);
075            throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET);
076        }
077
078        compressor = new Compressor(compressionMode);
079        compressor.setContext(context);
080
081        // wcs : without compression suffix
082        fileNamePatternWithoutCompSuffix = new FileNamePattern(
083                Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context);
084
085        addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file");
086
087        if (compressionMode == CompressionMode.ZIP) {
088            String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr);
089            zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context);
090        }
091
092        if (timeBasedFileNamingAndTriggeringPolicy == null) {
093            timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>();
094        }
095        timeBasedFileNamingAndTriggeringPolicy.setContext(context);
096        timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
097        timeBasedFileNamingAndTriggeringPolicy.start();
098
099        if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
100            addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
101            return;
102        }
103
104        // the maxHistory property is given to TimeBasedRollingPolicy instead of to
105        // the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient
106        // for the user at the cost of inconsistency here.
107        if (maxHistory != UNBOUNDED_HISTORY) {
108            archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
109            archiveRemover.setMaxHistory(maxHistory);
110            archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
111            if (cleanHistoryOnStart) {
112                addInfo("Cleaning on start up");
113                Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
114                cleanUpFuture = archiveRemover.cleanAsynchronously(now);
115            }
116        } else if (!isUnboundedTotalSizeCap()) {
117            addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value [" + totalSizeCap + "]");
118        }
119
120        super.start();
121    }
122
123    protected boolean isUnboundedTotalSizeCap() {
124        return totalSizeCap.getSize() == UNBOUNDED_TOTAL_SIZE_CAP;
125    }
126
127    @Override
128    public void stop() {
129        if (!isStarted())
130            return;
131        waitForAsynchronousJobToStop(compressionFuture, "compression");
132        waitForAsynchronousJobToStop(cleanUpFuture, "clean-up");
133        super.stop();
134    }
135
136    private void waitForAsynchronousJobToStop(Future<?> aFuture, String jobDescription) {
137        if (aFuture != null) {
138            try {
139                aFuture.get(CoreConstants.SECONDS_TO_WAIT_FOR_COMPRESSION_JOBS, TimeUnit.SECONDS);
140            } catch (TimeoutException e) {
141                addError("Timeout while waiting for " + jobDescription + " job to finish", e);
142            } catch (Exception e) {
143                addError("Unexpected exception while waiting for " + jobDescription + " job to finish", e);
144            }
145        }
146    }
147
148    private String transformFileNamePattern2ZipEntry(String fileNamePatternStr) {
149        String slashified = FileFilterUtil.slashify(fileNamePatternStr);
150        return FileFilterUtil.afterLastSlash(slashified);
151    }
152
153    public void setTimeBasedFileNamingAndTriggeringPolicy(
154            TimeBasedFileNamingAndTriggeringPolicy<E> timeBasedTriggering) {
155        this.timeBasedFileNamingAndTriggeringPolicy = timeBasedTriggering;
156    }
157
158    public TimeBasedFileNamingAndTriggeringPolicy<E> getTimeBasedFileNamingAndTriggeringPolicy() {
159        return timeBasedFileNamingAndTriggeringPolicy;
160    }
161
162    public void rollover() throws RolloverFailure {
163
164        // when rollover is called the elapsed period's file has
165        // been already closed. This is a working assumption of this method.
166
167        String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
168
169        String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
170
171        if (compressionMode == CompressionMode.NONE) {
172            if (getParentsRawFileProperty() != null) {
173                renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
174            } // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty ==
175              // null }
176        } else {
177            if (getParentsRawFileProperty() == null) {
178                compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName,
179                        elapsedPeriodStem);
180            } else {
181                compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
182            }
183        }
184
185        if (archiveRemover != null) {
186            Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
187            this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
188        }
189    }
190
191    Future<?> renameRawAndAsyncCompress(String nameOfCompressedFile, String innerEntryName) throws RolloverFailure {
192        String parentsRawFile = getParentsRawFileProperty();
193        String tmpTarget = nameOfCompressedFile + System.nanoTime() + ".tmp";
194        renameUtil.rename(parentsRawFile, tmpTarget);
195        return compressor.asyncCompress(tmpTarget, nameOfCompressedFile, innerEntryName);
196    }
197
198    /**
199     * 
200     * The active log file is determined by the value of the parent's filename
201     * option. However, in case the file name is left blank, then, the active log
202     * file equals the file name for the current period as computed by the
203     * <b>FileNamePattern</b> option.
204     * 
205     * <p>
206     * The RollingPolicy must know whether it is responsible for changing the name
207     * of the active file or not. If the active file name is set by the user via the
208     * configuration file, then the RollingPolicy must let it like it is. If the
209     * user does not specify an active file name, then the RollingPolicy generates
210     * one.
211     * 
212     * <p>
213     * To be sure that the file name used by the parent class has been generated by
214     * the RollingPolicy and not specified by the user, we keep track of the last
215     * generated name object and compare its reference to the parent file name. If
216     * they match, then the RollingPolicy knows it's responsible for the change of
217     * the file name.
218     * 
219     */
220    public String getActiveFileName() {
221        String parentsRawFileProperty = getParentsRawFileProperty();
222        if (parentsRawFileProperty != null) {
223            return parentsRawFileProperty;
224        } else {
225            return timeBasedFileNamingAndTriggeringPolicy.getCurrentPeriodsFileNameWithoutCompressionSuffix();
226        }
227    }
228
229    public boolean isTriggeringEvent(File activeFile, final E event) {
230        return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
231    }
232
233    /**
234     * Get the number of archive files to keep.
235     * 
236     * @return number of archive files to keep
237     */
238    public int getMaxHistory() {
239        return maxHistory;
240    }
241
242    /**
243     * Set the maximum number of archive files to keep.
244     * 
245     * @param maxHistory number of archive files to keep
246     */
247    public void setMaxHistory(int maxHistory) {
248        this.maxHistory = maxHistory;
249    }
250
251    public boolean isCleanHistoryOnStart() {
252        return cleanHistoryOnStart;
253    }
254
255    /**
256     * Should archive removal be attempted on application start up? Default is
257     * false.
258     * 
259     * @since 1.0.1
260     * @param cleanHistoryOnStart
261     */
262    public void setCleanHistoryOnStart(boolean cleanHistoryOnStart) {
263        this.cleanHistoryOnStart = cleanHistoryOnStart;
264    }
265
266    @Override
267    public String toString() {
268        return "c.q.l.core.rolling.TimeBasedRollingPolicy@" + this.hashCode();
269    }
270
271    public void setTotalSizeCap(FileSize totalSizeCap) {
272        addInfo("setting totalSizeCap to " + totalSizeCap.toString());
273        this.totalSizeCap = totalSizeCap;
274    }
275}