使用有意义的调试信息查找顽固错误

作者:John Goerzen

错误跟踪通常是软件开发中最困难的过程之一。用户的情况可能与开发者不同,对用户来说是重大问题的错误,在开发者的机器上甚至可能看不到。有时错误可能会时有时无,或者联网程序可能只在与特定服务器或客户端通信时才会遇到错误。在本文中,我将讨论软件开发者可以采用的技术,以帮助更轻松地跟踪错误。

首先,我将讨论两种方法,使接收和管理错误更容易,然后我将展示如何使您的程序生成更有用的调试输出。然后,我将讨论如何跟踪麻烦的错误。最后,我将介绍一些可以帮助从一开始就防止错误的做法。本文中描述的许多技术都应用于 OfflineIMAP(请参阅《旅行快速便捷邮件:OfflineIMAP》,《LJ》,2004 年 3 月)。

跟踪错误

在检查如何使更好的错误报告成为可能之前,关键的第一步是确保您可以处理收到的错误报告。对于一些小型项目,简单地发布一个电子邮件地址就足够了。然而,大多数项目需要更多。开发者经常会忙碌起来而忘记事情。错误可能很难解决,需要来自多人的输入,或者仅仅可能有大量的错误报告。

错误跟踪系统 (BTS) 是帮助确保错误不会被遗忘的好方法。大多数 BTS 实现都提供了一种方法来跟踪通信记录、处理附加文件并将责任委派给特定人员。有些还支持基于严重性、用户环境和特定组件等因素进行分类。

如果您的项目托管在 SourceForge 或 Savannah 等项目托管站点,您已经可以使用 BTS。您应该使用它,并鼓励您的用户通过该界面而不是邮件列表提交错误。

如果您需要更大的灵活性,您可以找到适用于 Linux 的 BTS 程序。一些最流行的自由软件 BTS 程序是

  • Bugzilla,Mozilla 项目使用的 BTS,是一个主要通过其 Web 界面使用的灵活系统。

  • Request Tracker 既可以用作错误跟踪系统,也可以用作支持跟踪系统。它具有 Web 和电子邮件界面,尽管某些管理功能只能通过 Web 界面进行。

  • Jitterbug 是 Samba 项目使用的 BTS。它在概念上类似于 Bugzilla,但更轻量级。

  • Debbugs 是 Debian 项目使用的 BTS。Debbugs 有一个 Web 界面,但它是只读的;所有操作都通过电子邮件进行。Debbugs 最适合具有明确可识别的组件以及这些组件责任的大型项目。

我个人更喜欢 Request Tracker,因为它似乎具有 BTS 功能的良好组合。您自己的要求可能会有所不同。

使提交错误变得容易

有时我在程序中发现一个讨厌的错误并想报告它。但要做到这一点,我必须填写一份详细的问卷,并可能泄露我宁愿不透露的信息。人们应该很容易提交错误以及跟踪错误所需的信息。如果您在 Web 上接受提交,请使过程简单。不要要求太多信息,即使人们不知道某些信息,也要接受提交。不要期望用户了解项目的不同组件或哪些开发者负责给定的问题。

日志记录

在跟踪问题时,您通常想知道程序处于什么状态。有时,您可能想知道在触发错误之前执行了哪些操作。由于程序的用户不一定具备您的代码和调试器的专业知识,因此通常需要日志记录。日志记录只是意味着写出已执行操作的记录。简单的程序可能只是打印出信息,但通常您会想要一些功能更强大的东西。

非交互式程序(例如网络服务器)没有屏幕来显示信息。这些程序通常维护日志文件或使用内置于 Linux 和 UNIX 系统的 syslog 工具。

交互式程序可能会在屏幕上显示信息,或者也可能生成文件。拥有日志文件可以使错误报告更容易,因为用户只需将其附加到错误报告即可。

