首页 > 编程 > Effective Debug Logging (part 1)

Effective Debug Logging (part 1)

有感于当时在学校的时候做项目和进入公司之后参与产品开发的经历,深感产品支持的难度——客户的环境千差万别,因此可能出现的问题也千差万别,而且很多问题只有在客户的环境当中才可能重现,这个时候,如何快速的定位问题并给出解决方案,是一个非常重要的问题。

通常我们采用的都是调试日志,也就是Debug Logging的方法。在正常的情况下,调试日志并不会被打印出来,只有当出现问题时,通过适当的方式(配置文件或者命令行)将其打开,在客户环境当中重现,并将调试日志收集到开发者处分析。因此,如何有效地在产品代码当中添加调试日志,是影响了软件维护效率的一个重要因素。

这几篇文章主要关注的就是如何有效的写调试日志以便于分析。因为我所从事的项目主要使用C/C++进行开发,因此文章当中的介绍也主要集中在如何在C/C++的大型程序当中撰写调试日志。

文章主要关注在以下几点问题:

  • 调试日志的目的:确定调试日志设计的目标
  • 输出调试日志的方式:调试日志应该输出到哪里
  • 调试日志的分级和分类:提高日志的信噪比
  • 如何写调试日志:应该在什么地方写什么
  • 如何分析调试日志:精力应该放在刀刃上

这是第一部分,我们主要关注在前两个主题:调试日志的目的和输出调试日志的方式。

调试日志的目的和设计目标

调试日志的根本目的是为了定位错误。在这个目的下,我们才会进行更加细致的调试日志的设计。

说起定位错误,首先是这种行为的主体:谁来定位错误?首先当然是用户。我们希望,在大多数情况下,用户可以在出现错误时通过对于调试日志的简单分析即可定位错误。但是这种方法很容易让人将调试日志的作用高估:程序员希望用户会去看调试日志。事实上,调试日志对于用户的排错工作并不应作为一线工具,至少不应该用来报告系统的错误;对于系统的错误,例如用户没有给与正确的输入,或配置文件解析错误,这种需要用户去做一些行为才能让系统工作起来的消息,应该用更加直接的方式通知用户。例如在Windows上面,使用Windows事件日志(Event Log)就是一种很好的选择。但是,这条消息仍然应该记录进调试日志,以便可以记录下错误发生的细节以便于程序员进行分析。

另外一个需要分析调试日志来定位错误的就是软件维护人员。因为软件的规模问题,或者由于流程的不同,也许软件维护人员并不是本来的系统开发人员,而无论如何,对于系统的工作方式,软件维护人员都比用户了解的更多。因此如果调试日志足够好,他们可以通过调试日志,结合对于软件工作方式的理解发现更多的问题,并给出用户解决方案。因此,将调试日志写得更加易懂,说明软件“做了什么,发生了什么结果”,而不是“调用了什么函数,返回了什么值”,就能够让更加前端的软件维护人员的处理掉更多的问题,相应的,需要开发人员亲自解决的问题的数量就会减少。

最后的会去分析日志的就是软件开发人员。因此,添加必要的开发人员理解的信息也是必要的,比如函数调用参数和返回值的跟踪,条件分支的跟踪,出现错误的代码行和源文件等等,以便掌握有源代码的开发人员可以对错误的流程进行跟踪并重现。同时,由于到了开发人员手中的往往都是非常棘手的问题,往往伴随的大量的调试日志条目,因此,让调试日志的分析更容易自动化对于开发人员定位错误的帮助也是非常大的。在建立了良好的自动的调试日志分析工具之后,对于已知的问题,就可以通过工具进行快速的定位了。

综上,我们所做得对于调试日志的工作都是为了能够更容易、更早的定位错误。为了达到这个目的,我们需要能够让调试日志便于查找和搜集,便于人读取和理解,以及便于工具进行解析。

Tip 1:调试日志的设计应该以“便于定位错误”为中心目标

第一个问题就是需要调试日志便于搜集,也就是调试日志的输出问题。

输出调试日志的方式

对于调试日志应该输出到哪里,人们有很多不同的看法,在不同的环境当中也各有优劣。比较流行的几种观点是:

  1. 输出到标准输出(stdout)或标准错误(stderr)。这种一般出现在交互式命令行工具当中,使用stdio或iostream当中的库函数来书写。这种书写方式符合程序员的习惯,例如Unix的传统的工具通常使用-v(如mplayer),-debug(如openssl)或其他的选项来把细致的日志输出到stdout和stderr。这种输出的优势在于便于重定向到文件或管道。重定向的文件比较适合搜集调试日志,而使用管道比较适合实时分析。其劣势也是非常明显的:这种方式仅适用于交互式命令行工具,对于GUI程序、后台守护进程或者服务程序就不太适合了。当然对于Windows GUI程序,有一种方式可以使用CRT Output Routing,但是并不通用,而且会失去使用这种方式的根本优势,即灵活的重定向。
  2. 输出到System Debug Buffer。即通过OutputDebugStringW/A函数,或者内核态的DbgPrint(Ex)函数。这种方式并不见得有多少优势,尤其是必须通过调试器或其他工具才可以看到或dump到文件,给在用户处搜集信息造成了很大的麻烦。在我看来,使用这种方式很多时候还是不得已而为之,例如在内核态除了DbgPrint(Ex),似乎没有别的选择。但是使用调试器来搜集一些信息还是一种可以选择的方案,例如对于Windows API调用的记录,skywing在他的blog上面提供了一种在WinDbg当中可用的方法,但是仍然如我所说,在实际的用户那里,并没有太大的意义。
  3. 输出到日志文件。这种是使用的最多的方案。这种方式可以适用于除了交互式命令行工具之外的所有组件,而且非常适合用来从用户出现问题的环境当中直接收集,并由程序员进行分析。但是这种方案可能存在一些细节问题需要考虑,例如将日志文件写在哪里(临时文件目录,专用的调试日志目录,或者其他地方),文件如何命名和切分,如果调试日志量过大如何处理,写调试日志时出现错误怎么办等等。这几个问题没有固定的答案,关键在于一旦确定下来,就需要在整个软件当中保持一致。在我现在从事的项目中,我们使用的是写入到专用的调试日志目录当中,同时这个目录可以由用户通过配置文件修改;文件名使用basename.yyyymmdd.####的格式,yyyymmdd是用户本地时间的年月日,####是从1开始的编号。每个文件大小不超过某个值,一旦超过就另开一个文件。这种结构便于辅助工具寻找和收集调试日志并打包,以及在调试日志过多的时候由一个守护程序进行清理。对于写入日志的错误,建议根据当前的日志级别来处理,如果需要打出的本身就是一个严重错误的日志,最好使用某种不太容易失败的方式将日志写入其他地方,例如系统事件日志;而如果写入的只是一些信息性的或者调试
    性的消息,则可以选择忽略。

Tip 2:选用合适的日志输出方式,以便于收集和处理为目标设计。用久经考验的方案,输出到stdout/stderr或具有统一文件名格式的日志文件。

如果没有特殊的需求,对于交互式命令行工具使用直接输出到stdout/stderr,对于其他直接输出到日志文件,是调试日志输出的首选。我们当前使用的文件组织方式也可以借鉴。接下来我们就开始谈论如何能够有效的书写调试日志的内容。

标签:
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.