使用Spring跟踪应用异常(1)
几周以前,一个同事请我帮他一周的忙。他想要享受一个难得的假期,但是找不到其他人帮他。由于我刚完成一个特别复杂的编码工作,需要来点自我调节,所以就答应了他的请求。换换环境对我也是非常有益的。
那份工作的一部分是监控一些非常关键的后台运行进程,观察它们是否运行正常。
开发者花了很多时间和精力在应用中加入日志输出,以此表明应用正常运行。当异常发生时,可以知道究竟出现了什么错误。这些日志文件通常可以说明我们的应用每天运行究竟是正常的还是异常的。
在这里,我将忽略其它技术。比如为应用增加类似HTTP或JMX监视器。这些技术提供的是应用程序即时信息,而不是本文讨论的二级监视结果。
监控日志文件通常有三种方式:
- 从不检查;
- 被动检查;
- 主动检查。
从不检查意味着那些应用一直运行从不停止,而且也不需要检查。这一点我敢打赌。
被动检查是很常见的。举个例子,Smith太太打电话投诉当她想要买一双新鞋时,网站就瘫痪了。她已经支付了两次,但是从没收到过鞋子。在这个传统的公司中,开发者和运维工程师是被完全隔离的。必须从运维工程师获得该问题发生时的日志,开发者才能开始分析。但是从运维那里返回的往往是一些完全不相关的日志,接下来开发者不得不再次向他们索要日志,甚至如此反复多次。这个过程耗费了几周时间,Smith太太变得怒不可遏。最后,日志终于到了开发者手中,问题解决。
可能很多公司都像上面“被动检查”的场景中描述的那样,开发者不被信任或允许操作在线服务器。这种情况司空见惯,我们中的绝大部分人都遇到过这种情况。我们应当信任开发者去操作在线系统。然而,作为一个开发者,在操作在线系统之前,必须记住两条黄金定律:
- 不要停止任何东西;
- 如果停止某些东西,请确保有人会在身边提醒你。
“主动检查”意味着检查日志文件要成为一个定期任务:每天、每小时或其它间隔定期执行。即使你的应用包含了大量的JMX、http或其他监视器,也不能确保一定会能发现每个问题。监视器只能发现你设定问题,除此之外的其他任何问题都不会报告。
再回到要我帮忙的问题,可能是历史的或其他原因,通过手动检查日志文件来监控系统运行,通常要对一些文件进行剪切或粘贴操作。这些重复的剪切和粘贴操作,每周大概会占用一个人半天的工作时间。
我非常不喜欢这些工作。我不善于这些需要手动且易出错的重复无聊的工作。每周要消耗半天人力,所以将这项任务自动化显然更符合成本效益。只要时间不是花在追求完美的解决方案上。那么,有什么选择呢?
如果你看了日志的规模,你一定会喜欢上Splunk,它能够监控多个消息源(譬如系统的日志守护进程)的消息。
这就意味着向Splunk发送错误只是简单地建立一个Log4j的系统日志附加器,不过那不是这篇博文的范畴…
最后,你需要写一些shell脚本来完成一些类似于“grep”命令的功能,将结果写入文件,并将这些文件通过邮件发送给你。
开发一个基于Spring的app有多难?它需要包含一些尽可能通用的、可重用的类,能够定期检查错误日志文件并将结果通过邮件发送给你。当然,结果还是通过邮件发送比较好,因为你总是会习惯性地查看你的邮箱。
开始
在其他类似的工程当中,都面临着如何迈出第一步的问题。要想让这个工程发布,需要写哪些类。有很多方法来确定你需要写哪些类,譬如简单地凭直觉,或者利用类似UML的设计工具,或者用快速原型或 测试驱动开发。在每种情况下,你真正要做的就是让类名体现的功能满足一些要求。例如,在这里我需要:
- 搜索一个给定的路径和子路径,找出某种特定类型的文件。
- 检查找到文件的时间,判断是否有必要在这个文件中搜索错误。
- 时间满足要求,则在其中查找异常。
- 找到异常,则判断是否是我们关注的,还是需要忽略它。
- 如果异常是我们关注的,则将其加到报告中。
- 检查完所有文件后,形成待发布报告。
- 通过邮件或其他方式发布报告。
- 以上所有操作每天定时执行。
这些操作就形成了几个类名:FileLocator、FileValidator、RegexValidator、FileAgeValidator和Report。
上述几个类名中包含了多次的“Validator”,意味着我们可以用一个接口,叫做“Validator”。利用接口的几个实现来完成上面的验证工作。这些实现可以结合起来组成一个应用。
这只是一个初步的想法。如果你看了代码,会发现没有一个类名是Report,只有一个Results类和 重构的Formatter接口、TextFormatter和HtmlFormatter类、Publisher接口和EmailPublisher类。
为了让任务定期执行,有多种选择。首先,需要将java代码和调用它的脚本放在一起,然后放在Unix机器上执行。但是这就意味着该应用不能在Windows上运行,并且只能作为一个独立的应用。这是个大问题,所以我们可以利用Spring和Quartz调度,使得建立一个调度任务非常简单。
而且Spring提供了一个非常好的Java邮件类和Email模板,这对我们的报告邮件非常有用。
这些只是开始,关于类设计一些模糊的想法通过一种松耦合的方式连接在一起组成了我们的应用。如果在正式的工作中,你可能需要花时间把这些整理成文档,甚至需要画出类图,加在一个Doc文档中,然后多次复查直到都不在改变。但是,我不需要去管这些…
配置应用
和其他应用一样,我们需要指出系统建立必须的属性值以及它们是如何被使用的。这个应用由app.properties文件配置,路径为src/main/resources。
# The path to the log file directory to scan for errors scan.in=/Library/Tomcat/logs # A regex defining what to look for - or what not to include scan.for=^.*Exception.* exclude=^.*IllegalStateException.* # The number of following lines to add to the report following.lines=10 # Where to email the report [email protected] # The max age of a file in days max.days=1000
第一个我们感兴趣的属性是scan.in,它是Web服务器的日志路径,作为类FileLocator的输入参数。
搜索文件
在编写FileLocator类时,我在要求的范围之外做了一点提升。
提升真的不是一个好主意。你应该为了满足功能要求而编写代码,那是为了完成工作。
下面的代码不只是搜索指定路径下的log文件,还搜索所有的子目录。
@Service public class FileLocator { private static final Logger logger = LoggerFactory.getLogger(FileLocator.class); @Value("${scan.in}") private String scanIn; @Autowired @Qualifier("fileValidator") private Validator validator; /** Search for the files requested */ public void findFile() { logger.info("Searching in... {}", scanIn); File file = createFile(scanIn); search(file); } @VisibleForTesting File createFile(String name) { return new File(name); } private void search(File file) { if (file.isDirectory()) { logger.debug("Searching directory: {}", file.getName()); File[] files = file.listFiles(); searchFiles(files); } else { logger.debug("Validating file: {}", file.getName()); validator.validate(file); } } private void searchFiles(File[] files) { for (File file : files) { search(file); } } }
上面的代码用比较传统的递归方法来搜索日志文件,主要的入口函数是findFile()。它利用Spring标记@Value标记的scanIn实例变量创建一个File对象,并把其传给search()方法,由该方法检查这个File对象是不是一个目录。如果是目录,则对该目录下的每一个File对象循环调用search()。如果File对象是一个文件,则用文件验证类来处理。
到目前为止,利用应用的第一个类,我们能搜索一个指定的日志文件路径,找到该路径下的日志文件。如果你想知道搜索到文件后做了什么,就需要等待我的下一篇博文。
最后一个思考:是否需要关注系统中的每一个错误?有一个古老的哲学寓言:如果森林里的一棵树到了,但是没有其他任何一棵树听见,还能认为它发出了声音吗?同样道理,如果你的应用抛出了一个异常,但是用户没有受到影响,这还是一个错误吗?是否还需要花时间研究它?
以上代码请见Github: https://github.com/roghughe/captaindebug/tree/master/error-track