更多内容请访问 rubyonrails.org:

调试 Rails 应用程序

本指南介绍了调试 Ruby on Rails 应用程序的技术。

阅读本指南后,您将了解

  • 调试的目的。
  • 如何追踪应用程序中测试未能发现的问题和错误。
  • 不同的调试方法。
  • 如何分析堆栈跟踪。

1. 用于调试的视图助手

一个常见的任务是检查变量的内容。Rails 提供了三种不同的方法来完成此操作

  • debug
  • to_yaml
  • inspect

1.1. debug

debug 助手将返回一个

 标签,该标签使用 YAML 格式渲染对象。这将从任何对象生成人类可读的数据。例如,如果您在视图中有以下代码

<%= debug @article %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

您将看到类似这样的内容

--- !ruby/object Article
attributes:
  updated_at: 2008-09-05 22:55:47
  body: It's a very helpful guide for debugging your Rails app.
  title: Rails debugging guide
  published: t
  id: "1"
  created_at: 2008-09-05 22:55:47
attributes_cache: {}


Title: Rails debugging guide

1.2. to_yaml

或者,对任何对象调用 to_yaml 会将其转换为 YAML。您可以将此转换后的对象传递给 simple_format 助手方法以格式化输出。这就是 debug 完成其魔力的方式。

<%= simple_format @article.to_yaml %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

上述代码将渲染类似这样的内容

--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
title: Rails debugging guide
published: t
id: "1"
created_at: 2008-09-05 22:55:47
attributes_cache: {}

Title: Rails debugging guide

1.3. inspect

另一个用于显示对象值的有用方法是 inspect,尤其是在处理数组或哈希时。它将对象值打印为字符串。例如

<%= [1, 2, 3, 4, 5].inspect %>
<p>
  <b>Title:</b>
  <%= @article.title %>
</p>

将渲染

[1, 2, 3, 4, 5]

Title: Rails debugging guide

2. 日志记录器

在运行时将信息保存到日志文件也很有用。Rails 为每个运行时环境维护一个单独的日志文件。

2.1. 什么是日志记录器?

Rails 使用 ActiveSupport::Logger 类来写入日志信息。可以替换其他日志记录器,例如 Log4r

# config/environments/production.rb
config.logger = Logger.new(STDOUT)
config.logger = Log4r::Logger.new("Application Log")

默认情况下,每个日志都在 Rails.root/log/ 下创建,并且日志文件以应用程序运行的环境命名。

2.2. 日志级别

当记录某项内容时,如果消息的日志级别等于或高于配置的日志级别,则会将其打印到相应的日志中。如果您想知道当前的日志级别,可以调用 Rails.logger.level 方法。

可用的日志级别是::debug:info:warn:error:fatal:unknown,分别对应日志级别数字 0 到 5。要更改默认日志级别

# config/environments/production.rb
config.log_level = :warn

当您希望在开发或测试阶段进行日志记录,而又不想用不必要的信息淹没生产日志时,这很有用。

默认的 Rails 日志级别是 :debug。但是,在默认生成的 config/environments/production.rb 中,它在 production 环境中设置为 :info

2.3. 发送消息

要在当前日志中写入,请在控制器、模型或邮件程序中使用 logger.(debug|info|warn|error|fatal|unknown) 方法

logger.debug "Person attributes hash: #{@person.attributes.inspect}"
logger.info "Processing the request..."
logger.fatal "Terminating application, raised unrecoverable error!!!"

这是一个使用额外日志记录的检测方法示例

class ArticlesController < ApplicationController
  # ...

  def create
    @article = Article.new(article_params)
    logger.debug "New article: #{@article.attributes.inspect}"
    logger.debug "Article should be valid: #{@article.valid?}"

    if @article.save
      logger.debug "The article was saved and now the user is going to be redirected..."
      redirect_to @article, notice: 'Article was successfully created.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  # ...

  private
    def article_params
      params.expect(article: [:title, :body, :published])
    end
end

这是执行此控制器操作时生成的日志示例

