log4j2 自動刪除過期日誌檔案配置及實現原理解析

等你歸去來發表於2020-07-31

  日誌檔案自動刪除功能必不可少,當然你可以讓運維去做這事,只是這不地道。而日誌元件是一個必備元件,讓其多做一件刪除的工作,無可厚非。本文就來探討下 log4j 的日誌檔案自動刪除實現吧。

0. 自動刪除配置參考樣例: (log4j2.xml)

<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="warn" monitorInterval="30" strict="true"
               schema="Log4J-V2.2.xsd">
    <Properties>
        <Property name="log_level">info</Property>
    </Properties>
    <Appenders>
        <!-- 輸出到控制檯 -->
        <Console name="Console" target="SYSTEM_OUT">
            <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY" />
            <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%t] %p - %c - %m%n" />
        </Console>
        <!-- 與properties檔案中位置存在衝突,如有問題,請注意調整 -->
        <RollingFile name="logFile" fileName="logs/app/test.log"
                     filePattern="logs/app/history/test-%d{MM-dd-yyyy}-%i.log.gz">
            <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY"  />
            <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%p] [%c:%L] -- %m%n" />
            <Policies>
                <!-- 按天遞計算頻率 -->
                <TimeBasedTriggeringPolicy interval="1" />
                <SizeBasedTriggeringPolicy size="500 MB" />
                <OnStartupTriggeringPolicy />
            </Policies>
            <!-- 刪除策略配置 -->
            <DefaultRolloverStrategy max="5">
                <Delete basePath="logs/app/history" maxDepth="1">
                    <IfFileName glob="*.log.gz"/>
                    <IfLastModified age="7d"/>
                </Delete>
                <Delete basePath="logs/app/history" maxDepth="1">
                    <IfFileName glob="*.docx"/>
                </Delete>
                <Delete basePath="logs/app/history" maxDepth="1">
                    <IfFileName glob="*.vsdx"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
        <Async name="Async" bufferSize="2000" blocking="false">
            <AppenderRef ref="logFile"/>
        </Async>
    </Appenders>

    <Loggers>
        <Root level="${log_level}">
            <AppenderRef ref="Console" />
            <AppenderRef ref="Async" />
        </Root>
        <!-- 配置個例 -->
        <Logger name="com.xx.filter" level="info" />
    </Loggers>
</Configuration>

  如果僅想停留在使用層面,如上log4j2.xml配置檔案足矣!

  不過,至少得注意一點,以上配置需要基於log4j2, 而如果你是 log4j1.x,則需要做下無縫升級:主要就是換下jar包版本,換個橋接包之類的,比如下參考配置:

    
            <dependency>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
                <version>1.2</version>
            </dependency>
            <!-- 橋接:告訴commons logging使用Log4j2 -->
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>jcl-over-slf4j</artifactId>
                <version>1.7.26</version>
            </dependency>

            <!-- 此處老版本,需註釋掉 -->
            <!--<dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>-->

            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-compress</artifactId>
                <version>1.10</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.8.2</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j-impl</artifactId>
                <version>2.8.2</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.8.2</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-web</artifactId>
                <version>2.8.2</version>
            </dependency>

  如果還想多瞭解一點其執行原理,就跟隨本文的腳步吧:

 

