logback服务器日志删除原理分析

查看以下的logback官方文档

Chapter 4: Appendershttps://logback.qos.ch/manual/appenders.html

按文档说明,maxHistory是设置保存归档日志的最大数量,该数量的单位受到fileNamePattern里的值%d控制,如果有多个%d,只能有一个主%d,其他的要用aux参数标记为辅助令牌。

/var/log/%d{yyyy/MM, aux}/myapplication.%d{yyyy-MM-dd}.log

比如上面的fileNamePattern主%d是%d{yyyy-MM-dd},意味着显示的文件名模式按年份和月份组织日志文件夹,但每天午夜滚动日志文件。

也就是说maxHistory是归档日志的最大数量,该数量的单位可以是多种,类型如下

小时、天、周、毫秒、秒、分钟、半天、月。

单位受%d控制。

cleanHistoryOnStart参数用于启动时删除需要删除日志文件,如果不配置默认是false,意味着启动时不删除日志。

在项目中我们发现当触发日志删除条件时,一些历史久远的日志无法删除。那日志的删除逻辑时怎么样的?下面我们先做了逻辑总结,各位有兴趣可以查看下面的源码分析。

logback是无法删除历史很久远的日志的。比如maxHistory设置为30,单位设置为日。那么执行时,根据单位删除的是距离今天31天前的到距离今天62(31+32-1)天前(的日志。

例如:今天是2023年10月16日删除的是31天前(2023年9月15日)到62天前(2023年8月15日)的日志。

这部分逻辑见以下源码分析:

以下是日志配置文件:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
    <!-- logger上下文名称(根据业务修改) -->
    <contextName>MyContextName</contextName>

    <!-- 定义了一个名为serverName的属性,它的值来自于logging.file.name,如果没有找到该属性默认为MyServerName(根据业务修改) -->
    <springProperty name="serverName" source="logging.file.name" defaultValue="MyServerName"/>
    <springProperty name="logging.path" source="logging.file.path" defaultValue="././logs/"/>

    <!-- 彩色日志依赖的渲染类 -->
    <!-- 定义了一个名为clr的转换规则,它使用org.springframework.boot.logging.logback.ColorConverter类进行转换,这个元素通常用于将日志输出中的文本着色,以便更容易地区分不同的日志级别或其他信息 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <!-- WhitespaceThrowableProxyConverter和ExtendedWhitespaceThrowableProxyConverter都是用于将异常信息转换为字符串,并将其中的换行符替换为空格,以便更容易地在日志输出中显示的类。它们之间的区别在于,ExtendedWhitespaceThrowableProxyConverter在输出异常信息时会包含更多的详细信息,例如异常的类名、方法名和行号等 -->
    <!-- 定义了一个名为wex的转换规则,它使用org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter类进行转换,这个元素通常用于将异常信息转换为字符串,并将其中的换行符替换为空格,以便更容易地在日志输出中显示 -->
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <!-- 定义了一个名为wEx的转换规则,它使用org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter类进行转换,这个元素通常用于将异常信息转换为字符串,并将其中的换行符替换为空格,以便更容易地在日志输出中显示 -->
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

    <!-- 彩色日志格式 -->
    <!-- value值是日志输出模板, :-是属性名和其默认值之间的分隔符,作用与:相同 -->
    <!-- 定义日志输出格式的转换规则,%d{yyyy-MM-dd HH:mm:ss.SSS}表示日期和时间,%clr表示将输出文本着色,{faint}表示使用淡色 -->
    <!-- %5p表示日志级别输出右对齐,左边以空格填充 -->
    <!-- ${PID:- }表示进程ID,%clr表示将输出文本着色,{magenta}表示使用洋红色 -->
    <!-- %t:显示产生该日志的线程名;%15:若字符长度小于15,则左边用空格填充;%.15:若字符长度超过15,截去多余字符 -->
    <!-- %-40:若字符长度小于40,则右边用空格填充;%.40:若字符长度超过40,截去多余字符;logger{39}对应的是"logging.WARNING"级别。具体来说,Python的logging模块定义了以下几个级别(从低到高):NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。因此,logger{39}表示的是WARNING级别,即日志记录器会记录所有WARNING级别及以上的日志信息 -->
    <!-- %m表示日志消息;%n表示换行符;${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}表示异常信息。如果日志输出中包含异常信息,这个规则将会将其转换为字符串,并将其中的换行符替换为空格,以便更容易地在日志输出中显示 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--1. 输出到控制台-->
    <!-- 定义控制台日志输出的appender,class="ch.qos.logback.core.ConsoleAppender"表示使用Logback框架提供的ConsoleAppender类来输出日志到控制台 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <!-- 定义日志输出级别的过滤器,class="ch.qos.logback.classic.filter.ThresholdFilter"表示使用Logback框架提供的ThresholdFilter类来过滤日志输出,<level>debug</level>表示只输出debug级别及以上的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <!-- ${CONSOLE_LOG_PATTERN}表示控制台日志输出格式,UTF-8表示编码格式 -->
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!--2. 输出到文档-->
    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
    <!-- 定义文件日志输出的appender,class="ch.qos.logback.core.rolling.RollingFileAppender"表示使用Logback框架提供的RollingFileAppender类来输出日志到文件 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_debug.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <!-- 定义日志文件滚动策略的标签,class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"表示使用Logback框架提供的TimeBasedRollingPolicy类来定义日志文件的滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <!-- 定义日志文件名的模式。在这个模式中,${logging.path}表示日志文件的路径,%d{yyyy-MM-dd}表示日期格式,%i表示文件索引 -->
            <fileNamePattern>${logging.path}/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 定义日志文件滚动策略的标签,class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"表示使用Logback框架提供的SizeAndTimeBasedFNATP类来定义日志文件的滚动策略,<maxFileSize>100MB</maxFileSize>表示日志文件的最大大小为100MB。这个滚动策略通常用于按照时间和文件大小滚动日志文件,以便更好地管理日志文件的大小和数量 -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录debug级别的 -->
        <!-- 定义日志输出级别的过滤器。在这个过滤器中,class="ch.qos.logback.classic.filter.LevelFilter"表示使用Logback框架提供的LevelFilter类来过滤日志输出,<level>debug</level>表示只输出debug级别及以上的日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <!-- <onMatch>ACCEPT</onMatch>表示如果日志事件与过滤器匹配,则接受该事件,<onMismatch>DENY</onMismatch>表示如果日志事件与过滤器不匹配,则拒绝该事件 -->
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_info.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 天天日志归档路径以及格式 -->
            <fileNamePattern>${logging.path}/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_warn.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logging.path}/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${logging.path}/web_error.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logging.path}/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--     4.1 开发环境:打印控制台-->
    <!-- 用于在Spring Boot应用程序中配置日志记录的标签。在这个标签中,name="dev"表示这个配置文件只在dev环境中生效,<logger name="com.myClass.controller" level="debug"/>表示为com.myClass.controller(根据业务修改)这个包下的类配置日志输出级别为debug -->
    <springProfile name="dev">
        <logger name="com.myClass.controller" level="debug"/>
    </springProfile>
    <!-- 用于配置日志输出的标签。在这个标签中,level="info"表示日志输出级别为info,<appender-ref ref="CONSOLE"/>、<appender-ref ref="DEBUG_FILE"/>、<appender-ref ref="INFO_FILE"/>、<appender-ref ref="WARN_FILE"/>、<appender-ref ref="ERROR_FILE"/>表示将日志输出到不同的appender中,分别为控制台、debug文件、info文件、warn文件和error文件 -->
    <root level="info">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="WARN_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
