1 概述
当业务运行出问题的时候,需要借助日志来定位问题,下面的情况可能会导致日志量比较大:
- 很多开发人员对日志的作用并不是很理解,大概是把日志当单步调试用,想看看程序是否运行到哪一步就打印个日志,确定程序到哪一步远远不够的,还需要其它信息,这样日志就会越打越多。
- 业务软件运行的时间越长,积攒的日志就越多。
日志的存储是要消耗存储空间的,如果不加以控制,量积累到一定程度就有可能会耗光有限的磁盘空间,甚至无法写入造成宕机,所以需要对日志量进行控制。
2 日志配置
2.1 配置
之前的配置文件里有以下的配置:
<property name="log.file.path" value="logs" /> <!-- 日志存放目录 -->
<property name="log.filename.pattern" value="${log.file.path}/app.%d{yyyy-MM-dd}.%i.log" /> <!-- 日志文件名格式 -->
<property name="log.file.maxsize" value="30MB" /> <!-- 每个日志文件最大的大小 -->
<property name="log.file.maxhistory" value="30" /> <!-- 日志文件的最大保留个数,默认为0(无限) -->
<property name="log.file.max.capacity" value="10GB" /> <!-- 所有日志文件总大小的上限,需要配了文件最大保留个数才有效,默认为0(无限) -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.file.path}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.filename.pattern}</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${log.file.maxsize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${log.file.maxhistory}</maxHistory>
<totalSizeCap>${log.file.max.capacity}</totalSizeCap>
</rollingPolicy>
</appender>
- 首先,要指定日志存储的位置,也就是存储在哪个目录下。
- 然后是一个日志文件不能过大,否则需要用的时候,想打开都比较困难,影响日志查看,需要设置一个单文件的最大量限制;既然一个文件不能过大,就必须对日志文件进行拆分,拆分后的文件名用什么格式命名;文件日志过多也不方便,在linux中一个目录下的文件个数也是有限的,需要对文件格式进行限制;
- 最后一个是对日志总量的限制,不管每个文件的大小和文件数量多少,总量不能超过指定的数量。
2.2 原理
2.2.1 根据文件名pattern初始化拆分文件的时间周期
在配置文件中,文件名的pattern配置如 logs/app.%d{yyyy-MM-dd}.%i.log 样式,它是希望日志文件能够按天进行拆分,如果一天有多个,则加后缀“.i”。其中yyyy-MM-dd 是datePattern,这datePattern可以配置成很多种形式,这里只是其中一种:按天的类型,如何确定datePattern也是个问题,也就是要确定日志文件是按天、按分钟还是按月等类型来拆分。
// 在加载loback.xml配置文件时,会初始化Policy类(TimeBasedRollingPolicy),并调start()方法
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
// 1. 把配置中的文件名pattern赋值给fileNamePattern变量
if (fileNamePatternStr != null) {
fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
determineCompressionMode();
}
// 2. 初始化timeBasedFileNamingAndTriggeringPolicy并调用start()方法,例子中配的是SizeAndTimeBasedFNATP
if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
timeBasedFileNamingAndTriggeringPolicy.setContext(context);
timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
timeBasedFileNamingAndTriggeringPolicy.start();
// 省略其它代码
}
// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public void start() {
// 3. 调用父类TimeBasedFileNamingAndTriggeringPolicyBase的start()
super.start();
// 省略其它代码
}
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
public void start() {
DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
if (dtc == null) {
throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
}
// 4. 初始化RollingCalendar,用于计算拆分文件的时间点
// 比如:当文件名pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,dtc.getDatePattern()为yyyy-MM-dd
if (dtc.getTimeZone() != null) {
rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
} else {
rc = new RollingCalendar(dtc.getDatePattern());
}
addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '" + tbrp.fileNamePattern.getPattern() + "'.");
rc.printPeriodicity(this);
if (!rc.isCollisionFree()) {
addError("The date format in FileNamePattern will result in collisions in the names of archived log files.");
addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
withErrors();
return;
}
setDateInCurrentPeriod(new Date(getCurrentTime()));
if (tbrp.getParentsRawFileProperty() != null) {
File currentFile = new File(tbrp.getParentsRawFileProperty());
if (currentFile.exists() && currentFile.canRead()) {
setDateInCurrentPeriod(new Date(currentFile.lastModified()));
}
}
addInfo("Setting initial period to " + dateInCurrentPeriod);
computeNextCheck();
}
// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public RollingCalendar(String datePattern) {
super();
// 记住日期pattern
this.datePattern = datePattern;
// 5. 根据pattern计算文件拆分周期类型,是按天还是按时分秒
this.periodicityType = computePeriodicityType();
}
public PeriodicityType computePeriodicityType() {
GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
Date epoch = new Date(0); // 设置初始值为1970-01-01 00:00:00 GMT
if (datePattern != null) {
// PeriodicityType.VALID_ORDERED_LIST里提供了从毫秒到月这7种粒度的类型
// TOP_OF_MILLISECOND 毫秒
// PeriodicityType.TOP_OF_SECOND 秒
// PeriodicityType.TOP_OF_MINUTE 分钟
// PeriodicityType.TOP_OF_HOUR 小时
// PeriodicityType.TOP_OF_DAY 天
// PeriodicityType.TOP_OF_WEEK 周
// PeriodicityType.TOP_OF_MONTH 月
for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
// 6. epoch是一个1970-01-01 00:00:00的时间(到毫秒),用datePattern去把这个时间格式化成一个字符串
// 由于不确定datePattern配置为哪一种(上面7中类型的一种),所以需要确定是哪一种
String r0 = simpleDateFormat.format(epoch);
// 7. 下一个时间同样也用datePattern格式化为字符串,方便比较,这里变化只有i,是7种时间粒度类型的一种
// 即循环7种时间粒度类型来比较,看看配置的是哪一种时间类型
Date next = innerGetEndOfThisPeriod(calendar, i, epoch);
String r1 = simpleDateFormat.format(next);
if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
return i;
}
}
}
return PeriodicityType.ERRONEOUS;
}
static private Date innerGetEndOfThisPeriod(Calendar cal, PeriodicityType periodicityType, Date now) {
// 8. periodicityType是当前的周期类型(遍历7种类型),now是1970-01-01 00:00:00,提供加1的参数
return innerGetEndOfNextNthPeriod(cal, periodicityType, now, 1);
}
static private Date innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Date now, int numPeriods) {
// 9. 按周期类型给指定时间now加上指定粒度的1,如果时间粒度是天则加上1天,如果时间粒度是毫秒则加上1毫秒等
cal.setTime(now);
switch (periodicityType) {
case TOP_OF_MILLISECOND:
cal.add(Calendar.MILLISECOND, numPeriods);
break;
case TOP_OF_SECOND:
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.SECOND, numPeriods);
break;
case TOP_OF_MINUTE:
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MINUTE, numPeriods);
break;
case TOP_OF_HOUR:
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.HOUR_OF_DAY, numPeriods);
break;
case TOP_OF_DAY:
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.DATE, numPeriods);
break;
case TOP_OF_WEEK:
cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
break;
case TOP_OF_MONTH:
cal.set(Calendar.DATE, 1);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MONTH, numPeriods);
break;
default:
throw new IllegalStateException("Unknown periodicity type.");
}
return cal.getTime();
}
// 回到RollingCalendar继续处理加上了1时间粒度的时间
// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public PeriodicityType computePeriodicityType() {
GregorianCalendar calendar = new GregorianCalendar(GMT_TIMEZONE, Locale.getDefault());
Date epoch = new Date(0); // 设置初始值为1970-01-01 00:00:00 GMT
if (datePattern != null) {
// PeriodicityType.VALID_ORDERED_LIST里提供了从毫秒到月这7种粒度的类型
// TOP_OF_MILLISECOND 毫秒
// PeriodicityType.TOP_OF_SECOND 秒
// PeriodicityType.TOP_OF_MINUTE 分钟
// PeriodicityType.TOP_OF_HOUR 小时
// PeriodicityType.TOP_OF_DAY 天
// PeriodicityType.TOP_OF_WEEK 周
// PeriodicityType.TOP_OF_MONTH 月
for (PeriodicityType i : PeriodicityType.VALID_ORDERED_LIST) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
simpleDateFormat.setTimeZone(GMT_TIMEZONE); // all date formatting done in GMT
// 6. epoch是一个1970-01-01 00:00:00的时间(到毫秒),用datePattern去把这个时间格式化成一个字符串
// 由于不确定datePattern配置为哪一种(上面7中类型的一种),所以需要确定是哪一种
String r0 = simpleDateFormat.format(epoch);
// 7. 下一个时间同样也用datePattern格式化为字符串,方便比较
Date next = innerGetEndOfThisPeriod(calendar, i, epoch);
String r1 = simpleDateFormat.format(next);
// 10. r0和r1都是时间格式化来的,r1是在1970-01-01 00:00:00这个时间点上按类型加1个时间粒度得来的
// 比如:以时间粒度为天作为例子,datePattern是yyyy-MM-dd,此时r0=1970-01-01,
// 遍历上面7种类型,第一种是毫秒,那么就在毫秒上加1,加1后用datePattern格式化,
// 此时r1=1970-01-01,由于格式化后的值是到天的,所以毫秒加1就不影响,
// 所以r0=r1,说明配置的datePattern并不是毫秒的类型,然后遍历下一种类型;
// 当遍历到天类型时,天加1后格式化,此时r1=1970-01-02,r0 != r1,说明是天类型,
// 从而找到了datePattern的类型,返回此类型,RollingCalendar初始化完成
if ((r0 != null) && (r1 != null) && !r0.equals(r1)) {
return i;
}
}
}
return PeriodicityType.ERRONEOUS;
}
// 回到TimeBasedFileNamingAndTriggeringPolicyBase的start()方法,使用初始化好的RollingCalendar
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
public void start() {
DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
if (dtc == null) {
throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
}
// 4. 初始化RollingCalendar,用于计算拆分文件的时间点
// 比如:当pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,dtc.getDatePattern()为yyyy-MM-dd
if (dtc.getTimeZone() != null) {
rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
} else {
rc = new RollingCalendar(dtc.getDatePattern());
}
addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '" + tbrp.fileNamePattern.getPattern() + "'.");
rc.printPeriodicity(this);
if (!rc.isCollisionFree()) {
addError("The date format in FileNamePattern will result in collisions in the names of archived log files.");
addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
withErrors();
return;
}
// 11. 设置当前的时间点
// 先把当前时间作为默认值,如果历史上还没有打印过日志,那么这个时间就是当前时间点;
// 然后从已有的日志文件中找出最后一次修改的时间作为当前时间点,如果中间很长时间没打印日志,那么这个时间可以是很久以前的;
setDateInCurrentPeriod(new Date(getCurrentTime()));
// 当pattern为logs/app.%d{yyyy-MM-dd}.%i.log时,tbrp.getParentsRawFileProperty()为logs/app.log
if (tbrp.getParentsRawFileProperty() != null) {
File currentFile = new File(tbrp.getParentsRawFileProperty());
if (currentFile.exists() && currentFile.canRead()) {
setDateInCurrentPeriod(new Date(currentFile.lastModified())); // 文件上次修改的时间
}
}
addInfo("Setting initial period to " + dateInCurrentPeriod);
// 12. 计算下一个日志文件要拆分的时间点
computeNextCheck();
}
public void setDateInCurrentPeriod(Date _dateInCurrentPeriod) {
this.dateInCurrentPeriod = _dateInCurrentPeriod; // 记录当前时间点
}
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
protected void computeNextCheck() {
// 13. rc为上面初始化好的RollingCalendar,调用其getNextTriggeringDate方法,参数是记录好的当前时间点
this.nextCheck = this.rc.getNextTriggeringDate(this.dateInCurrentPeriod).getTime();
}
// 源码位置:ch.qos.logback.core.rolling.helper.RollingCalendar
public Date getNextTriggeringDate(Date now) {
// 14. 按时间粒度类型,获取下一个日志要拆分的时间点,多提供参数1(表示一个时间粒度)
return getEndOfNextNthPeriod(now, 1);
}
public Date getEndOfNextNthPeriod(Date now, int periods) {
// 15. 计算下一个拆分日志文件的时间点,多提供参数当前类型(该类型为之前在computePeriodicityType()中计算出来的datePattern类型)
// 计算方式参考上面步骤9,比如时间粒度类型为天,那下一个要拆分的时间点应该是当前时间点的天加上1天
return innerGetEndOfNextNthPeriod(this, this.periodicityType, now, periods);
}
// 回到TimeBasedFileNamingAndTriggeringPolicyBase的computeNextCheck(),记录计算出来的下一个拆分日志文件的时间点
// 源码位置:ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicyBase
protected void computeNextCheck() {
// 16. 用nextCheck记录计算出来的下一个拆分日志文件的时间点
this.nextCheck = this.rc.getNextTriggeringDate(this.dateInCurrentPeriod).getTime();
}
2.2.2 拆分日志文件
按上面配置,时间到了(比如按天)就需要拆分文件,文件大小到达上限之后,也需要拆分文件,是否要拆分文件是在写日志的时候进行的,如果要拆分日志则先拆分文件,然后再写当前的日志。
// 1. logback处理写日志的时候,会把日志信息包装成一个eventObject,append到文件的末尾
// 源码位置:ch.qos.logback.core.OutputStreamAppender
protected void append(E eventObject) {
if (!isStarted()) {
return;
}
// 2. 调子类的subAppend(),例子配的子类appender为RollingFileAppender
subAppend(eventObject);
}
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(E event) {
synchronized (triggeringPolicy) {
// 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
rollover();
}
}
super.subAppend(event);
}
// 例子中配的拆分文件策略是基于时间和大小
// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public boolean isTriggeringEvent(File activeFile, final E event) {
long time = getCurrentTime();
// 4. 按时间判断是否要拆分文件
// 在初始化的时候已经计算好nextCheck,其来源要么是初始化时的当前时间,或者是日志文件的最后一次修改时间;
// nextCheck是在这个时间的基础上按时间粒度类型加1得到的,比如时间粒度是按天,那么就是加了1天;
// 所以这里用写日志的当前时间和nextCheck比较,如果当前时间比nextCheck大,就是需要拆分文件了;
if (time >= nextCheck) {
Date dateInElapsedPeriod = this.dateInCurrentPeriod;
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInElapsedPeriod, currentPeriodsCounter);
currentPeriodsCounter = 0;
setDateInCurrentPeriod(time);
computeNextCheck(); // 同样的方式用当前时间按时间粒度类型加1更新下一次的nextCheck时间
return true;
}
if (invocationGate.isTooSoon(time)) {
return false;
}
if (activeFile == null) {
addWarn("activeFile == null");
return false;
}
if (maxFileSize == null) {
addWarn("maxFileSize = null");
return false;
}
// 5. 如果文件的大小比指定的单个文件最大值大,则也是要拆分文件的
if (activeFile.length() >= maxFileSize.getSize()) {
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInCurrentPeriod, currentPeriodsCounter);
currentPeriodsCounter++;
return true;
}
return false;
}
// 回到RollingFileAppender的subAppend(),处理日志文件拆分
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(),处理日志文件拆分(E event) {
synchronized (triggeringPolicy) {
// 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
// 6. 拆分文件
rollover();
}
}
super.subAppend(event);
}
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
public void rollover() {
lock.lock();
try {
// 7. 先关掉文件流,避免当前日志丢失,也使得文件可以拆分
this.closeOutputStream();
// 8. 拆分文件
attemptRollover();
attemptOpenFile();
} finally {
lock.unlock();
}
}
private void attemptRollover() {
try {
// 9. 调用TimeBasedRollingPolicy的rollover()拆分文件
rollingPolicy.rollover();
} catch (RolloverFailure rf) {
addWarn("RolloverFailure occurred. Deferring roll-over.");
this.append = true;
}
}
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void rollover() throws RolloverFailure {
String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
// 10. 在拆分文件之前已经关闭文件流,拆分文件实际就是把文件重命名一下,然后写同名新文件
// 这里compressionMode是指是否配置了文件压缩,如果配置了压缩,除了重命名之外还得把重命名之后的文件压缩一下
if (compressionMode == CompressionMode.NONE) {
if (getParentsRawFileProperty() != null) {
renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
}
} else {
if (getParentsRawFileProperty() == null) {
compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
} else {
compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
}
}
if (archiveRemover != null) {
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
}
// 回到RollingFileAppender的subAppend(),写日志
// 源码位置:ch.qos.logback.core.rolling.RollingFileAppender
protected void subAppend(),处理日志文件拆分(E event) {
synchronized (triggeringPolicy) {
// 3. 判断是否达到需要拆分文件的条件,例子中triggeringPolicy配的是SizeAndTimeBasedFNATP
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
// 6. 拆分文件
rollover();
}
}
// 11. 在父类方法中写日志
super.subAppend(event);
}
// 源码位置:ch.qos.logback.core.OutputStreamAppender
protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
byte[] byteArray = this.encoder.encode(event);
// 12. 写日志
writeBytes(byteArray);
} catch (IOException ioe) {
// as soon as an exception occurs, move to non-started state
// and add a single ErrorStatus to the SM.
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
2.2.3 删除多余文件
如果配置了文件个数和文件大小限制,则达到了这些限制,需要把超限的文件删除掉,从而保持日志文件不超限。
// 在TimeBasedRollingPolicy中先初始化文件删除器
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
// 省略其它代码
if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
timeBasedFileNamingAndTriggeringPolicy.setContext(context);
timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
// 1. 在TriggeringPolicy创建文件删除器,例子中配置的是SizeAndTimeBasedFNATP
timeBasedFileNamingAndTriggeringPolicy.start();
if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
return;
}
if (maxHistory != UNBOUND_HISTORY) {
archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
archiveRemover.setMaxHistory(maxHistory);
archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
if (cleanHistoryOnStart) {
addInfo("Cleaning on start up");
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
} else if (!isUnboundedTotalSizeCap()) {
addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
}
super.start();
}
// 源码位置:ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP
public void start() {
// 2. 创建文件删除器
archiveRemover = createArchiveRemover();
archiveRemover.setContext(context);
// 省略其它代码
}
protected ArchiveRemover createArchiveRemover() {
// 3. 创建的是SizeAndTimeBasedArchiveRemover文件删除器,提供了文件名pattern,rc是RollingCalendar
return new SizeAndTimeBasedArchiveRemover(tbrp.fileNamePattern, rc);
}
// 回到TimeBasedRollingPolicy的start(),继续初始化
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
// 省略其它代码
if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
timeBasedFileNamingAndTriggeringPolicy.setContext(context);
timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
// 1. 在TriggeringPolicy创建文件删除器,例子中配置的是SizeAndTimeBasedFNATP
timeBasedFileNamingAndTriggeringPolicy.start();
if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
return;
}
// 4. 如果配置了maxHistory,那么文件删除器archiveRemover才起作用
if (maxHistory != UNBOUND_HISTORY) {
archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
archiveRemover.setMaxHistory(maxHistory); // 设置文件个数最大限制
archiveRemover.setTotalSizeCap(totalSizeCap.getSize()); // 设置文件大小最大限制
if (cleanHistoryOnStart) {
addInfo("Cleaning on start up");
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
} else if (!isUnboundedTotalSizeCap()) {
addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
}
super.start(); // 父类的start()只是把this.started置成true
}
// 在打印日志的时候会拆分文件,在拆分文件的时候,触发文件删除器工作
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void rollover() throws RolloverFailure {
String elapsedPeriodsFileName = timeBasedFileNamingAndTriggeringPolicy.getElapsedPeriodsFileName();
String elapsedPeriodStem = FileFilterUtil.afterLastSlash(elapsedPeriodsFileName);
if (compressionMode == CompressionMode.NONE) {
if (getParentsRawFileProperty() != null) {
renameUtil.rename(getParentsRawFileProperty(), elapsedPeriodsFileName);
}
} else {
if (getParentsRawFileProperty() == null) {
compressionFuture = compressor.asyncCompress(elapsedPeriodsFileName, elapsedPeriodsFileName, elapsedPeriodStem);
} else {
compressionFuture = renameRawAndAsyncCompress(elapsedPeriodsFileName, elapsedPeriodStem);
}
}
// 如果文件删除器存在,则触发文件删除操作
if (archiveRemover != null) {
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
// 5. 触发文件删除器的异步删除文件操作,archiveRemover为TimeBasedArchiveRemover
this.cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
}
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public Future<?> cleanAsynchronously(Date now) {
// 6. 创建一个runnable,放到线程池中异步执行
ArhiveRemoverRunnable runnable = new ArhiveRemoverRunnable(now);
ExecutorService executorService = context.getScheduledExecutorService();
Future<?> future = executorService.submit(runnable);
return future;
}
// ArhiveRemoverRunnable是TimeBasedArchiveRemover的内部类
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover.ArhiveRemoverRunnable
public class ArhiveRemoverRunnable implements Runnable {
Date now;
ArhiveRemoverRunnable(Date now) {
this.now = now;
}
@Override
public void run() {
// 7. 根据文件个数上限删除多余文件
clean(now);
if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
capTotalSize(now);
}
}
}
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void clean(Date now) {
long nowInMillis = now.getTime();
// 8. 计算文件的周期数量
// 上次删除文件的时间到当前时间的毫秒数,除以周期,得到循环的次数
// 如果文件名pattern里配置的是按天拆分文件,那么周期就是天,这个计算就是得到目前有多少天的日志文件
// 注意:如果一个周期内有多个文件,在此只算1
int periodsElapsed = computeElapsedPeriodsSinceLastClean(nowInMillis);
lastHeartBeat = nowInMillis;
if (periodsElapsed > 1) {
addInfo("Multiple periods, i.e. " + periodsElapsed + " periods, seem to have elapsed. This is expected at application start.");
}
// 9. 遍历周期,删除多余文件
for (int i = 0; i < periodsElapsed; i++) {
// 从下面代码可看出offset=(-maxHistory - 1),也就是比允许留下文件周期数大一个
int offset = getPeriodOffsetForDeletionTarget() - i;
// 计算出offset对应的时间,这个就是需要删除的文件
// 比如周期是按天,配置maxHistory为保留7天,那从当前往前数到第8天(maxHistory+1)就是要删除的文件
Date dateOfPeriodToClean = rc.getEndOfNextNthPeriod(now, offset);
cleanPeriod(dateOfPeriodToClean);
}
}
protected int getPeriodOffsetForDeletionTarget() {
return -maxHistory - 1;
}
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void cleanPeriod(Date dateOfPeriodToClean) {
// 10. 调用子类的方法找符合条件的文件,子类为SizeAndTimeBasedArchiveRemover
File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean);
for (File f : matchingFileArray) {
addInfo("deleting " + f);
f.delete();
}
if (parentClean && matchingFileArray.length > 0) {
File parentDir = getParentDir(matchingFileArray[0]);
removeFolderIfEmpty(parentDir);
}
}
// 源码位置:ch.qos.logback.core.rolling.helper.SizeAndTimeBasedArchiveRemover
protected File[] getFilesInPeriod(Date dateOfPeriodToClean) {
File archive0 = new File(fileNamePattern.convertMultipleArguments(dateOfPeriodToClean, 0));
File parentDir = getParentDir(archive0);
String stemRegex = createStemRegex(dateOfPeriodToClean);
// 11. 匹配符合条件的文件
// 比如文件名pattern为logs/app.%d{yyyy-MM-dd}.%i.log,用dateOfPeriodToClean替换%d{yyyy-MM-dd},
// 然后去匹配%i,例子中周期是天,也就是匹配这一天的多个文件
File[] matchingFileArray = FileFilterUtil.filesInFolderMatchingStemRegex(parentDir, stemRegex);
return matchingFileArray;
}
// 回到TimeBasedArchiveRemover的cleanPeriod(),删除匹配到的文件
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
public void cleanPeriod(),删除匹配到的文件(Date dateOfPeriodToClean) {
// 10. 调用子类的方法找符合条件的文件
File[] matchingFileArray = getFilesInPeriod(dateOfPeriodToClean);
// 12. 删除找到的文件,如果文件夹里没有文件了就删除文件夹
for (File f : matchingFileArray) {
addInfo("deleting " + f);
f.delete();
}
if (parentClean && matchingFileArray.length > 0) {
File parentDir = getParentDir(matchingFileArray[0]);
removeFolderIfEmpty(parentDir);
}
}
// 回到TimeBasedArchiveRemover.ArhiveRemoverRunnable的run(),处理文件容量
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover.ArhiveRemoverRunnable
public class ArhiveRemoverRunnable implements Runnable {
Date now;
ArhiveRemoverRunnable(Date now) {
this.now = now;
}
@Override
public void run() {
// 7. 根据文件个数上限删除多余文件
clean(now);
// 13. 根据文件大小上限删除超限文件,UNBOUNDED_TOTAL_SIZE_CAP = 0
if (totalSizeCap != UNBOUNDED_TOTAL_SIZE_CAP && totalSizeCap > 0) {
capTotalSize(now);
}
}
}
// 源码位置:ch.qos.logback.core.rolling.helper.TimeBasedArchiveRemover
void capTotalSize(Date now) {
long totalSize = 0;
long totalRemoved = 0;
// 14. 文件是按时间周期拆分的,每个周期可能有多个文件
// 如果要求文件大小不超总量限制,那么应该从时间的近到远,从一个周期内的序号大到小进行遍历
// 遍历到一个文件就把大小加起来,如果达到总量限制,那就把文件删除
// 把时间和序号倒排,方便在一个循环里完成所有多余文件删除
// 多出周期数的文件已经被删除,在这里按周期数遍历即可
for (int offset = 0; offset < maxHistory; offset++) {
// 计算出当前时间前一个周期的时间
Date date = rc.getEndOfNextNthPeriod(now, -offset);
// 获取到一个周期内的多个文件
File[] matchingFileArray = getFilesInPeriod(date);
// 文件按序号倒排
descendingSortByLastModified(matchingFileArray);
// 把一个个文件的length加起来,如果超过总量限制,则删除文件
for (File f : matchingFileArray) {
long size = f.length();
if (totalSize + size > totalSizeCap) {
addInfo("Deleting [" + f + "]" + " of size " + new FileSize(size));
totalRemoved += size;
f.delete();
}
totalSize += size;
}
}
addInfo("Removed " + new FileSize(totalRemoved) + " of files");
}
从上面可以看出,“文件的个数”限制并不是总文件个数,而是文件周期数。比如文件是按天拆分的,那么这个文件个数限制就是天数;若文件是按分钟拆分的,那么这个限制就是分钟数。每个周期内可能会有多个日志文件,所以文件个数是所有文件周期里的文件个数的总和,即文件个数比文件周期数大。所以,准确来说maxHistory配置的是文件周期数,这个如果不看源码,并不好理解,其配置的名称是maxhistory,而不是fileCount之类的,可能也是因为其不是文件个数。
3 架构一小步
增加日志的文件个数和大小限制配置,保护好系统,避免日志量过大而影响业务正常运行,注意文件个数是指周期数(比如按天拆分文件那么就是天数):
<property name="log.file.path" value="logs" /> <!-- 日志存放目录 -->
<property name="log.filename.pattern" value="${log.file.path}/app.%d{yyyy-MM-dd}.%i.log" /> <!-- 日志文件名格式 -->
<property name="log.file.maxsize" value="30MB" /> <!-- 每个日志文件最大的大小 -->
<property name="log.file.maxhistory" value="30" /> <!-- 日志文件的最大保留个数,默认为0(无限) -->
<property name="log.file.max.capacity" value="10GB" /> <!-- 所有日志文件总大小的上限,需要配了文件最大保留个数才有效,默认为0(无限) -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.file.path}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.filename.pattern}</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${log.file.maxsize}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>${log.file.maxhistory}</maxHistory>
<totalSizeCap>${log.file.max.capacity}</totalSizeCap>
</rollingPolicy>
</appender>