1. 自動清理大體執行流程

  自動刪除工作的執行原理大體流程如下。(大抵都是如此)

    1. 載入log4j2.xml配置檔案;
    2. 讀取appenders,並新增到log4j上下文中;
    3. 載入 policy, 載入 rollover 配置;
    4. 寫入日誌時判斷是否滿足rollover配置, 預設是一天執行一次, 可自行新增各種執行測試, 比如大小、啟動時;

  所以,刪除策略的核心是每一次新增日誌時。程式碼驗證如下:

    // 在每次新增日誌時判定
    // org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender#append
    /**
     * Write the log entry rolling over the file when required.
     *
     * @param event The LogEvent.
     */
    @Override
    public void append(final LogEvent event) {
        final RollingRandomAccessFileManager manager = getManager();
        // 重點:直接檢查是否需要 rollover, 如需要直接進行
        manager.checkRollover(event);

        // Leverage the nice batching behaviour of async Loggers/Appenders:
        // we can signal the file manager that it needs to flush the buffer
        // to disk at the end of a batch.
        // From a user's point of view, this means that all log events are
        // _always_ available in the log file, without incurring the overhead
        // of immediateFlush=true.
        manager.setEndOfBatch(event.isEndOfBatch()); // FIXME manager's EndOfBatch threadlocal can be deleted

        // LOG4J2-1292 utilize gc-free Layout.encode() method: taken care of in superclass
        super.append(event);
    }

    // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#checkRollover
    /**
     * Determines if a rollover should occur.
     * @param event The LogEvent.
     */
    public synchronized void checkRollover(final LogEvent event) {
        // 由各觸發策略判定是否需要進行 rolling
        // 如需要, 則呼叫 rollover()
        if (triggeringPolicy.isTriggeringEvent(event)) {
            rollover();
        }
    }

  所以,何時進行刪除?答案是在適當的時機,這個時機可以是任意時候。

 

2. log4j 日誌滾動

  日誌滾動,可以是重新命名,也可以是刪除檔案。但總體判斷是否可觸發滾動的前提是一致的。我們這裡主要關注檔案刪除。我們以時間作為依據看下判斷過程。

    // 1. 判斷是否是 觸發事件時機
    // org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy#isTriggeringEvent
    /**
     * Determines whether a rollover should occur.
     * @param event   A reference to the currently event.
     * @return true if a rollover should occur.
     */
    @Override
    public boolean isTriggeringEvent(final LogEvent event) {
        if (manager.getFileSize() == 0) {
            return false;
        }
        final long nowMillis = event.getTimeMillis();
        // TimeBasedTriggeringPolicy, 是基於時間判斷的, 此處為每天一次
        if (nowMillis >= nextRolloverMillis) {
            nextRolloverMillis = manager.getPatternProcessor().getNextTime(nowMillis, interval, modulate);
            return true;
        }
        return false;
    }
    // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover()
    public synchronized void rollover() {
        if (!hasOutputStream()) {
            return;
        }
        // strategy 是xml配置的策略
        if (rollover(rolloverStrategy)) {
            try {
                size = 0;
                initialTime = System.currentTimeMillis();
                createFileAfterRollover();
            } catch (final IOException e) {
                logError("Failed to create file after rollover", e);
            }
        }
    }
    // RollingFileManager 統一管理觸發器
    // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover
    private boolean rollover(final RolloverStrategy strategy) {

        boolean releaseRequired = false;
        try {
            // Block until the asynchronous operation is completed.
            // 上鎖保證執行緒安全
            semaphore.acquire();
            releaseRequired = true;
        } catch (final InterruptedException e) {
            logError("Thread interrupted while attempting to check rollover", e);
            return false;
        }

        boolean success = true;

        try {
            // 由各觸發器執行 rollover 邏輯
            final RolloverDescription descriptor = strategy.rollover(this);
            if (descriptor != null) {
                writeFooter();
                closeOutputStream();
                if (descriptor.getSynchronous() != null) {
                    LOGGER.debug("RollingFileManager executing synchronous {}", descriptor.getSynchronous());
                    try {
                        // 先使用同步方法,改名,然後再使用非同步方法操作更多
                        success = descriptor.getSynchronous().execute();
                    } catch (final Exception ex) {
                        success = false;
                        logError("Caught error in synchronous task", ex);
                    }
                }
                // 如果配置了非同步器, 則使用非同步進行 rollover
                if (success && descriptor.getAsynchronous() != null) {
                    LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous());
                    // CompositeAction, 使用非同步執行緒池執行使用者的 action
                    asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this));
                    // 在非同步執行action期間,鎖是不會被釋放的,以避免執行緒安全問題
                    // 直到非同步任務完成,再主動釋放鎖
                    releaseRequired = false;
                }
                return true;
            }
            return false;
        } finally {
            if (releaseRequired) {
                semaphore.release();
            }
        }

    }

  此處滾動有兩個處理點,1. 每個滾動策略可以自行處理業務; 2. RollingFileManager 統一管理觸發同步和非同步的滾動action;

