如何让Ruby打印完整的回溯信息并包含传递给函数的参数?

15

有时候回溯足以诊断问题。但是有时候,如果没有知道传递给函数的内容,崩溃原因就不明显。

获取导致崩溃的函数所接收到的信息会非常有用,特别是在无法重现崩溃的情况下。例如,在网络连接异常、奇怪的用户输入或程序依赖于随机化或处理来自外部传感器的数据时引起的崩溃。

假设有以下程序:

def handle_changed_input(changed_input)
    raise 'ops' if changed_input =~ /magic/
end

def do_something_with_user_input(input)
    input = "#{input.strip}c"
    handle_changed_input(input)
end

input = gets
do_something_with_user_input(input)

当用户输入“magic”时。通常情况下,人们会

test.rb:2:in `handle_changed_input': ops (RuntimeError)
    from test.rb:7:in `do_something_with_user_input'
    from test.rb:11:in `<main>'

作为输出。有什么方法可以展示传递给函数的参数?就像这样

test.rb:2:in `handle_changed_input("magic")': ops (RuntimeError)
    from test.rb:7:in `do_something_with_user_input("magi\n")'
    from test.rb:11:in `<main>'

在许多情况下这将是有用的(但当参数无法表示为合理长度的字符串时,它并不真正有用,这也是为什么默认情况下它没有启用的一个很好的原因)。

如何添加此功能?在正常运行期间必须保证程序像通常一样工作,并且最好在崩溃之前没有额外的输出。

例如,我尝试过:

def do_something_with_user_input(input)
    method(__method__).parameters.map do |_, name|
        puts "#{name}=#{binding.local_variable_get(name)}"
    end
    raise 'ops' if input =~ /magic/
end

input = gets

发现在Ruby中有一种访问方法参数的方法吗?但它会在每次函数进入时打印,这会导致输出泛滥并且使程序显著变慢。


如果我理解正确,您需要打印出哪些参数被传递给引发错误的方法。是这样吗? - Efesto
@fylooi 我知道有很多情况下这样做没有意义(例如没有转换为字符串的对象、非常长的字符串、处理私人数据的程序以及许多其他情况)。但在方法具有字符串或已实现字符串转换的对象作为参数的情况下,这将非常有用。另请参见“为什么默认情况下未启用它的一个很好的理由”。 - reducing activity
@Efesto 非常感谢,问题的初始版本不够清晰。我想打印出堆栈跟踪中所有方法的参数 - 从发生异常的方法开始。我修改了问题,以使更明显,我想看到所有方法的参数。 - reducing activity
@MateuszKonieczny:我想领取那个赏金,但我担心你想做的事情实际上是不可能的。 - Sergio Tulentsev
@MateuszKonieczny:是的,我本来想发表这个观点的,但我唯一能想到的理由是“因为这就是[所有地方]的做法”。尝试在 Ruby 源代码中寻找可能的解决方案,但结果超出了我的薪资等级。 :) - Sergio Tulentsev
显示剩余3条评论
4个回答

5
我没有完整的解决方案但是... 但你可以通过Ruby核心库中的TracePoint类,在受控环境下获取所有调用方法的参数。
看一下这个例子:
trace = TracePoint.new(:call) do |tp|
  puts "===================== #{tp.method_id}"
  b_self = tp.binding.eval('self')
  names = b_self.method(tp.method_id).parameters.map(&:last)
  values = names.map { |name| tp.binding.eval(name.to_s) }
  p names.zip(values)
end

trace.enable

def method_a(p1, p2, p3)
end

method_a(1, "foobar", false)

#=> ===================== method_a
#=> [[:p1, 1], [:p2, "foobar"], [:p3, false]]

0

为了打印异常回溯,Ruby使用C函数exc_backtraceerror.c(exc_backtrace on github)中获取。除非您使用所需的功能修补Ruby,否则我认为没有办法更改异常回溯输出。

这里是一个有用的片段(trace.rb):

set_trace_func -> (event, file, line, id, binding, classname) do
  if event == 'call' && meth = binding.eval('__method__')
    params = binding.method(meth).parameters.select{|e| e[0] != :block}
    values = params.map{|_, var| [var, binding.local_variable_get(var)]}
    printf "%8s %s:%-2d %15s %8s %s\n", event, file, line, id, classname, values.inspect
  else
    printf "%8s %s:%-2d %15s %8s\n", event, file, line, id, classname
  end
end


def foo(a,b = 0)
  bar(a, foo: true)
end

def bar(c, d = {})
  puts "!!!buz!!!\n"
end

foo('lol')

该代码片段的输出为:

c-return /path/to/trace.rb:1   set_trace_func   Kernel
    line /path/to/trace.rb:12
  c-call /path/to/trace.rb:12    method_added   Module
c-return /path/to/trace.rb:12    method_added   Module
    line /path/to/trace.rb:16
  c-call /path/to/trace.rb:16    method_added   Module