</configuration>

在上面的配置文档中,如果我们打印一个info级别的日志,执行的是以下的策略

在上面的配置文件中发现fileNamePattern中是%d{yyyy-MM-dd},也就是说maxHistory的数量是30,单位是天。

当日切时,会执行TimeBasedRollingPolicy里的rollover方法。

++TimeBasedRollingPolicy.rollover()

++TimeBasedArchiveRemover.cleanAsynchronously(Date now)

++ArhiveRemoverRunnable.run()

++ArhiveRemoverRunnable.clean(Date now)

通过rollover会执行ArhiveRemoverRunnable中的clean方法

复制代码
public void clean(Date now) {
        long nowInMillis = now.getTime();
        //获取要删除的时间段
        int periodsElapsed = this.computeElapsedPeriodsSinceLastClean(nowInMillis);
        this.lastHeartBeat = nowInMillis;
        if (periodsElapsed > 1) {
            this.addInfo("Multiple periods, i.e. " + periodsElapsed + " periods, seem to have elapsed. This is expected at application start.");
        }
        //循环删除日志文件
        for(int i = 0; i < periodsElapsed; ++i) {
            //获取开端,getPeriodOffsetForDeletionTarget返回的值是yaml
            //配置文件配置的maxHistory - 1
            //periodsElapsed值是32,那么offset的值是-30-1到-30-1-31即-31到-62
            int offset = this.getPeriodOffsetForDeletionTarget() - i;
            //dateOfPeriodToClean返回的是当前时间31天到62天前的数据
            Date dateOfPeriodToClean = this.rc.getEndOfNextNthPeriod(now, offset);
            //执行删除动作
            this.cleanPeriod(dateOfPeriodToClean);
        }

    }