Started POST "/articles" for 127.0.0.1 at 2018-10-18 20:09:23 -0400
Processing by ArticlesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"XLveDrKzF1SwaiNRPTaMtkrsTzedtebPPkmxEFIU0ordLjICSnXsSNfrdMa4ccyBjuGwnnEiQhEoMN6H1Gtz3A==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>"0"}, "commit"=>"Create Article"}
New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil}
Article should be valid: true
   (0.0ms)  begin transaction
  ↳ app/controllers/articles_controller.rb:31
  Article Create (0.5ms)  INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs."], ["published", 0], ["created_at", "2018-10-19 00:09:23.216549"], ["updated_at", "2018-10-19 00:09:23.216549"]]
  ↳ app/controllers/articles_controller.rb:31
   (2.3ms)  commit transaction
  ↳ app/controllers/articles_controller.rb:31
The article was saved and now the user is going to be redirected...
Redirected to https://:3000/articles/1
Completed 302 Found in 4ms (ActiveRecord: 0.8ms)

像这样添加额外的日志记录可以很容易地在日志中搜索意外或异常行为。如果您添加额外的日志记录,请务必明智地使用日志级别,以避免用无用的琐事填充您的生产日志。

2.4. 详细查询日志

查看日志中的数据库查询输出时,可能无法立即清楚为什么在调用单个方法时会触发多个数据库查询

irb(main):001:0> Article.pamplemousse
  Article Load (0.4ms)  SELECT "articles".* FROM "articles"
  Comment Load (0.2ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 1]]
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 2]]
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 3]]
=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">

启用 verbose_query_logs 后,我们可以看到每个查询的附加信息

irb(main):003:0> Article.pamplemousse
  Article Load (0.2ms)  SELECT "articles".* FROM "articles"
  ↳ app/models/article.rb:5
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 1]]
  ↳ app/models/article.rb:6
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 2]]
  ↳ app/models/article.rb:6
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ?  [["article_id", 3]]
  ↳ app/models/article.rb:6
=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10">

在每个数据库语句下方,您可以看到指向导致数据库调用的方法的特定源文件名(和行号)的箭头,例如 ↳ app/models/article.rb:5

这可以帮助您识别和解决由 N+1 查询引起的性能问题:即生成多个额外查询的单个数据库查询。

详细查询日志在开发环境日志中默认启用

我们建议不要在生产环境中使用此设置。它依赖于 Ruby 的 Kernel#caller 方法,该方法倾向于分配大量内存以生成方法调用的堆栈跟踪。请改用查询日志标签(参见下文)。

2.5. 详细入队日志

与上面的“详细查询日志”类似,允许打印入队后台作业的方法的源位置。

详细入队日志在开发环境日志中默认启用

# config/environments/development.rb
config.active_job.verbose_enqueue_logs = true
# bin/rails console
ActiveJob.verbose_enqueue_logs = true

我们建议不要在生产环境中使用此设置。

2.6. 详细重定向日志

与上面其他详细日志设置类似,这会记录重定向的源位置。