c-return /path/to/trace.rb:16    method_added   Module
    line /path/to/trace.rb:20
    call /path/to/trace.rb:12             foo   Object [[:a, "lol"], [:b, 0]]
    line /path/to/trace.rb:13             foo   Object
    call /path/to/trace.rb:16             bar   Object [[:c, "lol"], [:d, {:foo=>true}]]
    line /path/to/trace.rb:17             bar   Object
  c-call /path/to/trace.rb:17            puts   Kernel
  c-call /path/to/trace.rb:17            puts       IO
  c-call /path/to/trace.rb:17           write       IO
!!!buz!!!
c-return /path/to/trace.rb:17           write       IO
c-return /path/to/trace.rb:17            puts       IO
c-return /path/to/trace.rb:17            puts   Kernel
  return /path/to/trace.rb:18             bar   Object
  return /path/to/trace.rb:14             foo   Object

我希望这能像对我一样对你有所帮助。


0
MAX_STACK_SIZE = 200
tracer = proc do |event|
  if event == 'call' && caller_locations.length > MAX_STACK_SIZE
    fail "Probable Stack Overflow"
  end
end
set_trace_func(tracer)

0
我认为这是可能的。下面的代码并不完美,需要一些额外的工作,但它捕获了带有参数值的堆栈跟踪的主要思想。请注意,为了知道调用站点,我将原始堆栈跟踪与由跟踪函数捕获的入口站点压缩在一起。为了区分这些条目,我分别使用“>”和“<”。
class Reporting
  def self.info(arg1)
    puts "*** #{arg1} ***"
  end
end


def read_byte(arg1)
  Reporting.info(arg1)
  raise Exception.new("File not found")
end

def read_input(arg1)
  read_byte(arg1)
end

def main(arg1)
  read_input(arg1)
end


class BetterStacktrace
  def self.enable
    set_trace_func -> (event, file, line, id, binding, classname) do
      case event
      when 'call'
        receiver_type = binding.eval('self.class')
        if receiver_type == Object
          meth = binding.eval('__method__')
          params = binding.method(meth).parameters.select{|e| e[0] != :block}
          values = params.map{|_, var| [var, binding.local_variable_get(var)]}
          self.push(event, file, line, id, classname, values)
        else
          self.push(event, file, line, id, classname)
        end
      when 'return'
        self.pop
      when 'raise'
        self.push(event, file, line, id, classname)
        Thread.current[:_keep_stacktrace] = true
      end
    end
  end

  def self.push(event, file, line, id, classname, values=nil)
    Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
    unless Thread.current[:_keep_stacktrace]
      if values
        values_msg = values.map(&:last).join(", ")
        msg = "%s:%d:in `%s(%s)'" % [file, line, id, values_msg]
      else
        msg = "%s:%d:in `%s'" % [file, line, id]
      end
      Thread.current[:_saved_stacktrace] << msg
    end
  end

  def self.pop()
    Thread.current[:_saved_stacktrace] = [] unless Thread.current.key?(:_saved_stacktrace)
    unless Thread.current[:_keep_stacktrace]
      value = Thread.current[:_saved_stacktrace].pop
    end
  end

  def self.disable
    set_trace_func nil
  end

  def self.print_stacktrace(calls)
    enters = Thread.current[:_saved_stacktrace].reverse
    calls.zip(enters).each do |call, enter|
      STDERR.puts "> #{enter}"
      STDERR.puts "< #{call}"
    end
    Thread.current[:_saved_stacktrace] = []
  end
end

BetterStacktrace.enable

begin
  main(10)
rescue Exception => ex
  puts "--- Catched ---"
  puts ex
  BetterStacktrace.print_stacktrace(ex.backtrace)
end


BetterStacktrace.disable
begin
  main(10)
rescue Exception
  puts "--- Catched ---"
  puts ex
  puts ex.backtrace
end

以上代码的输出如下:
*** 10 ***
--- Catched ---
File not found
> work/tracing_with_params.rb:10:in `read_byte'
< work/tracing_with_params.rb:10:in `read_byte'
> work/tracing_with_params.rb:8:in `read_byte(10)'
< work/tracing_with_params.rb:14:in `read_input'
> work/tracing_with_params.rb:13:in `read_input(10)'
< work/tracing_with_params.rb:18:in `main'
> work/tracing_with_params.rb:17:in `main(10)'
< work/tracing_with_params.rb:82:in `<main>'
*** 10 ***
--- Catched ---
File not found
work/tracing_with_params.rb:10:in `read_byte'
work/tracing_with_params.rb:14:in `read_input'
work/tracing_with_params.rb:18:in `main'
work/tracing_with_params.rb:82:in `<main>'

编辑:

类函数的调用未被记录。必须修复此问题,以使堆栈跟踪打印函数不会产生无效输出。 此外,我使用 STDERR 作为输出,以便轻松获取其中一个输出。如果您愿意,可以更改它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接