At the Forge - 调试 Rails 应用程序

作者:Reuven M. Lerner

在过去的几个月里,我介绍了一些 Ruby 程序员(尤其是 Rails 开发者)可用的自动化测试系统。自动化测试,特别是当你在开发之前和开发过程中使用它(而不是之后)时,是提高你的设计质量以及代码健壮性的绝佳方法。无论你 практикуете TDD(测试驱动开发)、BDD(行为驱动开发)还是类似于这两者的东西,你的软件都可能比未经测试的同类软件拥有更少的错误,并且更易于维护。

那么,这是否意味着以这种方式编写的软件没有错误?别傻了——当然有。但是,如果你认真对待测试,你可能会有更少的错误,而且这些错误可能更难检测且更微妙。

如果错误更智能(或更狡猾),你的调试和测试工具也需要更智能。幸运的是,随着 Ruby 语言和用于 Web 开发的 Rails 框架越来越受欢迎,许多才华横溢的开发者挺身而出,提供了各种复杂的工具,可以帮助你识别和消除错误。

本月,我将介绍一些我在使用 Ruby on Rails 开发 Web 应用程序时使用的工具。如果你在开发 Rails 应用程序时遇到困难,我希望这些技术能帮助你比以前更快地识别和解决问题。

查看日志文件

我是日志文件的忠实粉丝。当我的电脑或我编写的程序出现问题时,我的第一反应是检查日志。当我在 Rails 开发课程中授课,有人问为什么他们的程序不起作用时,我的第一反应总是问他们日志文件说了什么。它可以成为调试问题的宝贵起点。

对我来说幸运的是,Rails 核心团队也非常喜欢日志文件。每个环境——默认情况下有三个,用于开发、测试和生产——都有自己的日志文件,名为 log/ENVIRONMENT.log,位于应用程序的根目录下。

日志文件中放入什么内容取决于你所做的设置。默认情况下,开发日志包含比生产日志更多的细节,向你展示(除其他外)发送到数据库的实际 SQL 查询。每个日志消息都有一个与之关联的级别,就像你在其他日志系统中可能看到的那样,例如 Apache 或 log4j。只有当日志消息至少与当前环境的最小日志文件优先级(优先级递增顺序:debug、info、warn、error 和 fatal)一样重要时,才会写入日志文件。因此,fatal 日志消息始终会写入日志,而 debug 消息只有在当前环境的日志级别为 debug 时才会写入。

日志文件至少在两个方面很有用。首先,它允许你查看程序执行时内部发生的情况,尤其是在出现问题时。在开发环境中,致命错误会在浏览器和日志文件中生成堆栈跟踪。但在生产环境中,你可能不希望全世界看到你代码缺陷的内部结构。因此,堆栈跟踪仅出现在生产日志中,而浏览器显示错误消息。正如我之前所说,通过日志文件进行跟踪是我找出程序中发生情况的最喜欢的方式。

除了堆栈回溯之外,日志(尤其是开发日志)还包含大量其他信息,你可以从这个简短的示例中看到

Processing ReviewController#view_one_review (for 74.212.146.115 
at 2009-11-10 09:25:55) [GET]
  Session ID: 9513bd79785b3d037804b45709a1f12c
  Parameters: {"id"=>"2567"}