Redirected to https://:3000/posts/1
↳ app/controllers/posts_controller.rb:32:in `block (2 levels) in create'

它在开发环境中默认启用。要在其他环境中启用,请使用此配置

config.action_dispatch.verbose_redirect_logs = true

与其他详细日志记录器一样,不建议在生产环境中使用它。

3. SQL 查询注释

SQL 语句可以使用包含运行时信息的标签进行注释,例如控制器或作业的名称,以将有问题的查询追溯到生成这些语句的应用程序区域。当您记录慢查询(例如 MySQLPostgreSQL)、查看当前运行的查询或用于端到端跟踪工具时,这很有用。

config.active_record.query_log_tags_enabled = true

启用查询标签会自动禁用预处理语句,因为它会使大多数查询变得唯一。

默认情况下,应用程序的名称、控制器的名称和操作,或作业的名称都会被记录。默认格式是 SQLCommenter。例如

Article Load (0.2ms)  SELECT "articles".* FROM "articles" /*application='Blog',controller='articles',action='index'*/

Article Update (0.3ms)  UPDATE "articles" SET "title" = ?, "updated_at" = ? WHERE "posts"."id" = ? /*application='Blog',job='ImproveTitleJob'*/  [["title", "Improved Rails debugging guide"], ["updated_at", "2022-10-16 20:25:40.091371"], ["id", 1]]

ActiveRecord::QueryLogs 的行为可以修改,以包含任何有助于连接 SQL 查询的点,例如应用程序日志的请求和作业 ID、账户和租户标识符等。

3.1. 标签化日志记录

在运行多用户、多账户应用程序时,能够使用一些自定义规则过滤日志通常很有用。Active Support 中的 TaggedLogging 通过用子域、请求 ID 和任何其他内容标记日志行来帮助您做到这一点,以帮助调试此类应用程序。

logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.tagged("BCX") { logger.info "Stuff" }                            # Logs "[BCX] Stuff"
logger.tagged("BCX", "Jason") { logger.info "Stuff" }                   # Logs "[BCX] [Jason] Stuff"
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"

3.2. 日志对性能的影响

日志记录总是会对 Rails 应用程序的性能产生一点影响,尤其是在写入磁盘时。此外,还有一些微妙之处

使用 :debug 级别将比 :fatal 产生更大的性能损失,因为有更多的字符串被评估并写入日志输出(例如磁盘)。

另一个潜在的陷阱是代码中对 Logger 的调用过多

logger.debug "Person attributes hash: #{@person.attributes.inspect}"

在上面的示例中,即使允许的输出级别不包括 debug,也会有性能影响。原因是 Ruby 必须评估这些字符串,其中包括实例化有些重量级的 String 对象并插入变量。

因此,建议将块传递给日志记录器方法,因为只有当输出级别与允许的级别相同或包含在允许的级别中时,才会评估这些块(即惰性加载)。重写的相同代码将是

logger.debug { "Person attributes hash: #{@person.attributes.inspect}" }

只有在启用调试时,才会评估块的内容以及字符串插值。这种性能节省只有在大量日志记录时才真正明显,但这是一种很好的做法。

4. 使用 debug Gem 调试

当您的代码行为异常时,您可以尝试打印到日志或控制台以诊断问题。不幸的是,有时这种错误跟踪在查找问题根源方面并不有效。当您实际需要深入到正在运行的源代码中时,调试器是您最好的伴侣。

如果您想了解 Rails 源代码但不知道从何开始,调试器也可以帮助您。只需调试对您的应用程序的任何请求,并使用本指南了解如何从您编写的代码进入底层的 Rails 代码。

Rails 7 在由 CRuby 生成的新应用程序的 Gemfile 中包含了 debug gem。默认情况下,它在 developmenttest 环境中可用。请查看其文档以了解用法。

4.1. 进入调试会话

默认情况下,在需要 debug 库后(在您的应用程序启动时发生)将启动调试会话。但请不要担心,会话不会干扰您的应用程序。

要进入调试会话,您可以使用 binding.break 及其别名:binding.bdebugger。以下示例将使用 debugger

class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  # GET /posts or /posts.json
  def index
    @posts = Post.all
    debugger
  end
  # ...
end

一旦您的应用程序评估了调试语句,它将进入调试会话

Processing by PostsController#index as HTML
[2, 11] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
     2|   before_action :set_post, only: %i[ show edit update destroy ]
     3|
     4|   # GET /posts or /posts.json
     5|   def index
     6|     @posts = Post.all
=>   7|     debugger
     8|   end
     9|
    10|   # GET /posts/1 or /posts/1.json
    11|   def show
=>#0    PostsController#index at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7
  #1    ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg)

您可以随时退出调试会话并使用 continue(或 c)命令继续应用程序执行。或者,要退出调试会话和应用程序,请使用 quit(或 q)命令。

4.2. 上下文

进入调试会话后,您可以像在 Rails 控制台或 IRB 中一样输入 Ruby 代码。

(rdbg) @posts    # ruby
[]
(rdbg) self
#<PostsController:0x0000000000aeb0>
(rdbg)

您还可以使用 ppp 命令来评估 Ruby 表达式,这在变量名与调试器命令冲突时很有用。

(rdbg) p headers    # command
=> {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff", "X-Download-Options"=>"noopen", "X-Permitted-Cross-Domain-Policies"=>"none", "Referrer-Policy"=>"strict-origin-when-cross-origin"}
(rdbg) pp headers    # command
{"X-Frame-Options"=>"SAMEORIGIN",
 "X-XSS-Protection"=>"1; mode=block",
 "X-Content-Type-Options"=>"nosniff",
 "X-Download-Options"=>"noopen",
 "X-Permitted-Cross-Domain-Policies"=>"none",
 "Referrer-Policy"=>"strict-origin-when-cross-origin"}
(rdbg)

除了直接评估之外,调试器还通过不同的命令帮助您收集大量信息,例如

  • info (或 i) - 有关当前帧的信息。
  • backtrace (或 bt) - 回溯(带有附加信息)。
  • outline (或 o, ls) - 当前作用域中可用的方法、常量、局部变量和实例变量。

4.2.1. info 命令

info 提供当前帧可见的局部变量和实例变量值的概述。

(rdbg) info    # command
%self = #<PostsController:0x0000000000af78>
@_action_has_layout = true
@_action_name = "index"
@_config = {}
@_lookup_context = #<ActionView::LookupContext:0x00007fd91a037e38 @details_key=nil, @digest_cache=...
@_request = #<ActionDispatch::Request GET "https://:3000/posts" for 127.0.0.1>
@_response = #<ActionDispatch::Response:0x00007fd91a03ea08 @mon_data=#<Monitor:0x00007fd91a03e8c8>...
@_response_body = nil
@_routes = nil
@marked_for_same_origin_verification = true
@posts = []
@rendered_format = nil

4.2.2. backtrace 命令

在不带任何选项使用时,backtrace 列出堆栈上的所有帧

=>#0    PostsController#index at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7
  #1    ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-2.0.alpha/lib/action_controller/metal/basic_implicit_render.rb:6
  #2    AbstractController::Base#process_action(method_name="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/abstract_controller/base.rb:214
  #3    ActionController::Rendering#process_action(#arg_rest=nil) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/action_controller/metal/rendering.rb:53
  #4    block in process_action at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-8.1.0.alpha/lib/abstract_controller/callbacks.rb:221
  #5    block in run_callbacks at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-8.1.0.alpha/lib/active_support/callbacks.rb:118
  #6    ActionText::Rendering::ClassMethods#with_renderer(renderer=#<PostsController:0x0000000000af78>) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-8.1.0.alpha/lib/action_text/rendering.rb:20
  #7    block {|controller=#<PostsController:0x0000000000af78>, action=#<Proc:0x00007fd91985f1c0 /Users/st0012/...|} in <class:Engine> (4 levels) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actiontext-8.1.0.alpha/lib/action_text/engine.rb:69
  #8    [C] BasicObject#instance_exec at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activesupport-8.1.0.alpha/lib/active_support/callbacks.rb:127
  ..... and more

每个帧都带有

  • 帧标识符
  • 调用位置
  • 附加信息(例如块或方法参数)

这会给您一个很好的了解您的应用程序中正在发生什么。但是,您可能会注意到

  • 帧太多(通常在 Rails 应用程序中超过 50 个)。
  • 大多数帧来自 Rails 或您使用的其他库。

backtrace 命令提供 2 个选项来帮助您过滤帧

  • backtrace [num] - 只显示 num 个帧,例如 backtrace 10
  • backtrace /pattern/ - 只显示其标识符或位置与模式匹配的帧,例如 backtrace /MyModel/

也可以将这些选项一起使用:backtrace [num] /pattern/

4.2.3. outline 命令

outline 类似于 pryirbls 命令。它将向您展示当前作用域中可访问的内容,包括

  • 局部变量
  • 实例变量
  • 类变量
  • 方法及其来源
ActiveSupport::Configurable#methods: config
AbstractController::Base#methods:
  action_methods  action_name  action_name=  available_action?  controller_path  inspect
  response_body
ActionController::Metal#methods:
  content_type       content_type=  controller_name  dispatch          headers
  location           location=      media_type       middleware_stack  middleware_stack=
  middleware_stack?  performed?     request          request=          reset_session
  response           response=      response_body=   response_code     session
  set_request!       set_response!  status           status=           to_a
ActionView::ViewPaths#methods:
  _prefixes  any_templates?  append_view_path   details_for_lookup  formats     formats=  locale
  locale=    lookup_context  prepend_view_path  template_exists?    view_paths
AbstractController::Rendering#methods: view_assigns

# .....

PostsController#methods: create  destroy  edit  index  new  show  update
instance variables:
  @_action_has_layout  @_action_name    @_config  @_lookup_context                      @_request
  @_response           @_response_body  @_routes  @marked_for_same_origin_verification  @posts
  @rendered_format
class variables: @@raise_on_open_redirects

4.3. 断点

有许多方法可以在调试器中插入和触发断点。除了直接在代码中添加调试语句(例如 debugger)之外,您还可以使用命令插入断点

  • break (或 b)
    • break - 列出所有断点
    • break - 在当前文件的 num 行设置断点
    • break - 在 filenum 行设置断点
    • break break - 在 Class#methodClass.method 设置断点
    • break . - 在 结果的 方法上设置断点。
  • catch - 设置一个断点,当抛出 Exception 时会停止
  • watch <@ivar> - 设置一个断点,当当前对象的 @ivar 结果改变时会停止(这很慢)

要删除它们,您可以使用

  • delete (或 del)
    • delete - 删除所有断点
    • delete - 删除 ID 为 num 的断点

4.3.1. break 命令

在指定的行号设置断点 - 例如 b 28

[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
    20|   end
    21|
    22|   # POST /posts or /posts.json
    23|   def create
    24|     @post = Post.new(post_params)
=>  25|     debugger
    26|
    27|     respond_to do |format|
    28|       if @post.save
    29|         format.html { redirect_to @post, notice: "Post was successfully created." }
=>#0    PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg) b 28    # break command
#0  BP - Line  /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line)
(rdbg) c    # continue command
[23, 32] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
    23|   def create
    24|     @post = Post.new(post_params)
    25|     debugger
    26|
    27|     respond_to do |format|
=>  28|       if @post.save
    29|         format.html { redirect_to @post, notice: "Post was successfully created." }
    30|         format.json { render :show, status: :created, location: @post }
    31|       else
    32|         format.html { render :new, status: :unprocessable_entity }
=>#0    block {|format=#<ActionController::MimeResponds::Collec...|} in create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:28
  #1    ActionController::MimeResponds#respond_to(mimes=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/mime_responds.rb:205
  # and 74 frames (use `bt' command for all frames)