分析computeElapsedPeriodsSinceLastClean该方法会计算删除日期范围。

复制代码
int computeElapsedPeriodsSinceLastClean(long nowInMillis) {
        long periodsElapsed = 0L;
        if (this.lastHeartBeat == -1L) {
            this.addInfo("first clean up after appender initialization");
            periodsElapsed = this.rc.periodBarriersCrossed(nowInMillis, nowInMillis + 2764800000L);
            periodsElapsed = Math.min(periodsElapsed, 336L);
        } else {
            periodsElapsed = this.rc.periodBarriersCrossed(this.lastHeartBeat, nowInMillis);
        }

        return (int)periodsElapsed;
    }

在上面的代码中可以看到 periodBarriersCrossed方法计算时间段,该方法有两个入参分别是start和end,上面的代码中可以看到这两个入参传入的值是nowInMillis和nowInMillis + 2764800000L。

在periodBarriersCrossed方法中可以看到diff = endFloored - startFloored;而endFloored和startFloored生成使用的是同一个方法同一套规则,两者的差异只和入参有关。diff计算出的差值还是2764800000L,如果logback-spring.yaml文件里配置的单位是日,进入的是以下代码里

复制代码
case TOP_OF_DAY:
                return diff / 86400000L;

这段逻辑。2764800000L/86400000L的到的值是32。

复制代码
   public long periodBarriersCrossed(long start, long end) {
        if (start > end) {
            throw new IllegalArgumentException("Start cannot come before end");
        } else {
            long startFloored = this.getStartOfCurrentPeriodWithGMTOffsetCorrection(start, this.getTimeZone());
            long endFloored = this.getStartOfCurrentPeriodWithGMTOffsetCorrection(end, this.getTimeZone());
            long diff = endFloored - startFloored;
            switch(this.periodicityType) {
            case TOP_OF_HOUR:
                return (long)((int)diff) / 3600000L;
            case TOP_OF_DAY:
                return diff / 86400000L;
            case TOP_OF_WEEK:
                return diff / 604800000L;
            case TOP_OF_MILLISECOND:
                return diff;
            case TOP_OF_SECOND:
                return diff / 1000L;
            case TOP_OF_MINUTE:
                return diff / 60000L;
            case HALF_DAY:
            default:
                throw new IllegalStateException("Unknown periodicity type.");
            case TOP_OF_MONTH:
                return (long)diffInMonths(start, end);
            }
        }
    }

periodsElapsed = Math.min(periodsElapsed, 336L);取periodsElapsed和336两者之间的最小值。

最终periodBarriersCrossed返回值为32即时间间隔为32天。

继续分析clean方中cleanPeriod方法,源码如下,该方法找到传入日期文件夹中的文件列表的执行删除动作 。

复制代码
    public void cleanPeriod(Date dateOfPeriodToClean) {
        //获取要删除日期文件夹下的文件列表
        File[] matchingFileArray = this.getFilesInPeriod(dateOfPeriodToClean);
        File[] arr$ = matchingFileArray;
        int len$ = matchingFileArray.length;
        //循环删除日期文件夹下的日志文件
        for(int i$ = 0; i$ < len$; ++i$) {
            File f = arr$[i$];
            this.addInfo("deleting " + f);
            //删除文件
            f.delete();
        }

        if (this.parentClean && matchingFileArray.length > 0) {
            File parentDir = this.getParentDir(matchingFileArray[0]);
            this.removeFolderIfEmpty(parentDir);
        }

    }
相关推荐
EumenidesJ20 天前
Java常用日志框架介绍
java·log4j·logback·slf4j
躲在没风的地方1 个月前
logback日志控制服务器日志输出
java·服务器·logback
ta叫我小白1 个月前
Spring Boot 设置滚动日志logback
java·spring boot·spring·logback
代码的余温1 个月前
Spring Boot集成Logback日志全攻略
xml·spring boot·logback
代码的余温1 个月前
Logback.xml配置详解与实战指南
xml·logback
清风92001 个月前
Logback——日志技术(基础)
java·前端·logback
代码的余温1 个月前
MyBatis集成Logback日志全攻略
java·tomcat·mybatis·logback
秋千码途1 个月前
小架构step系列08:logback.xml的配置
xml·java·logback
枣伊吕波1 个月前
第十五节:第六部分:日志技术:logback的核心配置文件详解、日志级别
logback
再见晴天*_*1 个月前
logback 日志不打印
java·服务器·logback