最近我们看到各种各样新的工具,能够帮助你搞定日志。开源的项目如Scribe和LogStash,在线的工具如Splunk,托管的服务如Sumologic和PaperTrail。这些工具可以帮你减少大量日志数据。
但是有一个东西它们都无法帮到你,它们都依赖你实际放入日志中的数据。获得更多、更高质量数据的任务就落在你身上了。所以,在关键时刻你需要调试部分代码和丢失的日志数据,你可能要取消晚饭了。
为了减少以上情况发生的次数,我要给你分享5件事情,当你在生产环境使用日志的时候你必须紧记在心:
1. 你好,我(线程)的名字是
和Ringo一样,线程的名称是java中最被低估的方法之一。原因是它主要是描述性的属性。那又怎么样呢?我们让它们的名称变得有意义。
线程的名称在多线程日志记录中扮演重要角色。许多日志框架会记录当前调用日志记录的线程名称。可悲的是,它们大部分类似 “http-nio-8080-exec-3″,这个是线程池或者容器给它们取的。
出于某种原因,我不止一次听到对线程名称是不可变的误解。它们是可变的。线程名称是你日志中主要标记,你必须确保正确地使用它。这就意味着给线程取的名称必须结合上下文,例如servlet的名称或者任务此刻的意义,还有一些动态的上下文环境,例如一个用户或消息ID。
因此,代码的入口处应该像下面这样:
Thread.currentThread().setName(ProcessTask.class.getName() + “: “+ message.getID);
一个更高级的写法是加载一个线程本地变量到当前线程中,并且配置一个自定义日志(appender),自动把这个变量加入到每一条日志记录中。
当多线程写服务器日志时,并且你需要关注某一个线程的时候,这是非常有用的。如果你的程序是分布式/面向服务的环境,你等会看到它的另一个好处。
2. 分布式标示
在面向服务或者消息驱动的架构中,一个任务的执行很可能要跨越多个机器。当这样的任务执行失败的时候,机器之间的连接点和它们的状态是搞明白到底发生了什么的关键。很多日志分析工具可以帮你进行日志归类,前提是你日志中带有它们可以用来做分类的唯一ID。(校对注:当A应用调用B应用接口时,而B应用的接口实现又需要调用应用C的接口时,一旦报错很难定位这个请求到底是在调用哪个应用时报错的?所以就使用一个唯一ID把这个请求链路串起来。)
从设计的角度来看,这意味着每一个操作进入到你的系统中都应该有一个唯一的ID,用这个ID直到它执行完成。注意,那些持久化标示符,例如用户的ID在这里可能不是很好的选择,因为一个用户可能有多个操作发生在同一个日志中,这会使得隔离出一个特定的操作流变得更难。UUID在这边是一个很好的选择,这个值可以被作为线程的名称或者作为一个TLS-线程本地存储。
3. 不要在循环中记录日志
你经常会看到一小段代码运行在一个紧凑的循环中,并且执行一个日志操作。潜在的假设是这段代码运行的次数是有限的。
当一切执行顺利的话这样写是可以的。但是当这段代码获得一个意外的输入,循环可能无法跳出,在那种情况下你处理的不仅是一个无限循环(那已经够糟了),你正在处理的代码是无限地写入大量数据到磁盘中或者网络上。
任由其一直写日志,这会使得服务器宕机,在分布式环境中,一整个集群都会挂了。所以可能的话,不要在循环中记录日志。尤其是在捕获错误的时候。让我们来看一个例子,我们在一个while循环中记录异常日志:
void read() { while (hasNext()) { try { readData(); } catch {Exception e) { // this isn’t recommend logger.error(“error reading data“, e); } } }
如果readData抛异常,并且hasNext方法返回true,最终我们无限记录日志。解决它的一个方法是确保我们不记录所有东西:
void read() { int exceptionsThrown = 0; while (hasNext()) { try { readData(); } catch {Exception e) { if (exceptionsThrown < THRESHOLD) { logger.error(“error reading data", e); exceptionsThrown++; } else { // Now the error won’t choke the system. } } } }
另一个方式是把日志从循环中完全移除,并且保存第一个或最后一个异常对象到其他地方记录。
4. 未捕获异常处理者
维斯特洛有绝境长城作为最后防线,你有 Thread.uncaughtExceptionHandler。所以,一定要确保你有使用它们。如果你没有设置这些处理器,你有可能会把异常抛到“野外”,如果发生这样的情况,很难控制日志记录下它们来。
找出你代码中曾经出现过大量错误,并且未被记录的,或者有关它们的记录是少量非状态日志,这是一个极大的错误。
注意,即使有未捕获异常处理器,从表面上看,你不能获得任何抛出异常线程(线程已经终止)中的变量,即便你可以获得线程对象的引用。如果你坚持第一步(给线程命名),你仍然可以通过调用 thread.getName() 方法记录一个有意义的值。
5. 捕获外部调用
无论什么时候你调用一个JDK之外的API,产生异常的几率都会大大增加。包括web service,HTTP,DB,文件系统和其他JNI调用。对待每一个调用都应该认为它会产生异常(很有可能在某一个点会抛异常)。
很多情况下,外部API调用失败的原因是提供的入参是未预知的。把那些输入参数现成的记录在日志中是你修复代码的关键。
这个点上你可能会选择不记录错误日志,但是必须记录抛出的异常,这是对的。在这种情况下,只需要尽可能多的收集传递给调用的相关参数,并且把他们格式化到异常错误信息中。
只需要却表异常被捕获,并且和调用栈一起被高日志级别记录。
try { return s3client.generatePresignedUrl(request); } catch (Exception e) { String err = String.format(“Error generating request: %s bucket: %s key: %s. method: %s", request, bucket, path, method); log.error(err, e); //you can also throw a nested exception here with err instead. }