3. DefaultRolloverStrategy 預設滾動策略驅動

  DefaultRolloverStrategy 作為一個預設的滾動策略實現,可以配置多個 Action, 然後處理刪除操作。

  刪除有兩種方式: 1. 當次滾動的檔案數過多,會立即進行刪除; 2. 配置單獨的 DeleteAction, 根據配置的具體策略進行刪除。(但該Action只會被返回給外部呼叫,自身則不會執行)

    // org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#rollover
    /**
     * Performs the rollover.
     *
     * @param manager The RollingFileManager name for current active log file.
     * @return A RolloverDescription.
     * @throws SecurityException if an error occurs.
     */
    @Override
    public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException {
        int fileIndex;
        // 預設 minIndex=1
        if (minIndex == Integer.MIN_VALUE) {
            final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
            fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1;
        } else {
            if (maxIndex < 0) {
                return null;
            }
            final long startNanos = System.nanoTime();
            // 刪除case1: 獲取符合條件的檔案數,同時清理掉大於  max 配置的日誌檔案
            // 如配置 max=5, 當前只有4個滿足時, 不會立即清理檔案, 但也不會阻塞後續流程
            // 只要沒有出現錯誤, fileIndex 不會小於0
            fileIndex = purge(minIndex, maxIndex, manager);
            if (fileIndex < 0) {
                return null;
            }
            if (LOGGER.isTraceEnabled()) {
                final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
                LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds", durationMillis);
            }
        }
        // 進入此區域即意味著,必然有檔案需要滾動,重新命名了
        final StringBuilder buf = new StringBuilder(255);
        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, fileIndex);
        final String currentFileName = manager.getFileName();

        String renameTo = buf.toString();
        final String compressedName = renameTo;
        Action compressAction = null;

        FileExtension fileExtension = manager.getFileExtension();
        if (fileExtension != null) {
            renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
            compressAction = fileExtension.createCompressAction(renameTo, compressedName,
                    true, compressionLevel);
        }
        // 未發生檔案重新命名情況,即檔案未被重新命名未被滾動
        // 該種情況應該不太會發生
        if (currentFileName.equals(renameTo)) {
            LOGGER.warn("Attempt to rename file {} to itself will be ignored", currentFileName);
            return new RolloverDescriptionImpl(currentFileName, false, null, null);
        }
        // 新建一個重命令的 action, 返回待用
        final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
                    manager.isRenameEmptyFiles());
        // 非同步處理器,會處理使用者配置的非同步action,如本文配置的 DeleteAction
        // 它將會在稍後被提交到非同步執行緒池中執行
        final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
        // 封裝Rollover返回, renameAction 是同步方法, 其他使用者配置的動態action 則是非同步方法
        // 刪除case2: 封裝非同步返回action
        return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
    }
    private int purge(final int lowIndex, final int highIndex, final RollingFileManager manager) {
        // 預設使用 accending 的方式進行清理檔案
        return useMax ? purgeAscending(lowIndex, highIndex, manager) : purgeDescending(lowIndex, highIndex, manager);
    }
    // org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#purgeAscending
    /**
     * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index, the
     * newest the highest.
     *
     * @param lowIndex low index. Log file associated with low index will be deleted if needed.
     * @param highIndex high index.
     * @param manager The RollingFileManager
     * @return true if purge was successful and rollover should be attempted.
     */
    private int purgeAscending(final int lowIndex, final int highIndex, final RollingFileManager manager) {
        final SortedMap<Integer, Path> eligibleFiles = getEligibleFiles(manager);
        final int maxFiles = highIndex - lowIndex + 1;

        boolean renameFiles = false;
        // 依次迭代 eligibleFiles, 刪除
        while (eligibleFiles.size() >= maxFiles) {
            try {
                LOGGER.debug("Eligible files: {}", eligibleFiles);
                Integer key = eligibleFiles.firstKey();
                LOGGER.debug("Deleting {}", eligibleFiles.get(key).toFile().getAbsolutePath());
                // 呼叫nio的介面刪除檔案
                Files.delete(eligibleFiles.get(key));
                eligibleFiles.remove(key);
                renameFiles = true;
            } catch (IOException ioe) {
                LOGGER.error("Unable to delete {}, {}", eligibleFiles.firstKey(), ioe.getMessage(), ioe);
                break;
            }
        }
        final StringBuilder buf = new StringBuilder();
        if (renameFiles) {
            // 針對未完成刪除的檔案,繼續處理
            // 比如使用 匹配的方式匹配檔案, 則不能被正常刪除
            // 還有些未超過maxFiles的檔案
            for (Map.Entry<Integer, Path> entry : eligibleFiles.entrySet()) {
                buf.setLength(0);
                // LOG4J2-531: directory scan & rollover must use same format
                manager.getPatternProcessor().formatFileName(strSubstitutor, buf, entry.getKey() - 1);
                String currentName = entry.getValue().toFile().getName();
                String renameTo = buf.toString();
                int suffixLength = suffixLength(renameTo);
                if (suffixLength > 0 && suffixLength(currentName) == 0) {
                   renameTo = renameTo.substring(0, renameTo.length() - suffixLength);
                }
                Action action = new FileRenameAction(entry.getValue().toFile(), new File(renameTo), true);
                try {
                    LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}", action);
                    if (!action.execute()) {
                        return -1;
                    }
                } catch (final Exception ex) {
                    LOGGER.warn("Exception during purge in RollingFileAppender", ex);
                    return -1;
                }
            }
        }
        // 此處返回的 findIndex 一定是 >=0 的
        return eligibleFiles.size() > 0 ?
                (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex;
    }

 

