${log.file.path}/app.log
${log.filename.pattern}
${log.file.maxsize}
${log.file.maxhistory}
${log.file.max.capacity}
// 在加载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();
}
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
按上面配置,时间到了(比如按天)就需要拆分文件,文件大小到达上限之后,也需要拆分文件,是否要拆分文件是在写日志的时候进行的,如果要拆分日志则先拆分文件,然后再写当前的日志。
// 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));
}
}
如果配置了文件个数和文件大小限制,则达到了这些限制,需要把超限的文件删除掉,从而保持日志文件不超限。
// 在TimeBasedRollingPolicy中先初始化文件删除器
// 源码位置:ch.qos.logback.core.rolling.TimeBasedRollingPolicy
public void start() {
// 省略其它代码
if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy();
}
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();
}
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之类的,可能也是因为其不是文件个数。
增加日志的文件个数和大小限制配置,保护好系统,避免日志量过大而影响业务正常运行,注意文件个数是指周期数(比如按天拆分文件那么就是天数):
${log.file.path}/app.log
${log.filename.pattern}
${log.file.maxsize}
${log.file.maxhistory}
${log.file.max.capacity}