有时,您可能需要相当多的数据才能弄清楚特定问题的情况。但是,所有这些数据对于正常会话来说可能太过分了——它可能会从用户的屏幕上流走或填满硬盘。因此,许多程序都有日志级别的概念。用户可以在运行时设置应记录多少信息。有些程序甚至可能有日志类别,用户可以在其中配置记录哪些类型的信息。OfflineIMAP 使用这种方法。对于麻烦的问题,用户可以打开通信日志,该日志记录发送到 IMAP 服务器或从 IMAP 服务器接收的所有数据。

Python 2.3 引入了一个名为 logging 的有用模块。logging 模块为多种不同的消息记录方式提供统一的接口。它支持的日志记录方法包括将消息写入文件、网络服务、syslog、电子邮件消息和其他几种。以下是一个简单的示例,说明了 logging 模块的用法

#!/usr/bin/env python
import logging, sys

# Create the logger object
l = logging.getLogger('testlog')

# Create a handler and assign it to the object
handler = logging.StreamHandler(sys.stderr)
l.addHandler(handler)

# Levels are DEBUG, INFO, WARNING, ERROR, CRITICAL.
# Set the default level here. Any log messages
# beneath that level are dropped.
l.setLevel(logging.INFO)

# Try it out.

l.debug("Debug message -- system initialized.")
l.info("Here's some info.  I've just debugged.")
l.warning("I don't have many messages left.")
l.error("Only one more message to go.")
l.critical("Nothing else to do!")

该程序首先初始化记录器。它使用 StreamHandler 将记录的文本写入标准错误。它还将日志级别设置为 INFO。然后它记录五个消息。当您运行此程序时,您只会看到最后四个。调试消息已被设置为 INFO 的级别过滤掉。许多程序都有一个配置或命令行选项来在运行时设置级别。您只需向 Logger 对象添加不同的处理程序即可使用不同的日志记录方法。Python 文档中有所有可用处理程序的参考。

检查输入

确保您收到的输入是有效的。例如,如果您期望在命令行上输入某些内容,请检查以确保您在尝试使用它们之前具有适当数量的参数(或捕获生成的异常)。这为用户提供了更好的错误消息。这是一个演示此操作的 Python 示例程序

#!/usr/bin/env python
import sys

try:
    print "You supplied: %s" % sys.argv[1]
except IndexError:
    print "You forgot an argument."
处理异常

几种编程语言(例如 Java、Python 和 OCaml)都包含对异常的支持。使用异常,您可以在您选择的位置捕获错误,而不必检查和处理每个可能产生问题的调用中的错误。有时,让异常未处理可能是正确的,但通常情况并非如此。应该捕获和处理异常。虽然如果您无法打开用户要求的文件而终止程序可能是合适的,但最好还是使用错误消息给出文件名和问题来执行此操作,而不是让用户收到难看的异常消息。

捕获异常

对于对您的程序来说确实是致命的异常,您仍然可能想要捕获它们。例如,这将允许您将它们记录到文件中或在 GUI 应用程序的弹出框中显示异常。这使得用户更容易将堆栈跟踪发送回给您。您还可以使用通用异常捕获器来执行其他活动,或许可以输出各种缓冲区的内容,以帮助您弄清楚当时发生了什么。

以下是一个示例,它记录任何异常以及有关当前正在运行的程序的一些信息。然后它重新引发异常并退出

#!/usr/bin/env python
import logging, sys, StringIO, traceback, os

l = logging.getLogger('testlog')

handler = logging.StreamHandler(sys.stderr)
l.addHandler(handler)
formatter = logging.Formatter("LOG: %(message)s")
handler.setFormatter(formatter)

l.setLevel(logging.INFO)

def logexception():
    sbuf = StringIO.StringIO()
    traceback.print_exc(file = sbuf)
    excval = sbuf.getvalue()
    l.critical(" *** Exception Detected ***")
    l.critical("Current PID: %d" % os.getpid())
    l.critical("Program name: %s" % sys.argv[0])
    l.critical("Command line: %s" % \
               str(sys.argv[1:]))
    for line in excval.split("\n"):
        l.critical(line)

def main():
    print "Hello, I'm running."
    raise RuntimeError("Oops! I've had a problem!")

try:
    main()