4. 符合過濾條件的檔案查詢

  當配置了 max 引數,這個引數是如何匹配的呢?比如我某個資料夾下有很歷史檔案,是否都會匹配呢?

    // 檔案查詢規則 
    // org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy#getEligibleFiles
    protected SortedMap<Integer, Path> getEligibleFiles(final RollingFileManager manager) {
        return getEligibleFiles(manager, true);
    }
    protected SortedMap<Integer, Path> getEligibleFiles(final RollingFileManager manager,
                                                        final boolean isAscending) {
        final StringBuilder buf = new StringBuilder();
        // 此處的pattern 即是在appender上配置的 filePattern, 一般會受限於 MM-dd-yyyy-$i.log.gz
        String pattern = manager.getPatternProcessor().getPattern();
        // 此處會將時間替換為當前, 然後按照此規則進行匹配要處理的檔案
        manager.getPatternProcessor().formatFileName(strSubstitutor, buf, NotANumber.NAN);
        return getEligibleFiles(buf.toString(), pattern, isAscending);
    }
    // 細節匹配要處理的檔案
    protected SortedMap<Integer, Path> getEligibleFiles(String path, String logfilePattern, boolean isAscending) {
        TreeMap<Integer, Path> eligibleFiles = new TreeMap<>();
        File file = new File(path);
        File parent = file.getParentFile();
        if (parent == null) {
            parent = new File(".");
        } else {
            parent.mkdirs();
        }
        if (!logfilePattern.contains("%i")) {
            return eligibleFiles;
        }
        Path dir = parent.toPath();
        String fileName = file.getName();
        int suffixLength = suffixLength(fileName);
        if (suffixLength > 0) {
            fileName = fileName.substring(0, fileName.length() - suffixLength) + ".*";
        }
        String filePattern = fileName.replace(NotANumber.VALUE, "(\\d+)");
        Pattern pattern = Pattern.compile(filePattern);

        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            for (Path entry: stream) {
                // 該匹配相當精確
                // 只會刪除當天或者在時間交替的時候刪除上一天的資料咯
                // 如果在這個時候進行了重啟操作,就再也不會刪除此檔案了
                Matcher matcher = pattern.matcher(entry.toFile().getName());
                if (matcher.matches()) {
                    Integer index = Integer.parseInt(matcher.group(1));
                    eligibleFiles.put(index, entry);
                }
            }
        } catch (IOException ioe) {
            throw new LoggingException("Error reading folder " + dir + " " + ioe.getMessage(), ioe);
        }
        return isAscending? eligibleFiles : eligibleFiles.descendingMap();
    }
    // 此處會將 各種格式的檔名,替換為當前時間或者最後一次滾動的檔案的時間。所以匹配的時候,並不會匹配超時當前認知範圍的檔案
    /**
     * Formats file name.
     * @param subst The StrSubstitutor.
     * @param buf string buffer to which formatted file name is appended, may not be null.
     * @param obj object to be evaluated in formatting, may not be null.
     */
    public final void formatFileName(final StrSubstitutor subst, final StringBuilder buf, final boolean useCurrentTime,
                                     final Object obj) {
        // LOG4J2-628: we deliberately use System time, not the log4j.Clock time
        // for creating the file name of rolled-over files.
        final long time = useCurrentTime && currentFileTime != 0 ? currentFileTime :
                prevFileTime != 0 ? prevFileTime : System.currentTimeMillis();
        formatFileName(buf, new Date(time), obj);
        final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build();
        final String fileName = subst.replace(event, buf);
        buf.setLength(0);
        buf.append(fileName);
    }
    

  AsyncAction 是一個 Runnable 的實現, 被直接提交到執行緒池執行. AsyncAction -> AbstractAction -> Action -> Runnable

  它是一個統一管理非同步Action的包裝,主要是管理鎖和異常類操作。

    // org.apache.logging.log4j.core.appender.rolling.RollingFileManager.AsyncAction
    /**
     * Performs actions asynchronously.
     */
    private static class AsyncAction extends AbstractAction {

        private final Action action;
        private final RollingFileManager manager;

        /**
         * Constructor.
         * @param act The action to perform.
         * @param manager The manager.
         */
        public AsyncAction(final Action act, final RollingFileManager manager) {
            this.action = act;
            this.manager = manager;
        }

        /**
         * Executes an action.
         *
         * @return true if action was successful.  A return value of false will cause
         *         the rollover to be aborted if possible.
         * @throws java.io.IOException if IO error, a thrown exception will cause the rollover
         *                             to be aborted if possible.
         */
        @Override
        public boolean execute() throws IOException {
            try {
                // 門面呼叫 action.execute(), 一般是呼叫  CompositeAction, 裡面封裝了多個 action
                return action.execute();
            } finally {
                // 任務執行完成,才會釋放外部的鎖
                // 雖然不是很優雅,但是很準確很安全
                manager.semaphore.release();
            }
        }
        ...
    }

    // CompositeAction 封裝了多個 action 處理
    // org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run
    /**
     * Execute sequence of actions.
     *
     * @return true if all actions were successful.
     * @throws IOException on IO error.
     */
    @Override
    public boolean execute() throws IOException {
        if (stopOnError) {
            // 依次呼叫action
            for (final Action action : actions) {
                if (!action.execute()) {
                    return false;
                }
            }

            return true;
        }
        boolean status = true;
        IOException exception = null;

        for (final Action action : actions) {
            try {
                status &= action.execute();
            } catch (final IOException ex) {
                status = false;

                if (exception == null) {
                    exception = ex;
                }
            }
        }

        if (exception != null) {
            throw exception;
        }

        return status;
    }

  DeleteAction 是我們真正關心的動作。

    // CompositeAction 封裝了多個 action 處理
    // org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run
    /**
     * Execute sequence of actions.
     *
     * @return true if all actions were successful.
     * @throws IOException on IO error.
     */
    @Override
    public boolean execute() throws IOException {
        if (stopOnError) {
            // 依次呼叫action
            for (final Action action : actions) {
                if (!action.execute()) {
                    return false;
                }
            }

            return true;
        }
        boolean status = true;
        IOException exception = null;

        for (final Action action : actions) {
            try {
                status &= action.execute();
            } catch (final IOException ex) {
                status = false;

                if (exception == null) {
                    exception = ex;
                }
            }
        }

        if (exception != null) {
            throw exception;
        }

        return status;
    }
    
    // DeleteAction 做真正的刪除動作
    // org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute()
    @Override
    public boolean execute() throws IOException {
        // 如果沒有script配置,則直接委託父類處理
        return scriptCondition != null ? executeScript() : super.execute();
    }
    org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute()
    @Override
    public boolean execute() throws IOException {
        // 根據指定的basePath, 和過濾條件,選擇相關檔案
        // 呼叫 DeleteAction 的 createFileVisitor(), 返回 DeletingVisitor
        return execute(createFileVisitor(getBasePath(), pathConditions));
    }
    // org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute(java.nio.file.FileVisitor<java.nio.file.Path>)
    @Override
    public boolean execute(final FileVisitor<Path> visitor) throws IOException {
        // 根據maxDepth設定,遍歷所有可能的檔案路徑
        // 使用 Files.walkFileTree() 實現, 新增到 collected 中
        final List<PathWithAttributes> sortedPaths = getSortedPaths();
        trace("Sorted paths:", sortedPaths);

        for (final PathWithAttributes element : sortedPaths) {
            try {
                // 依次呼叫 visitFile, 依次判斷是否需要刪除
                visitor.visitFile(element.getPath(), element.getAttributes());
            } catch (final IOException ioex) {
                LOGGER.error("Error in post-rollover Delete when visiting {}", element.getPath(), ioex);
                visitor.visitFileFailed(element.getPath(), ioex);
            }
        }
        // TODO return (visitor.success || ignoreProcessingFailure)
        return true; // do not abort rollover even if processing failed
    }

  最終,即和想像的一樣:找到要查詢的資料夾,遍歷各檔案,用多個條件判斷是否滿足。刪除符合條件的檔案。

  只是這其中注意的點:如何刪除檔案的執行緒安全性;如何保證刪除工作不影響業務執行緒;很常見的鎖和多執行緒的應用。

  

