log4j2 自动删除过期日志文件的配置及实现原理
日志文件自动删除功能必不可少,当然你可以让运维去做这事,只是这不地道。而日志组件是一个必备组件,让其多做一件删除的工作,无可厚非。本文就来探讨下log4j的日志文件自动删除实现吧。
0.自动删除配置参考样例:(log4j2.xml)
info
如果仅想停留在使用层面,如上log4j2.xml配置文件足矣!
不过,至少得注意一点,以上配置需要基于log4j2,而如果你是log4j1.x,则需要做下无缝升级:主要就是换下jar包版本,换个桥接包之类的,比如下参考配置:
commons-logging commons-logging 1.2 org.slf4j jcl-over-slf4j 1.7.26 org.apache.commons commons-compress 1.10 org.apache.logging.log4j log4j-core 2.8.2 org.apache.logging.log4j log4j-slf4j-impl 2.8.2 org.apache.logging.log4j log4j-api 2.8.2 org.apache.logging.log4j log4j-web 2.8.2
如果还想多了解一点其运行原理,就跟随本文的脚步吧:
1.自动清理大体运行流程
自动删除工作的运行原理大体流程如下。(大抵都是如此)
1.加载log4j2.xml配置文件;
2.读取appenders,并添加到log4j上下文中;
3.加载policy,加载rollover配置;
4.写入日志时判断是否满足rollover配置,默认是一天运行一次,可自行添加各种运行测试,比如大小、启动时;
所以,删除策略的核心是每一次添加日志时。代码验证如下:
//在每次添加日志时判定 //org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender#append /** *Writethelogentryrollingoverthefilewhenrequired. * *@parameventTheLogEvent. */ @Override publicvoidappend(finalLogEventevent){ finalRollingRandomAccessFileManagermanager=getManager(); //重点:直接检查是否需要rollover,如需要直接进行 manager.checkRollover(event); //LeveragethenicebatchingbehaviourofasyncLoggers/Appenders: //wecansignalthefilemanagerthatitneedstoflushthebuffer //todiskattheendofabatch. //Fromauser'spointofview,thismeansthatalllogeventsare //_always_availableinthelogfile,withoutincurringtheoverhead //ofimmediateFlush=true. manager.setEndOfBatch(event.isEndOfBatch());//FIXMEmanager'sEndOfBatchthreadlocalcanbedeleted //LOG4J2-1292utilizegc-freeLayout.encode()method:takencareofinsuperclass super.append(event); } //org.apache.logging.log4j.core.appender.rolling.RollingFileManager#checkRollover /** *Determinesifarollovershouldoccur. *@parameventTheLogEvent. */ publicsynchronizedvoidcheckRollover(finalLogEventevent){ //由各触发策略判定是否需要进行rolling //如需要,则调用rollover() if(triggeringPolicy.isTriggeringEvent(event)){ rollover(); } }
所以,何时进行删除?答案是在适当的时机,这个时机可以是任意时候。
2.log4j日志滚动
日志滚动,可以是重命名,也可以是删除文件。但总体判断是否可触发滚动的前提是一致的。我们这里主要关注文件删除。我们以时间作为依据看下判断过程。
//1.判断是否是触发事件时机 //org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy#isTriggeringEvent /** *Determineswhetherarollovershouldoccur. *@parameventAreferencetothecurrentlyevent. *@returntrueifarollovershouldoccur. */ @Override publicbooleanisTriggeringEvent(finalLogEventevent){ if(manager.getFileSize()==0){ returnfalse; } finallongnowMillis=event.getTimeMillis(); //TimeBasedTriggeringPolicy,是基于时间判断的,此处为每天一次 if(nowMillis>=nextRolloverMillis){ nextRolloverMillis=manager.getPatternProcessor().getNextTime(nowMillis,interval,modulate); returntrue; } returnfalse; } //org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover() publicsynchronizedvoidrollover(){ if(!hasOutputStream()){ return; } //strategy是xml配置的策略 if(rollover(rolloverStrategy)){ try{ size=0; initialTime=System.currentTimeMillis(); createFileAfterRollover(); }catch(finalIOExceptione){ logError("Failedtocreatefileafterrollover",e); } } } //RollingFileManager统一管理触发器 //org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover privatebooleanrollover(finalRolloverStrategystrategy){ booleanreleaseRequired=false; try{ //Blockuntiltheasynchronousoperationiscompleted. //上锁保证线程安全 semaphore.acquire(); releaseRequired=true; }catch(finalInterruptedExceptione){ logError("Threadinterruptedwhileattemptingtocheckrollover",e); returnfalse; } booleansuccess=true; try{ //由各触发器运行rollover逻辑 finalRolloverDescriptiondescriptor=strategy.rollover(this); if(descriptor!=null){ writeFooter(); closeOutputStream(); if(descriptor.getSynchronous()!=null){ LOGGER.debug("RollingFileManagerexecutingsynchronous{}",descriptor.getSynchronous()); try{ //先使用同步方法,改名,然后再使用异步方法操作更多 success=descriptor.getSynchronous().execute(); }catch(finalExceptionex){ success=false; logError("Caughterrorinsynchronoustask",ex); } } //如果配置了异步器,则使用异步进行rollover if(success&&descriptor.getAsynchronous()!=null){ LOGGER.debug("RollingFileManagerexecutingasync{}",descriptor.getAsynchronous()); //CompositeAction,使用异步线程池运行用户的action asyncExecutor.execute(newAsyncAction(descriptor.getAsynchronous(),this)); //在异步运行action期间,锁是不会被释放的,以避免线程安全问题 //直到异步任务完成,再主动释放锁 releaseRequired=false; } returntrue; } returnfalse; }finally{ if(releaseRequired){ semaphore.release(); } } }
此处滚动有两个处理点,1.每个滚动策略可以自行处理业务;2.RollingFileManager统一管理触发同步和异步的滚动action;
3.DefaultRolloverStrategy默认滚动策略驱动
DefaultRolloverStrategy作为一个默认的滚动策略实现,可以配置多个Action,然后处理删除操作。
删除有两种方式:1.当次滚动的文件数过多,会立即进行删除;2.配置单独的DeleteAction,根据配置的具体策略进行删除。(但该Action只会被返回给外部调用,自身则不会执行)
//org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#rollover /** *Performstherollover. * *@parammanagerTheRollingFileManagernameforcurrentactivelogfile. *@returnARolloverDescription. *@throwsSecurityExceptionifanerroroccurs. */ @Override publicRolloverDescriptionrollover(finalRollingFileManagermanager)throwsSecurityException{ intfileIndex; //默认minIndex=1 if(minIndex==Integer.MIN_VALUE){ finalSortedMapeligibleFiles=getEligibleFiles(manager); fileIndex=eligibleFiles.size()>0?eligibleFiles.lastKey()+1:1; }else{ if(maxIndex<0){ returnnull; } finallongstartNanos=System.nanoTime(); //删除case1:获取符合条件的文件数,同时清理掉大于max配置的日志文件 //如配置max=5,当前只有4个满足时,不会立即清理文件,但也不会阻塞后续流程 //只要没有出现错误,fileIndex不会小于0 fileIndex=purge(minIndex,maxIndex,manager); if(fileIndex<0){ returnnull; } if(LOGGER.isTraceEnabled()){ finaldoubledurationMillis=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-startNanos); LOGGER.trace("DefaultRolloverStrategy.purge()took{}milliseconds",durationMillis); } } //进入此区域即意味着,必然有文件需要滚动,重新命名了 finalStringBuilderbuf=newStringBuilder(255); manager.getPatternProcessor().formatFileName(strSubstitutor,buf,fileIndex); finalStringcurrentFileName=manager.getFileName(); StringrenameTo=buf.toString(); finalStringcompressedName=renameTo; ActioncompressAction=null; FileExtensionfileExtension=manager.getFileExtension(); if(fileExtension!=null){ renameTo=renameTo.substring(0,renameTo.length()-fileExtension.length()); compressAction=fileExtension.createCompressAction(renameTo,compressedName, true,compressionLevel); } //未发生文件重命名情况,即文件未被重命名未被滚动 //该种情况应该不太会发生 if(currentFileName.equals(renameTo)){ LOGGER.warn("Attempttorenamefile{}toitselfwillbeignored",currentFileName); returnnewRolloverDescriptionImpl(currentFileName,false,null,null); } //新建一个重命令的action,返回待用 finalFileRenameActionrenameAction=newFileRenameAction(newFile(currentFileName),newFile(renameTo), manager.isRenameEmptyFiles()); //异步处理器,会处理用户配置的异步action,如本文配置的DeleteAction //它将会在稍后被提交到异步线程池中运行 finalActionasyncAction=merge(compressAction,customActions,stopCustomActionsOnError); //封装Rollover返回,renameAction是同步方法,其他用户配置的动态action则是异步方法 //删除case2:封装异步返回action returnnewRolloverDescriptionImpl(currentFileName,false,renameAction,asyncAction); } privateintpurge(finalintlowIndex,finalinthighIndex,finalRollingFileManagermanager){ //默认使用accending的方式进行清理文件 returnuseMax?purgeAscending(lowIndex,highIndex,manager):purgeDescending(lowIndex,highIndex,manager); } //org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#purgeAscending /** *Purgesandrenamesoldlogfilesinpreparationforrollover.Theoldestfilewillhavethesmallestindex,the *newestthehighest. * *@paramlowIndexlowindex.Logfileassociatedwithlowindexwillbedeletedifneeded. *@paramhighIndexhighindex. *@parammanagerTheRollingFileManager *@returntrueifpurgewassuccessfulandrollovershouldbeattempted. */ privateintpurgeAscending(finalintlowIndex,finalinthighIndex,finalRollingFileManagermanager){ finalSortedMap eligibleFiles=getEligibleFiles(manager); finalintmaxFiles=highIndex-lowIndex+1; booleanrenameFiles=false; //依次迭代eligibleFiles,删除 while(eligibleFiles.size()>=maxFiles){ try{ LOGGER.debug("Eligiblefiles:{}",eligibleFiles); Integerkey=eligibleFiles.firstKey(); LOGGER.debug("Deleting{}",eligibleFiles.get(key).toFile().getAbsolutePath()); //调用nio的接口删除文件 Files.delete(eligibleFiles.get(key)); eligibleFiles.remove(key); renameFiles=true; }catch(IOExceptionioe){ LOGGER.error("Unabletodelete{},{}",eligibleFiles.firstKey(),ioe.getMessage(),ioe); break; } } finalStringBuilderbuf=newStringBuilder(); if(renameFiles){ //针对未完成删除的文件,继续处理 //比如使用匹配的方式匹配文件,则不能被正常删除 //还有些未超过maxFiles的文件 for(Map.Entry entry:eligibleFiles.entrySet()){ buf.setLength(0); //LOG4J2-531:directoryscan&rollovermustusesameformat manager.getPatternProcessor().formatFileName(strSubstitutor,buf,entry.getKey()-1); StringcurrentName=entry.getValue().toFile().getName(); StringrenameTo=buf.toString(); intsuffixLength=suffixLength(renameTo); if(suffixLength>0&&suffixLength(currentName)==0){ renameTo=renameTo.substring(0,renameTo.length()-suffixLength); } Actionaction=newFileRenameAction(entry.getValue().toFile(),newFile(renameTo),true); try{ LOGGER.debug("DefaultRolloverStrategy.purgeAscendingexecuting{}",action); if(!action.execute()){ return-1; } }catch(finalExceptionex){ LOGGER.warn("ExceptionduringpurgeinRollingFileAppender",ex); return-1; } } } //此处返回的findIndex一定是>=0的 returneligibleFiles.size()>0? (eligibleFiles.lastKey() 4.符合过滤条件的文件查找
当配置了max参数,这个参数是如何匹配的呢?比如我某个文件夹下有很历史文件,是否都会匹配呢?
//文件查找规则 //org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy#getEligibleFiles protectedSortedMapgetEligibleFiles(finalRollingFileManagermanager){ returngetEligibleFiles(manager,true); } protectedSortedMap getEligibleFiles(finalRollingFileManagermanager, finalbooleanisAscending){ finalStringBuilderbuf=newStringBuilder(); //此处的pattern即是在appender上配置的filePattern,一般会受限于MM-dd-yyyy-$i.log.gz Stringpattern=manager.getPatternProcessor().getPattern(); //此处会将时间替换为当前,然后按照此规则进行匹配要处理的文件 manager.getPatternProcessor().formatFileName(strSubstitutor,buf,NotANumber.NAN); returngetEligibleFiles(buf.toString(),pattern,isAscending); } //细节匹配要处理的文件 protectedSortedMap getEligibleFiles(Stringpath,StringlogfilePattern,booleanisAscending){ TreeMap eligibleFiles=newTreeMap<>(); Filefile=newFile(path); Fileparent=file.getParentFile(); if(parent==null){ parent=newFile("."); }else{ parent.mkdirs(); } if(!logfilePattern.contains("%i")){ returneligibleFiles; } Pathdir=parent.toPath(); StringfileName=file.getName(); intsuffixLength=suffixLength(fileName); if(suffixLength>0){ fileName=fileName.substring(0,fileName.length()-suffixLength)+".*"; } StringfilePattern=fileName.replace(NotANumber.VALUE,"(\\d+)"); Patternpattern=Pattern.compile(filePattern); try(DirectoryStream stream=Files.newDirectoryStream(dir)){ for(Pathentry:stream){ //该匹配相当精确 //只会删除当天或者在时间交替的时候删除上一天的数据咯 //如果在这个时候进行了重启操作,就再也不会删除此文件了 Matchermatcher=pattern.matcher(entry.toFile().getName()); if(matcher.matches()){ Integerindex=Integer.parseInt(matcher.group(1)); eligibleFiles.put(index,entry); } } }catch(IOExceptionioe){ thrownewLoggingException("Errorreadingfolder"+dir+""+ioe.getMessage(),ioe); } returnisAscending?eligibleFiles:eligibleFiles.descendingMap(); } //此处会将各种格式的文件名,替换为当前时间或者最后一次滚动的文件的时间。所以匹配的时候,并不会匹配超时当前认知范围的文件 /** *Formatsfilename. *@paramsubstTheStrSubstitutor. *@parambufstringbuffertowhichformattedfilenameisappended,maynotbenull. *@paramobjobjecttobeevaluatedinformatting,maynotbenull. */ publicfinalvoidformatFileName(finalStrSubstitutorsubst,finalStringBuilderbuf,finalbooleanuseCurrentTime, finalObjectobj){ //LOG4J2-628:wedeliberatelyuseSystemtime,notthelog4j.Clocktime //forcreatingthefilenameofrolled-overfiles. finallongtime=useCurrentTime&¤tFileTime!=0?currentFileTime: prevFileTime!=0?prevFileTime:System.currentTimeMillis(); formatFileName(buf,newDate(time),obj); finalLogEventevent=newLog4jLogEvent.Builder().setTimeMillis(time).build(); finalStringfileName=subst.replace(event,buf); buf.setLength(0); buf.append(fileName); } AsyncAction是一个Runnable的实现,被直接提交到线程池运行.AsyncAction->AbstractAction->Action->Runnable
它是一个统一管理异步Action的包装,主要是管理锁和异常类操作。
//org.apache.logging.log4j.core.appender.rolling.RollingFileManager.AsyncAction /** *Performsactionsasynchronously. */ privatestaticclassAsyncActionextendsAbstractAction{ privatefinalActionaction; privatefinalRollingFileManagermanager; /** *Constructor. *@paramactTheactiontoperform. *@parammanagerThemanager. */ publicAsyncAction(finalActionact,finalRollingFileManagermanager){ this.action=act; this.manager=manager; } /** *Executesanaction. * *@returntrueifactionwassuccessful.Areturnvalueoffalsewillcause *therollovertobeabortedifpossible. *@throwsjava.io.IOExceptionifIOerror,athrownexceptionwillcausetherollover *tobeabortedifpossible. */ @Override publicbooleanexecute()throwsIOException{ try{ //门面调用action.execute(),一般是调用CompositeAction,里面封装了多个action returnaction.execute(); }finally{ //任务执行完成,才会释放外部的锁 //虽然不是很优雅,但是很准确很安全 manager.semaphore.release(); } } ... } //CompositeAction封装了多个action处理 //org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run /** *Executesequenceofactions. * *@returntrueifallactionsweresuccessful. *@throwsIOExceptiononIOerror. */ @Override publicbooleanexecute()throwsIOException{ if(stopOnError){ //依次调用action for(finalActionaction:actions){ if(!action.execute()){ returnfalse; } } returntrue; } booleanstatus=true; IOExceptionexception=null; for(finalActionaction:actions){ try{ status&=action.execute(); }catch(finalIOExceptionex){ status=false; if(exception==null){ exception=ex; } } } if(exception!=null){ throwexception; } returnstatus; }DeleteAction是我们真正关心的动作。
//CompositeAction封装了多个action处理 //org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run /** *Executesequenceofactions. * *@returntrueifallactionsweresuccessful. *@throwsIOExceptiononIOerror. */ @Override publicbooleanexecute()throwsIOException{ if(stopOnError){ //依次调用action for(finalActionaction:actions){ if(!action.execute()){ returnfalse; } } returntrue; } booleanstatus=true; IOExceptionexception=null; for(finalActionaction:actions){ try{ status&=action.execute(); }catch(finalIOExceptionex){ status=false; if(exception==null){ exception=ex; } } } if(exception!=null){ throwexception; } returnstatus; } //DeleteAction做真正的删除动作 //org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute() @Override publicbooleanexecute()throwsIOException{ //如果没有script配置,则直接委托父类处理 returnscriptCondition!=null?executeScript():super.execute(); } org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute() @Override publicbooleanexecute()throwsIOException{ //根据指定的basePath,和过滤条件,选择相关文件 //调用DeleteAction的createFileVisitor(),返回DeletingVisitor returnexecute(createFileVisitor(getBasePath(),pathConditions)); } //org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute(java.nio.file.FileVisitor) @Override publicbooleanexecute(finalFileVisitor visitor)throwsIOException{ //根据maxDepth设置,遍历所有可能的文件路径 //使用Files.walkFileTree()实现,添加到collected中 finalList sortedPaths=getSortedPaths(); trace("Sortedpaths:",sortedPaths); for(finalPathWithAttributeselement:sortedPaths){ try{ //依次调用visitFile,依次判断是否需要删除 visitor.visitFile(element.getPath(),element.getAttributes()); }catch(finalIOExceptionioex){ LOGGER.error("Errorinpost-rolloverDeletewhenvisiting{}",element.getPath(),ioex); visitor.visitFileFailed(element.getPath(),ioex); } } //TODOreturn(visitor.success||ignoreProcessingFailure) returntrue;//donotabortrolloverevenifprocessingfailed } 最终,即和想像的一样:找到要查找的文件夹,遍历各文件,用多个条件判断是否满足。删除符合条件的文件。
只是这其中注意的点:如何删除文件的线程安全性;如何保证删除工作不影响业务线程;很常见的锁和多线程的应用。
5.真正的删除
真正的删除动作就是在DeleteAction中配置的,但上面可以看它是调用visitor的visitFile方法,所以有必要看看是如何真正处理删除的。(实际上前面在purge时已经做过一次删除操作了,所以别被两个点迷惑了,建议尽量只依赖于Delete配置,可以将外部max设置很大以避免两处生效)
//org.apache.logging.log4j.core.appender.rolling.action.DeletingVisitor#visitFile @Override publicFileVisitResultvisitFile(finalPathfile,finalBasicFileAttributesattrs)throwsIOException{ for(finalPathConditionpathFilter:pathConditions){ finalPathrelative=basePath.relativize(file); //遍历所有条件,只要有一个不符合,即不进行删除。 //所以,所以条件是AND关系,没有OR关系 //如果想配置OR关系,只能配置多个DELETE if(!pathFilter.accept(basePath,relative,attrs)){ LOGGER.trace("Notdeletingbase={},relative={}",basePath,relative); returnFileVisitResult.CONTINUE; } } //直接删除文件 if(isTestMode()){ LOGGER.info("Deleting{}(TESTMODE:filenotactuallydeleted)",file); }else{ delete(file); } returnFileVisitResult.CONTINUE; }删除策略配置比如:
另外说明,之所以能够无缝替换,是因为利用了不同实现版本的org/slf4j/impl/StaticLoggerBinder.class,而外部都使用slf4j接口定义实现的,比如org.apache.logging.log4j:log4j-slf4j-impl包的实现。
总结
到此这篇关于log4j2自动删除过期日志文件的配置及实现原理解析的文章就介绍到这了,更多相关log4j2自动删除过期日志文件内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!