except:
    logexception()
    raise

当您运行此程序时,您应该在屏幕上看到如下内容

Hello, I'm running.
LOG:  *** Exception Detected ***
LOG: Current PID: 28441
LOG: Program name: /tmp/logerror.py
LOG: Command line: []
LOG: Traceback (most recent call last):
LOG:   File "/tmp/logerror.py", line 30, in ?
LOG:     main()
LOG:   File "/tmp/logerror.py", line 27, in main
LOG:     raise RuntimeError("Oops! I've had a problem!")
LOG: RuntimeError: Oops! I've had a problem!
LOG:

在这里,异常处理程序找到了异常,获取了有关它的信息,并能够记录它。您还可以第二次看到回溯。程序末尾的 raise 语句导致异常被引发并在正常情况下也被处理。这意味着它会中止您的程序并显示回溯。根据您的要求,您可以选择使用 sys.exit() 来终止。

查找报告的错误

既然您已经有一些方法可以帮助用户提交良好的错误报告,那么让我们看看如何使用这些错误报告来跟踪问题。有了日志和可能的回溯信息,这里有一些要问自己的问题

  • 我可以在我的环境中重现该错误吗?如果您可以在自己的机器上重现问题,那么您离轻松解决问题已经很近了。使用调试器或其他工具来跟踪它,现在您可以随意触发它了。

  • 输入和输出是否符合我的预期?也许用户提供了您在编写程序时没有考虑到的值。或者,也许网络客户端或服务器对协议的处理方式与您的预期略有不同。也许输入或输出本身是格式错误的,而错误甚至不在您的程序中。显示所有 I/O 的调试日志在这里非常有用。

  • 程序流程是否符合预期?如果您的日志调用了各种函数或方法,您应该能够跟踪程序中执行的流程。也许某些条件导致跳过重要的代码,从而导致以后的问题。

  • 最后一个正确的执行点在哪里?这可能是在错误发生之前,或者可能在崩溃之前,不正确的数据已经传递了一段时间。查明程序历史记录中最近一次正常运行的时间可以帮助跟踪问题发生的准确位置。

  • 如果手头有回溯,堆栈看起来正常吗?检查以确保函数调用符合预期,并且传递给它们的数据看起来是合法的。

预防错误

我在本文中描述的所有技术都很有用,但不应在真空中部署它们。采用有助于降低错误发生可能性的做法也很重要。以下是一些需要考虑的事项

  • 采用单元测试。Java、Python、OCaml、Perl 和 C 都提供了单元测试框架。使用它们并尽可能多地执行代码路径。对于像 Python 这样的语言来说,这一点尤其重要,在 Python 中,程序的某些执行甚至可能无法解析您的所有代码。对于 Java 来说,这一点也很重要;例如,由于不正确的对象类型转换,可能会发生运行时异常。

  • 避免全局变量。避免全局(或类全局)变量有助于隔离问题,并有助于防止多线程程序中的同步问题。全局变量可能是函数调用中意外副作用的来源,这很难跟踪。

  • 为工作选择合适的工具。每种语言都有其自身的优势和劣势,没有一种语言是完成每项任务的最佳工具。例如,虽然 Perl 使使用正则表达式解析分隔的文本文件变得容易,但 OCaml 提供了专门为编写编译器而设计的工具。在一种语言中容易表达的问题在另一种语言中可能变得更加难以表达。

  • 但是,不要使用太多不同的工具。大多数项目都受益于标准化的工具集。选择最适合手头项目的语言和库,除非有令人信服的理由,否则不要引入新的语言和库。

  • 使用字符串和内存管理工具。许多语言,包括 Java、Python、OCaml、Perl 和 Ruby,都提供透明的内存管理。您不需要分配和释放内存。您也不需要显式地关注字符串结束标记和字符串大小限制。这两种都是 C 程序中的常见问题,会导致运行时错误或安全漏洞。如果您必须使用 C,请考虑使用垃圾回收或内存池库。

  • 首先使其工作,然后再优化。在许多情况下,最好先开发可工作的代码,然后再对其进行优化。许多人首先进行优化,这在某些情况下确实有效。但是,简单、无错误的代码通常比尽可能快的代码更重要。

  • 编写清晰的代码。将代码拆分为函数。编写注释。记录每个函数的作用及其对环境的影响。

