分析日志并不是Java擅长的工作,多数时候我们经常会把这项工作交给shell\Perl\Python之类的脚本去处理。但是很多时候因为所处的“大环境”是Java,在分析日志的时候需要去调用一些Java的接口,那么日志分析工具就得用Java来写了。分析日志是个很繁琐的工作,需要每次都去打开文件、一行行处理然后关闭文件,如果是动态日志分析还需要去维护文件指针。于是这种工作做多了烦了之后就把这些繁琐的通用逻辑抽出来,做成框架的形式,这样在每次编写日志分析程序的时候只需要去关注业务逻辑,而不必去关心指针或者IO之类的琐事。
小项目,没必要起个很文艺的名,其实更重要的是我也不文艺,干脆就叫LogAnalyzerFramework,项目地址:https://github.com/chihongze/LogAnalyzerFramework 下面来简单谈下这东西的设计。
对于一个日志文件的分析做为一个"task",一个task可以包含多个analyzelet,比如我们要分析web服务的日志,既要分析出其中的异常,又要分析用户行为,那么就可以把这两个业务编写成两个不同的analyzelet,这样在遍历一次日志的时候会同时执行多个业务。可以理解为一个事件触发多个监听器的模式。定义task不需要编写任何Java代码,只需要编写一个简单的yaml就可以了,比如:
taskName: testTask pointerName: /data/test_ptr cmdOptions: [a,b,c,d] initParams: {test: true} resetPtrFileTime: 30 logFileName: /data/web_log/logs/www_stdout.log analyzelets: [loganalyzer.core.test.TestAnalyzelet]
taskName:定义任务的名称,没什么大作用,只是在log中做个标识
pointerName:记录文件指针的文件路径,如果分析的是动态增长的log,那么就需要记录上次分析到什么地方,以便下次访问的时候不会重复访问。如果指定这个选项,那么就会按文件中记录的指针来访问文件,如果不指定,那么就每次都是从头开始访问文件。
cmdOptions:接受的命令行参数名称,通过从命令行指定的参数,都会被封装到loganalyzer.core.AnalyzerContext对象当中。可以在Analyzelet中获取它们。
initParams:初始化配置参数,也同样会写到loganalyzer.core.AnalyerContext对象中
resetPtrFileTime:指针文件重置时间。很多时候为了防止一个日志文件过大,会采取通过时间来切割的方式,对于切割的日志文件,指针文件也需要跟随更新,这里指定指针文件的更新周期,时间单位是分钟。
logFileName:要分析的日志文件名。但是很多时候我们要分析的日志文件名需要去动态生成,比如今天要去分析昨天切割的日志,这里的文件名就不能写死,怎么办呢?提供有另一个选项:logFileNameGenerator,只要写一个类实现loganalyzer.core.LogFileNameGenerator,定义文件名的动态生成逻辑就可以了。然后把这个类的全限定名作为值指定给logFileNameGenerator属性即可。嗯,其实同Spring的BeanNameGenerator一样。
analyzelets:这里指定该task要附加运行的analzyelets,日志分析的所有业务逻辑都包含在analyzlet中。
下面来看一下如何编写一个Analyzelet,很简单,只需要实现loganalyzer.core.Analyzelet接口就可,该接口的定义如下:
package loganalyzer.core; /** * 定义一个日志分析逻辑的声明周期 * @author [email protected] * */ public interface Analyzelet { /** * 分析之前要处理的事务,相当于awk的begin操作块 * @param analyzerContext */ public void begin(AnalyzerContext analyzerContext); /** * 处理每一行log的逻辑 * @param line * @param analyzerContext */ public void doLine(String line, AnalyzerContext analyzerContext); /** * 最终结束的逻辑,相当于awk的end操作,注意,如果分析中途发生错误,那么不会执行此方法,而是执行onError方法。 * @param analyzerContext */ public void end(AnalyzerContext analyzerContext); /** * 错误处理逻辑 * @param analyzerContext */ public void onError(AnalyzerContext analyzerContext, Throwable t); /** * 遇到错误是否立即停止 * @return */ public boolean onErrorStop(); }
其实,可以把它想像成shell下awk的几个过程,begin同awk的BEGIN,定义在分析之前要做得事情,比如你要在日志分析的过程中累加一个数值,那么就可以在begin中把它定义到AnalyzerContext里面,AnalyzerContext存储key-value对。然后在分析的时候,在doLine中将这个值累加,在end的时候把这个值做最终处理,比如发个邮件报表之类的。另外比awk多的过程就是onError,定义了出错时的处理逻辑,还有一个项是onErrorStop,如果这个返回的是true,那么一旦在处理的过程中发生错误,那么就会立即停止Analyzelet的处理,否则会忽略这条错误,继续处理下去,需要跟据自己的业务情况来指定返回值。
多数时候我们只需要关注doLine就可以了,那么继承loganalyzer.core.BasicAnalyzelet,实现doLine,其余的方法如果不重写的话,只会输出一行简单的log。
编写完analyzelet和task,就可以进行日志分析了,如何启动呢?
1.从命令行环境启动:
专门有一个从命令行启动的入口:loganalyzer.core.AnalyseLogTaskLauncher
java -cp ./lib/ loganalyzer.core.AnalyseLogTaskLauncher -log ./log4j.xml -task /data/tasks/test_task.yaml
有两个命令行参数,-log参数指定log4j配置文件的位置,-task指定要运行的task配置,后面可以跟task配置中cmdOptions所指定的项。
2. 在Java代码中调用:
也可以在其它的Java程序中启动一个日志分析任务:
AnalyzerLogTaskExecutor executor = BasicAnalyzerLogTaskExecutor.getInstance(); AnalyzeTaskConfig taskConfig = AnalyzeTaskConfig.loadFromConfigFile("/data/test_task.yaml"); AnalyzerContext context = new AnalyzerContext(); context.addParam(taskConfig.getInitParams()); executor.execute(taskConfig, context);
AnalyzerLogTaskExecutor是一个封装日志分析过程的门面,把需要的参数传给它,执行execute方法,就可以完成日志分析任务。