美文网首页
Log4j 2 rollover 过程源码分析

Log4j 2 rollover 过程源码分析

作者: kerwinX | 来源:发表于2019-06-01 20:59 被阅读0次

    前言

    近日工作中发现应用容器的磁盘空间打满,执行完日志清理后磁盘空间依然无法释放。怀疑有文件只是释放了链接,但是没有实际删除。排查后发现,应用容器上有个日志订阅工具,导致log4j日志文件没有办法真正执行删除,解除订阅之后,空间得到释放。
    通过出现的现象可以猜测,在写日志达到触发归档条件时会对日志文件执行了删除操作。由此对log4j2日志归档的源码产生了兴趣,所以阅读源码,写下此笔记。

    RollingFileAppender

    RollingFileAppender 负责写入日志到指定文件,并根据TriggeringPolicyRolloverPolicy滚动文件。所以我们从它的源码入手。关键代码如下:

    //Writes the log entry rolling over the file when required.
     @Override
        public void append(final LogEvent event) {
            //我的注释:检查并触发Rollover
            getManager().checkRollover(event);
            super.append(event);
        }
    

    可以看出,在append()执行第一步就是检查是否需要rollover。这个行为是通过RollingFileManager执行的。下面让我们看下它的代码。

    RollingFileManager

    /**
         * Determines if a rollover should occur.
         * @param event The LogEvent.
         */
        public synchronized void checkRollover(final LogEvent event) {
            //我的注释:符合条件则执行rollover
            if (triggeringPolicy.isTriggeringEvent(event)) {
                rollover();
            }
        }
    

    符合条件,则执行rollover。至于如何判断的细节我们暂不关注,目前只需要了解这取决于我们配置的TriggeringPolicies。其中常见的就是:TimeBasedTriggeringPolicySizeBasedTriggeringPolicy

    下面让我进入 rollover()

    public synchronized void rollover() {
            if (!hasOutputStream()) {
                return;
            }
            if (rollover(rolloverStrategy)) {
                try {
                    size = 0;
                    initialTime = System.currentTimeMillis();
                    createFileAfterRollover();
                } catch (final IOException e) {
                    logError("Failed to create file after rollover", e);
                }
            }
        }
    
    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 {
                //我的注释:
                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);
                        }
                    }
    
                    if (success && descriptor.getAsynchronous() != null) {
                        LOGGER.debug("RollingFileManager executing async {}", descriptor.getAsynchronous());
                        asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(), this));
                        releaseRequired = false;
                    }
                    return true;
                }
                return false;
            } finally {
                if (releaseRequired) {
                    semaphore.release();
                }
            }
        }
    

    rollover()总结一下:

    • 获取锁
    • 获取RolloverDescriptiondescriptor中包含了一系列rollover需要执行的行为。它是通过strategy.rollover创建的。这里的strategy我们以DefaultRolloverStrategy的实现为例往下分析。
    • 写页脚,writeFooter()
    • 关闭写入流,closeOutputStream()
    • 执行descriptor中的一系列操作。
    • 返回并释放锁。

    所以关键就在于strategy.rollover()定义了那些行为。似乎离真相越来越近了,让我们继续。

    DefaultRolloverStrategy

    注意:这里我们使用DefaultRolloverStrategy 来分析,这也是默认和最常用的。

    直接上代码,矿就都在这块了 :

    /**
         * 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 {
            //我的注释:算fileIndex,为归档文件命名作准备。
            int fileIndex;
            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();
                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;
            //我的注释:获取归档压缩文件扩展名类的实例,并依据不同的压缩文件类型创建压缩行为。
            final FileExtension fileExtension = manager.getFileExtension();
            if (fileExtension != null) {
                final File renameToFile = new File(renameTo);
                renameTo = renameTo.substring(0, renameTo.length() - fileExtension.length());
                if (tempCompressedFilePattern != null) {
                    buf.delete(0, buf.length());
                    tempCompressedFilePattern.formatFileName(strSubstitutor, buf, fileIndex);
                    final String tmpCompressedName = buf.toString();
                    final File tmpCompressedNameFile = new File(tmpCompressedName);
                    final File parentFile = tmpCompressedNameFile.getParentFile();
                    if (parentFile != null) {
                        parentFile.mkdirs();
                    }
                    compressAction = new CompositeAction(
                            Arrays.asList(fileExtension.createCompressAction(renameTo, tmpCompressedName,
                                    true, compressionLevel),
                                    new FileRenameAction(tmpCompressedNameFile,
                                            renameToFile, true)),
                            true);
                } else {
                    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);
            }
    
            if (compressAction != null && manager.isAttributeViewEnabled()) {
                // Propagate posix attribute view to compressed file
                // @formatter:off
                final Action posixAttributeViewAction = PosixViewAttributeAction.newBuilder()
                                                            .withBasePath(compressedName)
                                                            .withFollowLinks(false)
                                                            .withMaxDepth(1)
                                                            .withPathConditions(new PathCondition[0])
                                                            .withSubst(getStrSubstitutor())
                                                            .withFilePermissions(manager.getFilePermissions())
                                                            .withFileOwner(manager.getFileOwner())
                                                            .withFileGroup(manager.getFileGroup())
                                                            .build();
                // @formatter:on
                compressAction = new CompositeAction(Arrays.asList(compressAction, posixAttributeViewAction), false);
            }
            //我的注释:创建文件重命名行为
            final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName), new File(renameTo),
                        manager.isRenameEmptyFiles());
            //我的注释:合并压缩行为和文件重命名行为,创建RolloverDescription并返回
            final Action asyncAction = merge(compressAction, customActions, stopCustomActionsOnError);
            return new RolloverDescriptionImpl(currentFileName, false, renameAction, asyncAction);
        }
    

    总结一下:

    • 计算归档文件的文件名。
    • 根据归档文件扩展类型,创建压缩行为 fileExtension.createCompressAction()FileExtension是一组枚举,包含支持的文件压缩类型及压缩行为。
    • 判断是否需要 AttributeView,并添加posixAttributeViewAction。这里我们不关心,也就不问了。
    • 创建文件重命名行为renameActionFileRenameAction.excute()代码在这就不贴了,就是复制文件到指定位置。
    • 合并所有归档需要的行为,并创建RolloverDescription实例,最后返回。

    总结

    至此,log4j2 的归档实现主要流程,我们大致了解了。文字描述一下就是:

    1. 停止写入原日志文件。
    2. 复制源文件到指定位置。
    3. 执行压缩。
    4. 创建新的日志文件,继续写入。

    参考:
    Log4j – Log4j 2 Appenders
    log4j2 版本:log4j-core-2.9.1

    相关文章

      网友评论

          本文标题:Log4j 2 rollover 过程源码分析

          本文链接:https://www.haomeiwen.com/subject/atzatctx.html