查看以下的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日)的日志。
这部分逻辑见以下源码分析:
以下是日志配置文件:
MyContextName debug ${CONSOLE_LOG_PATTERN} UTF-8 ${logging.path}/web_debug.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.path}/web-debug-%d{yyyy-MM-dd}.%i.log 100MB 30 debug ACCEPT DENY ${logging.path}/web_info.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.path}/web-info-%d{yyyy-MM-dd}.%i.log 100MB 30 info ACCEPT DENY ${logging.path}/web_warn.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.path}/web-warn-%d{yyyy-MM-dd}.%i.log 100MB 30 warn ACCEPT DENY ${logging.path}/web_error.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n UTF-8 ${logging.path}/web-error-%d{yyyy-MM-dd}.%i.log 100MB 30 ERROR ACCEPT DENY
在上面的配置文档中,如果我们打印一个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); } }