案例研究:OfflineIMAP 中的一个错误

OfflineIMAP 是一个与 IMAP 服务器对话并将 IMAP 文件夹树与本地树同步的程序。存在许多 IMAP 服务器,它们的工作方式并不完全相同。在其两年的历史中,OfflineIMAP 获得了本文中讨论的越来越多的调试技术。用户遇到的问题通常无法在我的特定设置中重现,因此详细的日志记录是必须的。一些 IMAP 服务器本身就有错误,因此许多报告需要解决的第一个问题是:这甚至是 OfflineIMAP 中的错误吗?在令人惊讶的许多情况下,答案是否定的。OfflineIMAP 使用某些大多数其他 IMAP 客户端不使用的 IMAP 功能,而这些功能在某些服务器中往往测试不佳。

我想带您了解我一直在处理的一个特别顽固的 OfflineIMAP 错误。大约一年前,有人使用 Debian 错误跟踪系统报告了 OfflineIMAP 中的一个错误。不幸的是,我无法重现该问题,并且原始提交者在问题发生时没有打开日志记录。他也无法获得调试信息。鉴于他确实拥有的信息,其中包括一条错误消息,我能够按照本文前面概述的步骤收集一些信息。我没有关于输入和输出的信息,但程序流程和堆栈看起来都很正常。最后,我能够确定程序崩溃的位置,但不知道为什么,所以该错误在那里搁置了一段时间。事情变得更加困难,因为该错误是间歇性的——有时程序会运行良好,有时会崩溃。

后来,第二个人遇到了同样的问题。他注意到了 Debian 上现有的错误报告,并发送了他的信息。OfflineIMAP 会在发生致命错误时自动尝试打印出调试日志的一部分,他能够捕获此输出。事实证明,OfflineIMAP 的这一功能在过去很有价值,因为它并非总是可以重现导致问题的情况。

在这种情况下,这些信息很有帮助。我现在能够看到 OfflineIMAP 在错误发生之前立即在做什么。但是,仍然没有足够的信息来发现确切的问题——一切看起来仍然正常。但是,该错误是间歇性的,他无法捕获任何其他信息。

最终,第三个人遇到了同样的问题。同样,他有一些信息,但还不足以弄清楚。还需要发生其他事情,所以我使特定代码部分的日志记录更加详细。希望通过额外的日志记录,下次遇到问题时,我将有足够的信息来跟踪它。

有几件事在这个过程中发挥了重要作用。首先,OfflineIMAP 总是在发生致命错误时生成可用的堆栈跟踪。即使是最不详细的报告也准确地显示了程序崩溃时的位置。其次,错误日志很有帮助,但如果人们无法轻松重现特定错误,则帮助较小。当程序崩溃或发生故障时打印出调试信息可能是有助于解决该问题的有用方法。

此外,错误跟踪系统在跟踪问题方面发挥了重要作用。由于 Debian 错误报告是公开的,因此涉及的三个提交者能够识别出一个未解决的错误报告并将他们的信息添加到其中。这有助于每个人管理与特定问题相关的信息,也为第一次遇到问题的人提供了一个起点。

结论

有很多方法可以帮助您的用户报告程序中的错误并跟踪它们,但不应在真空中使用它们。不要忘记使报告和跟踪错误变得容易,并从一开始就编写清晰的代码。最后,请记住,这些步骤都不是灵丹妙药。它们结合在一起可以简化您的错误跟踪过程并帮助找到许多问题,但它们不一定能解决所有问题。

本文的资源: /article/7747

John Goerzen 是一位资深的 Linux 程序员,《Python 网络编程基础》的作者。他还担任 Software in the Public Interest, Inc. 的总裁。John 欢迎您通过 jgoerzen@complete.org 发表评论。

加载 Disqus 评论