Log4j 2 rollover 过程源码分析

前言

近日工作中发现应用容器的磁盘空间打满,执行完日志清理后磁盘空间依然无法释放。怀疑有文件只是释放了链接,但是没有实际删除。排查后发现,应用容器上有个日志订阅工具,导致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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 本文主要内容: 1. 概述 我们为了分析程序的执行情况,需要把我们关心的一些调试信息、错误信息等保存到日志中,Lo...
    morninz阅读 5,740评论 0 4
  • WinRAR - 最新版本的更新 版本 5.50 1. WinRAR 和命令行 RAR 默认使用 RAR ...
    王舒璇阅读 2,389评论 0 2
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,304评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 1.常见的日志框架以及选择日志门面:JCL(springboot不用这个)、SLF4J、jboss-logging...
    noobBird阅读 302评论 0 0