Search This Blog

2013-05-01

Java Logging使用经验

目录  [+]
下面未指明版本的log4j都是指1.2版本,源码是apache-log4j-1.2.16-src。log4j-1.2相关的配置方法看说log4j 1.2.x

1 logging框架分类

)具体的实现
    比如,JUL (java.util.logging), Log4j, Logback
)代理,bridge / facade
    比如,Apache Commons Logging, SLF4J
    只需要告诉这些框架具体的实现是哪个。Commons Logging:在classpath放配置好的commons-logging.properties;SLF4J:在classpath放官网打包好的binding jar包,比如slf4j-log4j12-1.7.5.jar,SLF4J会搜索"org/slf4j/impl/StaticLoggerBinder.class"并完成绑定。
相关链接:
Log4j 1.2 - logging.apache.org
Logback - logback.qos.ch
Commons Logging - commons.apache.org
SLF4J - www.slf4j.org

2 经验总结

2.1 输出详细的日志

对服务器端程序来说,尽量多输出日志,尽量输出详细的日志,最好记录所有请求以及响应过程,便于发现并解决问题。不仅对服务器端有帮助,还对客户端有帮助(便于发现客户端的问题,也可以帮助客户端确定问题)。我在实际项目里就是这么用的,确实很有用。顺便提一下另外几个重要方法:防御式编程、bug自动报告、服务器状态监控。

2.2 日志级别

)测试阶段,Level全部使用DEBUG,没问题的依赖库可以换成WARN;上线以后也可以保持在DEBUG;便于发现问题。
)FATAL级别的日志需要另外记录一份,便于查看,最好配置成发送邮件。log4j可以做到。

2.3 日志格式

尽量记录格式更清晰的日志,包括已有的日志以及格式扩展以后的日志,格式最好比较固定,因为以后可能需要解析日志用来做统计分析。

2.4 线程安全

对Java应用来说,这是必须的。JUL, log4j, logback都是使用synchronized来提供线程安全。

2.5 缓存化输出

)JUL每次log都是直接输出,对性能会有影响。log4j, logback可以使用输出缓存,自定义输出缓存大小,指的是BufferedWriter之类的bufferSize。

)如果logging库开启了输出缓存,那程序退出的时候需要清空缓存的日志,否则会有部分日志丢失。可以在程序退出触发器里面清空缓存的日志来解决。
))程序退出触发器:JavaSE应用可以使用Runtime.getRuntime().addShutdownHook(Thread hook);JavaEE应用可以使用javax.servlet.ServletContextListener.contextDestroyed(ServletContextEvent sce),注册到web.xml。
))强制输出缓存的日志:log4j 1.2可以使用org.apache.log4j.LogManager.shutdown()。

)另外一种缓存:log4j, logback都可以使用AsyncAppender,日志可以一条一条缓存起来,log4j用的是ArrayList+synchronized,logback用的是BlockingQueue。

2.6 协调logging库

)自己开发的项目可以直接使用某种logging实现,比如log4j。为依赖的库提供它们需要的logging配置文件。
)如果项目依赖的库直接使用了不同的logging实现(比如,一个库直接使用log4j,另一个库直接使用logback),并且全部日志都想要输出,那需要注意并发输出问题。如果是输出到同一个文件,那就有问题,可以配置成输出到不同的文件,或者相同的数据库/socket/web server。

2.7 在运行状态调节日志配置

这样就可以实现一个功能:在管理员控制台调节日志配置,提交之后即可生效。
比较常用的是在需要输出更多日志用来调试的时候修改Level。log4j可以做到,其它框架没试过。

2.8 扩展logging框架

一般都可以扩展。log4j可定制性很强。

2.9 性能

)为了提高性能,需要给logging库开启输出缓存。

)避免无谓的字符串拼接造成的性能损失。比如,有很多debug跟trace,但是没有开启相应的日志级别。有几种解决方法:
(1)使用支持字符串延迟初始化的库
SLF4J对不支持这个功能的底层库提供了相应实现,比如,org.slf4j.impl.Log4jLoggerAdapter.debug(String format, Object... arguments)。SLF4J的格式化功能比String.format弱一些,但是性能更好。
logback也支持。

(2)对于不支持字符串延迟初始化的库,比如log4j,或者想使用String.format,可以在每次调用之前都判断isLevelEnabled。写代码的时候不需要加,以后用正则统一搜索替换一下就行。

2.10 重定向控制台输出

)有些库会使用System.out/System.err/throwable.printStackTrace()来输出数据。
尽早在类的静态代码块里面,通过System.setOut(PrintStream out)、System.setErr(PrintStream err)重定向System.out、System.err。
需要重定向到一个单独的文件或者某server,最好扩展一下PrintStream,输出当前线程、时间之类的数据。

)在Unix/Linux上,Tomcat会把控制台输出重定向到Catalina.out,这种情况就不用自己重定向了。

2.11 资源释放

有些logging框架提供资源释放接口。比如,org.apache.commons.logging.impl.ServletContextCleaner实现了ServletContextListener接口,可以配置到web.xml。

2.12 经历过的3个例子

)未记录非法请求
刚工作的时候犯的错,由于日志不全,我无法判断问题出在哪,后来是给客户提供加了日志的版本。拿到日志之后,解决问题就简单了。
我做的是一个插件,系统处理了用户输入的数据,给插件提供的是没处理过的数据,我不了解情况,直接就用了。后来有人报告过这样的问题:Openfire supports only lower-case logins / usernames - community.igniterealtime.org
简单来说,总会有自己无法预料的情况,哪怕是浪费,也得记录所有非法请求,需要包括具体的请求参数(敏感数据除外)。

)ServletContextListener实现类被调用两次
webapp1放在了tomcat/webapps/目录,并且在server.xml配置了Context docBase。
ServletContextListener实现类会初始化一些重要资源,正好记录了日志,结果发现初始化方法被调用两次,我预想初始化方法被调用一次。
被调用两次的原因是有两个地址可以访问这个webapp1,而且各自拥有独立资源(我以前不这么认为,我以为只是在url那里处理一下,内部共用一套资源)。
后来把webapp1从tomcat/webapps/目录移走,docBase配置成绝对路径,这样就只有一个地址可以访问了,也只有一套资源。把webapp1放到tomcat/webapps/ROOT/目录也能解决问题,但我觉得这不是正确的方法。
尽量在关键代码路径记录日志,总会有自己无法预料的情况。

)重大bug诊断
一个DAO类的某方法里面,SQL语句的LIKE子语句可能包括动态设置进去的'_','_'在LIKE子语句里面可以匹配任意一个字符,这不是想要的结果。
还好所有的SQL语句都有执行结果日志,包括参数填充回去之后的SQL(不包括敏感参数)、耗时、查询语句有没有结果、修改语句影响了多少条记录。客户端发现异常情况之后,根据大概的时间找到相应日志,马上就发现了问题,这是内部测试。
这是个严重问题,顺便提一下产生的原因。当时写代码的时候很着急,糊涂了,写完没检查没测试;这条语句本来就可能删除多条记录,所以没法加断言;后来的代码审查没注意到这里。

=文章版本=

2013040x
20130501 修改一下格式,顺便增减一部分内容
20130506 改为html格式

No comments:

Post a Comment