5. 真正的刪除

  真正的刪除動作就是在 DeleteAction中配置的,但上面可以看它是呼叫 visitor 的visitFile 方法,所以有必要看看是如何真正處理刪除的。(實際上前面在 purge時已經做過一次刪除操作了,所以別被兩個點迷惑了,建議儘量只依賴於 Delete 配置,可以將外部 max 設定很大以避免兩處生效)

    // org.apache.logging.log4j.core.appender.rolling.action.DeletingVisitor#visitFile
    @Override
    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
        for (final PathCondition pathFilter : pathConditions) {
            final Path relative = basePath.relativize(file);
            // 遍歷所有條件,只要有一個不符合,即不進行刪除。
            // 所以,所以條件是 AND 關係, 沒有 OR 關係
            // 如果想配置 OR 關係,只能配置多個DELETE
            if (!pathFilter.accept(basePath, relative, attrs)) {
                LOGGER.trace("Not deleting base={}, relative={}", basePath, relative);
                return FileVisitResult.CONTINUE;
            }
        }
        // 直接刪除檔案
        if (isTestMode()) {
            LOGGER.info("Deleting {} (TEST MODE: file not actually deleted)", file);
        } else {
            delete(file);
        }
        return FileVisitResult.CONTINUE;
    }

 

  刪除策略配置比如:

    <RollingFile name="logFile" fileName="logs/app/test.log"
                 filePattern="logs/app/history/test-%d{MM-dd-yyyy}-%i.log.gz">
        <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY"  />
        <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%p] [%c:%L] -- %m%n" />
        <Policies>
            <!-- 按天遞計算頻率 -->
            <TimeBasedTriggeringPolicy interval="1" />
            <SizeBasedTriggeringPolicy size="500 MB" />
            <OnStartupTriggeringPolicy />
        </Policies>
        <!-- 刪除策略配置 -->
        <DefaultRolloverStrategy max="5000">
            <Delete basePath="logs/app/history" maxDepth="1">
                <!-- 配置且關係 -->
                <IfFileName glob="*.log.gz"/>
                <IfLastModified age="7d"/>
            </Delete>
            <!-- 配置或關係 -->
            <Delete basePath="logs/app/history" maxDepth="1">
                <IfFileName glob="*.docx"/>
            </Delete>
            <Delete basePath="logs/app/history" maxDepth="1">
                <IfFileName glob="*.vsdx"/>
            </Delete>
        </DefaultRolloverStrategy>
    </RollingFile>

  另外說明,日誌實現之所以能夠無縫升級或替換,是因為利用了不同實現版本的 org/slf4j/impl/StaticLoggerBinder.class, 而外部都使用 slf4j 介面定義實現的,比如 org.apache.logging.log4j:log4j-slf4j-impl 包的實現。這是因為 org.slf4j.LoggerFactory 中留了這麼個坑位,等你來填的好處。

相關文章