Rendering template within layouts/one-review.rhtml
Rendering review/view_one_review
Completed in 1400ms (View: 16, DB: 973) | 200 OK
[http://example.com/book_reviews/view_one_review/2567]

第一行显示了发出请求的日期和时间、调用的控制器和操作,以及导致调用该控制器和操作的 URL(在日志条目的底部)。它显示 HTTP 响应代码为(200,或“OK”),以及执行所花费的时间,让你大致了解特定方法的效率。而且,你甚至可以获得数据库和视图各自花费的时间的细分,使你能够将优化策略集中在你系统中真正需要的方法上。

你还可以获得提交给控制器操作的完整参数列表。这在我多次接到客户电话时特别有用。能够查看提交给控制器操作的参数,让我能够测试这些精确的参数,并在系统中跟踪它们的使用情况。

最后,你可以看到呈现了哪些视图模板。尤其是在具有复杂视图集的站点上,了解正在调用和显示的内容通常很有帮助。

在开发环境中,你还会看到这样的内容

Parameter Load (1.9ms)   SELECT * FROM "parameters"
          WHERE ("parameters"."param_name" = E'Blocked IPs'
          AND "parameters"."param_group" = E'Restrictions') LIMIT 1
PaymentMethod Load (2.0ms)   SELECT * FROM "payment_methods"
          WHERE (disabled = false) ORDER BY payment_method_name
State Load (6.5ms)   SELECT * FROM "states" ORDER BY abbreviation

换句话说,你可以看到对象何时从数据库初始化,以及为了实现这一点使用了哪些查询。

到目前为止,我已经展示了如何使用 Rails 日志文件中的默认输出,来查找有关应用程序正在执行的操作的有用信息。但我经常发现将信息写入日志文件也很有用,表明我在代码中的位置或已完成的特定任务。例如,在一个我参与开发的具有复杂定价算法的图书商店应用程序中,我会将定价算法中的每次计算都记录到日志文件中。这将允许我们“重放”用户看到的算法,并以各种方式仔细检查我们的工作。

要将信息记录到 Rails 日志中,只需使用 logger 对象并向其发送与你想要使用的日志级别对应的消息即可。例如,如果你想知道某个产品是否有库存,你可以说

logger.warn "Checking to see if ISBN '#{isbn}' is in stock."
if Isbn.in_stock?(["isbn = ? ", isbn])
  logger.warn "\tYup, we have it!"
end

这种“喋喋不休”的日志可能效率稍低,但它们使你更容易阅读(和跟踪)程序内部发生的事情,在你需要阅读日志来调试问题时。请记住,你创建这些日志文件是因为你知道有一天你需要阅读它们,并且通过使它们尽可能地令人愉快、流畅和具有启发性,你将使这种体验更加愉快。

我倾向于对写入日志的大多数内容使用 warn 级别,并在我希望某些内容仅出现在开发日志中时使用 debug。如果我必须显示复杂的数据结构,我通常会使用 YAML 和 .to_yaml 方法来显示它。这使得理解结构更容易,尤其是在你只对它的一个或两个属性感兴趣时。

我也是tail -f在日志文件上的重度用户,这让我可以查看日志文件的增长情况。结合 grep,这使得可以搜索日志文件中出现的特定方法、值或任何其他内容。例如,你可以使用以下命令查找每次提及 Person 对象的情况

tail -f log/development.log | grep Person

如果我想查看跨多个不同调用的值,我有时会在我的 logger 调用中放置一个特殊标记,例如方法名称,方法是将以下内容放入控制器方法中

logger.warn "[interesting_method] The value is '#{foo}'."

请注意,我在值周围放置了单引号;这使我可以更轻松地识别空格和空字符串。然后我可以查看何时调用此代码

tail -f log/development.log | grep interesting_method

虽然我最常查看开发日志(在编写代码时)和生产日志(在运行的服务器上),但我也发现有机会查看测试日志,其中显示了运行测试的结果。(如果你使用 Cucumber 进行测试,请注意它有自己的环境和日志文件。)

使用控制台

多年来,我一直使用日志文件来帮助我进行调试和开发,早在我开始使用 Rails 之前就开始了。但是日志文件只允许你被动地查看过去发生的事情。Rails 开发者工具箱中最好的工具之一是控制台,这是一个交互式命令行界面,允许你查询、执行和测试命令和变量值。如果你熟悉 Ruby 的用于交互式工作的“irb”,那么控制台对你来说会很熟悉。

控制台通常是我编写任何代码的第一个地方。它将你置于类似于控制器的上下文中,允许你通过 ActiveRecord 与数据库对话,创建(以及修改和销毁)对象并分配它们。

例如,我经常在控制台中摆弄关联和命名作用域,测试我可以轻松地通过另一个对象检索一个对象,或者使用方法调用检索对象子集。例如,如果我有一个 Person 对象和一个 Vehicle 对象,我应该能够在控制台中说 Person.new.vehicles 并获得空数组。或者,我应该能够使用命名作用域来说 Person.men,仅从数据库中检索男性。

我还大量使用控制台来测试对象的有效性。ActiveRecord 对象的 valid? 方法,加上在保存失败时引发异常的 save! 方法,让我可以测试对象,看看 ActiveRecord 是否认为它们是有效的,以及为什么。例如,如果我说

Person.new.save!

我应该得到一个失败的验证列表(我承认格式不漂亮),通常表明需要设置哪些属性才能成功保存人员。没错,你总是可以在保存失败后在对象上调用 errors 方法,但我发现这更快更明显,这正是我在控制台中操作的原因。

我经常使用控制台来拼凑我将要使用的方法,或者只是试验最终将进入类或模块的代码。如果你在控制台中修改了模型定义,你需要使用以下命令重新加载模型

reload!

以便控制台与源代码的当前状态同步。

默认情况下,控制台在开发环境中运行,因此当你编写

./script/console

你正在与开发数据库对话。

我应该注意到,有许多 Ruby gem 旨在改善 irb 体验。其中之一是 wirble,它可以为 irb 界面着色并以其他方式改进它。我已经使用它一段时间了,发现没有它的各种改进,很难使用 irb。

一个较新的条目是 looksee,它提供了一个新的 lp(查找路径)方法,该方法显示对象响应的每个方法,并按祖先列表分类和排序。使用 lp 使我更容易知道我需要检查哪个类或模块定义来调查特定方法。

如果你想将控制台用于生产环境,我经常在调试生产服务器上的问题时这样做,你将需要显式声明环境名称

./script/console production

我应该补充一点,最新版本的 Rails 包含一个类似的命令,dbconsole,它允许你直接与你连接的环境的关系数据库对话。我经常使用 dbconsole 而不是键入mysqlpsql(取决于我使用的系统)。但是,重要的是要记住,当你在 Rails 控制台中工作时,对象会受到验证和其他完整性检查的约束,而这些检查在与关系数据库的原始连接中是不存在的。因此,为了安全起见,通常最好通过 Ruby 而不是数据库来完成操作。(尽管如果你的数据库支持事务,你可以通过在 BEGIN-COMMIT 块中进行所有修改来获得一定的安全性。)

调试器

当然,控制台非常适合测试代码,但你无法在控制台中运行整个 Rails 应用程序。但是,有时你希望可以进入控制台,只是为了 Rails 应用程序的一部分,以便在其中探索。没错,你可以使用日志语句将当前状态写入日志文件,但是没有什么比从内部交互式探索应用程序更能让你更好地了解它的工作方式(或不工作方式)。

这个问题的解决方案是看似简单的 ruby-debug gem,你可以通过以下命令安装它(像所有 Ruby gem 一样)

sudo gem install ruby-debug

然后你需要包含 gem。这通常在开发和/或测试环境中完成,但在生产环境中不完成,原因很明显。在 config/environments/development.rb(或 test.rb)中,你添加

require 'ruby-debug'

你就设置好了!

在大多数情况下,ruby-debug 将完全不执行任何操作。它不会影响你的代码、执行或任何其他内容。但是,如果你删除方法

debugger

到你的 Rails 应用程序中,应用程序将在到达该行时停止,并为你提供一个真正的调试器,看起来有点像 GDB。你可以在当前行周围获得几行上下文,你可以使用 p 命令打印当前值(或任何其他表达式),并且你可以使用 n 命令逐行在程序中前进。你的 Web 浏览器,当它调用特定的控制器操作时,可能会触发调试器,在你使用调试器、单步执行代码和检查环境时,它将挂起。

为了更全面地探索事物,你可以随时进入 irb,获得另一个版本的 Rails 控制台。当你想做的事情不仅仅是检查变量值时,这很好——例如,探索数据库或深入了解系统的内部结构。

请注意,由于 ruby-debug 的性质,它实际上只适用于你在前台运行的 HTTP 服务器,例如 WEBrick。但是,没有什么可以阻止你拥有两个不同的应用程序实例(一个使用 Phusion,另一个使用 WEBrick)在同一环境中运行或在同一数据库上工作——只需确保在不同的端口上运行它们,并确保跟踪你可能在 Web 浏览器中打开的多个选项卡。

在过去的几个月里,我才开始认真地使用 ruby-debug,我已经想知道没有它我以前是怎么过来的。如果说有什么的话,那就是从内部探索我的应用程序给了我很多我本来永远不会有的见解,并且它给了我一个积极地查看事物的机会,而不仅仅是使用日志文件。

第三方选项

最后,你可能想尝试一家或两家涌现出来的商业 Rails 服务,它们为 Rails 应用程序提供监控和通知。我应该明确指出,这两家公司都由营利性公司托管,尽管它们提供产品的免费版本,但它们的最终目标可能是赚钱。

New Relic RPM 是一个性能监视器,你可以作为插件安装到你的 Rails 应用程序中。每隔几分钟,插件会将你当前的应用程序状态报告回 New Relic 的服务器,然后在 New Relic 的服务器上以易于理解的格式提供数据。New Relic 的基本产品是免费的,虽然它比商业版本的功能要有限得多,但我发现它在为我提供当前系统性能和瓶颈的快照方面非常有用。如果你的网站带来了收入,那么为 New Relic 的商业产品之一付费可能是值得的,这些产品不仅提供过去 30 分钟的控制器和服务器性能指标,还提供过去几周的指标,以及更详细的内存、数据库和 CPU 使用情况分析等等。

Hoptoad 是 Thoughtbot 运行的一项服务,它与 New Relic RPM 类似,因为它也有免费版本和商业版本。Hoptoad 类似于许多通知系统,当你的应用程序中发生异常时,它会向你发送电子邮件。但是,它会跟踪整个堆栈跟踪和请求上下文,并且还在 Hoptoad 的网站上保留一个日志,将类似的错误放在一起。你还可以指示何时解决了问题,将其用作一种原始的错误跟踪应用程序。(虽然我发现令人恼火的是,你只会在第一次出现特定错误时收到电子邮件,直到你将其标记为已解决。)Hoptoad 已经打入了许多我参与过的 Rails 项目,我发现它比更简单的异常通知系统更可靠,更易于在我的项目中使用。

结论

调试 Web 应用程序从来都不是一件容易的事,但 Ruby on Rails 社区已经设法创建了一套有用的、强大的工具,这些工具可以为普通 Web 开发者带来巨大的改变。无论你是新手开发者还是经验丰富的开发者,在你的工具箱中拥有这些工具都可以让你更有效地查找错误,并让你的应用程序无错误地交付给你的客户。

资源

Rails 调试的整个主题的一个很好的介绍在 Rails Guides 系列中,特别是关于调试的文章:guides.rubyonrails.org/debugging_rails_applications.html

关于 ruby-debug 的一个稍微过时的教程,但它很直接且易于理解,由 Patrick Lenz 在 articles.sitepoint.com/article/debug-rails-app-ruby-debug 编写。

Amy Hoy,像往常一样,对这个主题有很多有趣且有用的见解:slash7.com/articles/2006/12/21/secrets-of-the-rails-console-ninjas

Reuven M. Lerner,一位长期的 Web/数据库开发者和顾问,是西北大学学习科学博士候选人,研究在线学习社区。在芝加哥地区生活四年后,他最近(与妻子和三个孩子)返回他们在以色列莫迪因的家。

加载 Disqus 评论