Stop by #0  BP - Line  /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line)

在给定的方法调用上设置断点 - 例如 b @post.save

[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
    20|   end
    21|
    22|   # POST /posts or /posts.json
    23|   def create
    24|     @post = Post.new(post_params)
=>  25|     debugger
    26|
    27|     respond_to do |format|
    28|       if @post.save
    29|         format.html { redirect_to @post, notice: "Post was successfully created." }
=>#0    PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg) b @post.save    # break command
#0  BP - Method  @post.save at /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:43

(rdbg) c    # continue command
[39, 48] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb
    39|         SuppressorRegistry.suppressed[name] = previous_state
    40|       end
    41|     end
    42|
    43|     def save(**) # :nodoc:
=>  44|       SuppressorRegistry.suppressed[self.class.name] ? true : super
    45|     end
    46|
    47|     def save!(**) # :nodoc:
    48|       SuppressorRegistry.suppressed[self.class.name] ? true : super
=>#0    ActiveRecord::Suppressor#save(#arg_rest=nil) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:44
  #1    block {|format=#<ActionController::MimeResponds::Collec...|} in create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:28
  # and 75 frames (use `bt' command for all frames)

Stop by #0  BP - Method  @post.save at /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:43

4.3.2. catch 命令

当抛出异常时停止 - 例如 catch ActiveRecord::RecordInvalid

[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
    20|   end
    21|
    22|   # POST /posts or /posts.json
    23|   def create
    24|     @post = Post.new(post_params)
=>  25|     debugger
    26|
    27|     respond_to do |format|
    28|       if @post.save!
    29|         format.html { redirect_to @post, notice: "Post was successfully created." }
=>#0    PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg) catch ActiveRecord::RecordInvalid    # command
#1  BP - Catch  "ActiveRecord::RecordInvalid"
(rdbg) c    # continue command
[75, 84] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb
    75|     def default_validation_context
    76|       new_record? ? :create : :update
    77|     end
    78|
    79|     def raise_validation_error
=>  80|       raise(RecordInvalid.new(self))
    81|     end
    82|
    83|     def perform_validations(options = {})
    84|       options[:validate] == false || valid?(options[:context])
=>#0    ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80
  #1    ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53
  # and 88 frames (use `bt' command for all frames)

Stop by #1  BP - Catch  "ActiveRecord::RecordInvalid"

4.3.3. watch 命令

当实例变量改变时停止 - 例如 watch @_response_body

[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
    20|   end
    21|
    22|   # POST /posts or /posts.json
    23|   def create
    24|     @post = Post.new(post_params)
=>  25|     debugger
    26|
    27|     respond_to do |format|
    28|       if @post.save!
    29|         format.html { redirect_to @post, notice: "Post was successfully created." }
=>#0    PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg) watch @_response_body    # command
#0  BP - Watch  #<PostsController:0x00007fce69ca5320> @_response_body =
(rdbg) c    # continue command
[173, 182] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal.rb
   173|       body = [body] unless body.nil? || body.respond_to?(:each)
   174|       response.reset_body!
   175|       return unless body
   176|       response.body = body
   177|       super
=> 178|     end
   179|
   180|     # Tests if render or redirect has already happened.
   181|     def performed?
   182|       response_body || response.committed?
=>#0    ActionController::Metal#response_body=(body=["<html><body>You are being <a href=\"ht...) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal.rb:178 #=> ["<html><body>You are being <a href=\"ht...
  #1    ActionController::Redirecting#redirect_to(options=#<Post id: 13, title: "qweqwe", content:..., response_options={:allow_other_host=>false}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/redirecting.rb:74
  # and 82 frames (use `bt' command for all frames)

Stop by #0  BP - Watch  #<PostsController:0x00007fce69ca5320> @_response_body =  -> ["<html><body>You are being <a href=\"https://:3000/posts/13\">redirected</a>.</body></html>"]
(rdbg)

4.3.4. 断点选项

除了不同类型的断点之外,您还可以指定选项以实现更高级的调试工作流。目前,调试器支持 4 个选项

  • do: - 当断点被触发时,执行给定的命令/表达式并继续程序
    • break Foo#bar do: bt - 当调用 Foo#bar 时,打印堆栈帧。
  • pre: - 当断点被触发时,在停止之前执行给定的命令/表达式
    • break Foo#bar pre: info - 当调用 Foo#bar 时,在停止之前打印其周围变量。
  • if: - 只有当 的结果为真时,断点才停止
    • break Post#save if: params[:debug] - 如果 params[:debug] 也为真,则在 Post#save 处停止。
  • path: - 只有当触发它的事件(例如方法调用)发生在给定路径时,断点才停止
    • break Post#save path: app/services/a_service - 如果方法调用发生在包含 app/services/a_service 的路径中,则在 Post#save 处停止。

另请注意,前 3 个选项:do:pre:if: 也可用于我们前面提到的调试语句。例如

[2, 11] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb
     2|   before_action :set_post, only: %i[ show edit update destroy ]
     3|
     4|   # GET /posts or /posts.json
     5|   def index
     6|     @posts = Post.all
=>   7|     debugger(do: "info")
     8|   end
     9|
    10|   # GET /posts/1 or /posts/1.json
    11|   def show
=>#0    PostsController#index at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7
  #1    ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 72 frames (use `bt' command for all frames)
(rdbg:binding.break) info
%self = #<PostsController:0x00000000017480>
@_action_has_layout = true
@_action_name = "index"
@_config = {}
@_lookup_context = #<ActionView::LookupContext:0x00007fce3ad336b8 @details_key=nil, @digest_cache=...
@_request = #<ActionDispatch::Request GET "https://:3000/posts" for 127.0.0.1>
@_response = #<ActionDispatch::Response:0x00007fce3ad397e8 @mon_data=#<Monitor:0x00007fce3ad396a8>...
@_response_body = nil
@_routes = nil
@marked_for_same_origin_verification = true
@posts = #<ActiveRecord::Relation [#<Post id: 2, title: "qweqwe", content: "qweqwe", created_at: "...
@rendered_format = nil

4.3.5. 编写调试工作流

有了这些选项,您可以像这样在一行中编写调试工作流

def create
  debugger(do: "catch ActiveRecord::RecordInvalid do: bt 10")
  # ...
end

然后调试器将运行脚本命令并插入捕获断点

(rdbg:binding.break) catch ActiveRecord::RecordInvalid do: bt 10
#0  BP - Catch  "ActiveRecord::RecordInvalid"
[75, 84] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb
    75|     def default_validation_context
    76|       new_record? ? :create : :update
    77|     end
    78|
    79|     def raise_validation_error
=>  80|       raise(RecordInvalid.new(self))
    81|     end
    82|
    83|     def perform_validations(options = {})
    84|       options[:validate] == false || valid?(options[:context])
=>#0    ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80
  #1    ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53
  # and 88 frames (use `bt' command for all frames)

一旦捕获断点被触发,它将打印堆栈帧

Stop by #0  BP - Catch  "ActiveRecord::RecordInvalid"

(rdbg:catch) bt 10
=>#0    ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80
  #1    ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53
  #2    block in save! at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/transactions.rb:302

此技术可以帮助您避免重复的手动输入,并使调试体验更顺畅。

您可以从其文档中找到更多命令和配置选项。

5. 使用 web-console Gem 调试

Web Console 有点像 debug,但它在浏览器中运行。您可以在任何页面的视图或控制器的上下文中请求一个控制台。控制台将渲染在您的 HTML 内容旁边。

5.1. 控制台

在任何控制器动作或视图中,您都可以通过调用 console 方法来调用控制台。

例如,在控制器中

class PostsController < ApplicationController
  def new
    console
    @post = Post.new
  end
end

或者在视图中

<% console %>

<h2>New Post</h2>

这将在您的视图中渲染一个控制台。您无需关心 console 调用的位置;它不会在其调用位置渲染,而是在您的 HTML 内容旁边渲染。

控制台执行纯 Ruby 代码:您可以定义和实例化自定义类,创建新模型,并检查变量。

每个请求只能渲染一个控制台。否则,web-console 将在第二次 console 调用时引发错误。

5.2. 检查变量

您可以调用 instance_variables 来列出上下文中所有可用的实例变量。如果您想列出所有局部变量,可以使用 local_variables

5.3. 设置

  • config.web_console.allowed_ips:IPv4 或 IPv6 地址和网络的授权列表(默认值:127.0.0.1/8, ::1)。
  • config.web_console.whiny_requests:当阻止控制台渲染时记录一条消息(默认值:true)。

由于 web-console 在服务器上远程评估纯 Ruby 代码,因此不要尝试在生产环境中使用它。

6. 调试内存泄漏

一个 Ruby 应用程序(无论是否在 Rails 上),都可能泄漏内存——无论是 Ruby 代码还是 C 代码级别。

在本节中,您将学习如何使用 Valgrind 等工具查找和修复此类泄漏。

6.1. Valgrind

Valgrind 是一个用于检测基于 C 的内存泄漏和竞争条件的应用程序。

有一些 Valgrind 工具可以自动检测许多内存管理和线程错误,并详细分析您的程序。例如,如果解释器中的 C 扩展调用了 malloc() 但没有正确调用 free(),则在应用程序终止之前,此内存将不可用。

有关如何安装 Valgrind 并与 Ruby 一起使用的更多信息,请参阅 Evan Weaver 的Valgrind 和 Ruby

6.2. 查找内存泄漏

Derailed 有一篇关于检测和修复内存泄漏的优秀文章,您可以在此处阅读

7. 用于调试的插件

有一些 Rails 插件可以帮助您查找错误并调试您的应用程序。以下是一些有用的调试插件列表

  • Query Trace 为您的日志添加查询源跟踪。
  • Exception Notifier 提供一个邮件程序对象和一组默认模板,用于在 Rails 应用程序中发生错误时发送电子邮件通知。
  • Better Errors 用一个包含更多上下文信息(如源代码和变量检查)的新页面替换标准的 Rails 错误页面。
  • RailsPanel 用于 Rails 开发的 Chrome 扩展,它将结束您对 development.log 的追踪。在浏览器中的开发人员工具面板中获取有关您的 Rails 应用程序请求的所有信息。提供数据库/渲染/总时间、参数列表、渲染视图等方面的见解。
  • Pry 一个 IRB 替代品和运行时开发人员控制台。

8. 参考资料



回到顶部