Logback源碼賞析-日誌按時間滾動(切割)

引言

用過Logback的同窗們大多都知道Logback日誌框架能夠自動按照某個時間點切割日誌的功能。但瞭解其中工做原理的同窗可能並非不少。樓主今天就帶領各位瞭解一下其中的核心源碼。本文的示例引用了Logback 1.1.7版的源碼。java

舉個實際的例子,若是但願某一個Appender按天切割日誌,那麼咱們須要相似以下的配置:windows

<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/service-log.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %level [%class:%line] - %m%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>logs/service-log.%d{yyyy-MM-dd}.log.zip</fileNamePattern>
        </rollingPolicy>
    </appender>

若是須要日誌切割功能,首先要選用RollingFileAppender這種Appender,以後要配置TimeBasedRollingPolicy做爲該Appender的滾動策略。app

源碼解讀

業務代碼在調用Logback的記錄日誌的方法時,Logger類會調用ch.qos.logback.core.Appender#doAppend方法。Appender的doAppend就是Appender記錄日誌功能的入口。
咱們先來看一下RollingFileAppender的繼承關係框架

clipboard.png

看起來貌似有些暈。不要緊,doAppend方法通過幾層以後會調到ch.qos.logback.core.rolling.RollingFileAppender#subAppend 這個方法。而跟日誌切割相關的邏輯就在這裏面。所以從doAppend到subAppend之間的調用鏈路咱們在此略過不提,而是從subAppend這個方法切入。異步

/**
    * This method differentiates RollingFileAppender from its super class.
    */
    @Override
    protected void subAppend(E event) {
        // The roll-over check must precede actual writing. This is the
        // only correct behavior for time driven triggers.

        // We need to synchronize on triggeringPolicy so that only one rollover
        // occurs at a time
        synchronized (triggeringPolicy) {
            if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
                rollover();
            }
        }

        super.subAppend(event);
    }

第一步判斷是否須要切割日誌,須要就執行滾動操做。第二步執行父類中的寫日誌操做。
TimeBasedRollingPolicy中實現了按照時間切割日誌的策略。async

clipboard.png

ch.qos.logback.core.rolling.TimeBasedRollingPolicy#isTriggeringEvent方法ide

public boolean isTriggeringEvent(File activeFile, final E event) {
        return timeBasedFileNamingAndTriggeringPolicy.isTriggeringEvent(activeFile, event);
    }

這個timeBasedFileNamingAndTriggeringPolicy是DefaultTimeBasedFileNamingAndTriggeringPolicy類的實例。
ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy#isTriggeringEvent方法工具

public boolean isTriggeringEvent(File activeFile, final E event) {
        long time = getCurrentTime();//得到當前時間
        if (time >= nextCheck) {//若是當前時間大於下一次滾動時間點,則執行以下邏輯
            Date dateOfElapsedPeriod = dateInCurrentPeriod;//以前時間段的開始時間
            addInfo("Elapsed period: " + dateOfElapsedPeriod);
            elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convert(dateOfElapsedPeriod);//算出以前一個時間段的文件名字(用於將當前文件重命名)
            setDateInCurrentPeriod(time);//將當前時間保存起來,做爲下次滾動時的上一次滾動的時間點
            computeNextCheck();//計算下次滾動時間
            return true;
        } else {
            return false;
        }
    }

ch.qos.logback.core.rolling.helper.FileNamePattern類的convert方法根據上次滾動執行的開始時間計算出了本次操做的文件名。this

計算下次滾動執行的時間。spa

protected void computeNextCheck() {
        nextCheck = rc.getNextTriggeringDate(dateInCurrentPeriod).getTime();
    }

上述方法調用了ch.qos.logback.core.rolling.helper.RollingCalendar#getNextTriggeringDate
RollingCalendar這個工具類是用來計算每一次觸發滾動操做的時間的。RollingCalendar的periodicityType成員變量就是滾動時間類型枚舉,能夠是天、小時、分鐘等等時間單位。這個值並非在logback.xml文件中配置的,而是拿到日期格式「%d{yyyy-MM-dd}」以後自動計算出來的。計算原理就是用日期格式建立一個SimpleDateFormat而後按照時間單位從小到大(必須是從小到大)的順序依次試驗。每次試驗的方法就是用SimpleDateFormat生成兩個時間,這兩個時間相差1倍的該時間單位的時間,而後將兩個時間轉換成字符串進行對比,若是這兩個字符串不相等就說明該日期格式對應的時間單位是當前試驗的時間單位。

看完判斷是否觸發滾動的邏輯以後咱們把視線轉回ch.qos.logback.core.rolling.RollingFileAppender#subAppend方法。若是到了應該切割日誌的時間則會調用rollover方法。

/**
     * Implemented by delegating most of the rollover work to a rolling policy.
     */
    public void rollover() {
        lock.lock();
        try {
            // Note: This method needs to be synchronized because it needs exclusive
            // access while it closes and then re-opens the target file.
            //
            // make sure to close the hereto active log file! Renaming under windows
            // does not work for open files.
            this.closeOutputStream();
            attemptRollover();
            attemptOpenFile();
        } finally {
            lock.unlock();
        }
    }

滾動方法裏面本身控制了線程同步邏輯,保證多個線程只有一個會執行滾動操做。attemptRollover方法調用了ch.qos.logback.core.rolling.TimeBasedRollingPolicy#rollover方法

public void rollover() throws RolloverFailure {

        // when rollover is called the elapsed period's file has
        // been already closed. This is a working assumption of this method.

        String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();//得到上個週期的文件名字

        String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);

        if (compressionMode == CompressionMode.NONE) {
            if (getParentsRawFileProperty() != null) {
                renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);//將當前文件重命名
            } // else { nothing to do if CompressionMode == NONE and parentsRawFileProperty == null }
        } else {
            if (getParentsRawFileProperty() == null) {
                compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);//執行異步壓縮,裏面用到了java語言的Future
            } else {
                compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
            }
        }

        if (archiveRemover != null) {
            Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
            cleanUpFuture = archiveRemover.cleanAsynchronously(now);//執行清理,清楚過時日誌
        }
    }

其中有一些工具類能夠供咱們在業務開發過程當中使用,好比ch.qos.logback.core.rolling.helper.RenameUtil#rename能夠將文件重命名,拿來用就能夠不用重複造輪子了。

private void attemptOpenFile() {
        try {
            // update the currentlyActiveFile LOGBACK-64
            currentlyActiveFile = new File(rollingPolicy.getActiveFileName());//建立一個當前的文件,新的日誌輸出到這個文件。所以日誌的滾動操做就完成了

            // This will also close the file. This is OK since multiple close operations are safe.
            this.openFile(rollingPolicy.getActiveFileName());
        } catch (IOException e) {
            addError("setFile(" + fileName + ", false) call failed.", e);
        }
    }

結束語

因爲篇幅關係,一些細節沒有展開。歡迎你們拍磚。

相關文章
相關標籤/搜索