program story

Ruby에서 예외 메시지에 정보를 어떻게 추가합니까?

inputbox 2021. 1. 6. 08:18
반응형

Ruby에서 예외 메시지에 정보를 어떻게 추가합니까?


Ruby에서 클래스를 변경하지 않고 예외 메시지에 정보를 추가하려면 어떻게해야합니까?

현재 사용하고있는 접근 방식은

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.class, "Problem with string number #{i}: #{$!}"
  end
end

이상적으로는 역 추적도 보존하고 싶습니다.

더 좋은 방법이 있습니까?


예외 클래스와 역 추적을 유지하면서 예외를 다시 발생시키고 메시지를 수정하려면 다음을 수행하십시오.

strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue Exception => e
    raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace
  end
end

결과는 다음과 같습니다.

# RuntimeError: Problem with string number 0: Original error message here
#     backtrace...

그다지 좋지는 않지만 새 메시지로 예외를 다시 발생시킬 수 있습니다.

raise $!, "Problem with string number #{i}: #{$!}"

exception메서드를 사용하여 수정 된 예외 개체를 직접 가져올 수도 있습니다 .

new_exception = $!.exception "Problem with string number #{i}: #{$!}"
raise new_exception

다른 방법이 있습니다.

class Exception
  def with_extra_message extra
    exception "#{message} - #{extra}"
  end
end

begin
  1/0
rescue => e
  raise e.with_extra_message "you fool"
end

# raises an exception "ZeroDivisionError: divided by 0 - you fool" with original backtrace

( exception내부적으로 메서드 를 사용하도록 수정되었습니다. @Chuck에게 감사드립니다)


내 접근하는 것 오류의 확장 익명 모듈과 D 오류 방법 :extendrescuemessage

def make_extended_message(msg)
    Module.new do
      @@msg = msg
      def message
        super + @@msg
      end
    end
end

begin
  begin
      raise "this is a test"
  rescue
      raise($!.extend(make_extended_message(" that has been extended")))
  end
rescue
    puts $! # just says "this is a test"
    puts $!.message # says extended message
end

이렇게하면 예외의 다른 정보 (예 :)를 방해하지 않습니다 backtrace.


나는 Ryan Heneise의 대답이 받아 들여 져야한다고 투표했습니다 .

이것은 복잡한 응용 프로그램에서 일반적인 문제이며 원래 역 추적을 보존하는 것이 종종 매우 중요하므로 ErrorHandling이를위한 도우미 모듈에 유틸리티 메서드가 있습니다 .

우리가 발견 한 문제 중 하나는 시스템이 엉망인 상태 일 때 더 의미있는 메시지를 생성하려고하면 예외 처리기 자체에서 예외가 생성되어 다음과 같이 유틸리티 기능이 강화된다는 점입니다.

def raise_with_new_message(*args)
  ex = args.first.kind_of?(Exception) ? args.shift : $!
  msg = begin
    sprintf args.shift, *args
  rescue Exception => e
    "internal error modifying exception message for #{ex}: #{e}"
  end
  raise ex, msg, ex.backtrace
end

일이 잘 될 때

begin
  1/0
rescue => e
  raise_with_new_message "error dividing %d by %d: %s", 1, 0, e
end

멋지게 수정 된 메시지를받습니다.

ZeroDivisionError: error dividing 1 by 0: divided by 0
    from (irb):19:in `/'
    from (irb):19
    from /Users/sim/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'

상황이 나빠질 때

begin
  1/0
rescue => e
  # Oops, not passing enough arguments here...
  raise_with_new_message "error dividing %d by %d: %s", e
end

당신은 여전히 ​​큰 그림을 잃지 않습니다

ZeroDivisionError: internal error modifying exception message for divided by 0: can't convert ZeroDivisionError into Integer
    from (irb):25:in `/'
    from (irb):25
    from /Users/sim/.rvm/rubies/ruby-2.0.0-p247/bin/irb:16:in `<main>'

나는이 파티에 6 년 늦었다는 것을 알고 있지만 ... 나는 이번 주까지 Ruby 오류 처리를 이해하고이 질문을 만났다. 답변은 유용하지만이 스레드의 미래 독자에게 유용 할 수있는 명확하지 않은 (그리고 문서화되지 않은) 동작이 있습니다. 모든 코드는 루비 v2.3.1에서 실행되었습니다.

@Andrew Grimm이 묻습니다.

Ruby에서 클래스를 변경하지 않고 예외 메시지에 정보를 추가하려면 어떻게해야합니까?

그런 다음 샘플 코드를 제공합니다.

raise $!.class, "Problem with string number #{i}: #{$!}"

나는 이것이 원래 오류 인스턴스 객체 에 정보 추가하지 않고 대신 동일한 클래스를 가진 NEW 오류 객체를 발생 시킨다는 점을 지적 하는 것이 중요 하다고 생각합니다 .

@BoosterStage라고

예외를 다시 발생시키고 메시지를 수정하려면 ...

그러나 다시 제공된 코드

raise $!, "Problem with string number #{i}: #{$!}", $!.backtrace

$!가 참조하는 오류 클래스의 새 인스턴스를 발생 시키지만 $! 와 똑같은 인스턴스 아닙니다 .

@Andrew Grimm의 코드와 @BoosterStage의 예제의 차이점 #raise은 첫 번째 경우에 대한 첫 번째 인수가 Class이고 두 번째 경우에는 일부 (아마도) 인스턴스라는 사실입니다 StandardError. Kernel # raise에 대한 문서에 다음과 같이 나와 있기 때문에 차이가 중요합니다 .

단일 String 인수를 사용하여 문자열을 메시지로 사용하여 RuntimeError를 발생시킵니다. 그렇지 않으면 첫 번째 매개 변수는 Exception 클래스 (또는 예외 메시지를 보낼 때 Exception 개체를 반환하는 개체)의 이름이어야합니다.

하나의 인수가 주어지고이 오류 객체 인스턴스 인 경우, 그 객체가 될 raise거라고 경우 해당 개체의 #exception메소드 상속 또는 구현 기본 동작에 정의 된 예외 번호 예외 (문자열) :

인수가 없거나 인수가 수신자와 동일한 경우 수신자를 반환합니다. 그렇지 않으면 수신자와 동일한 클래스의 새 예외 객체를 생성하지만 메시지는 string.to_str과 같습니다.

많은 사람들이 추측 하듯이 :

...
catch StandardError => e
  raise $!
...

$!가 참조하는 동일한 오류를 발생시킵니다.

...
catch StandardError => e
  raise
...

그러나 아마도 생각할 수있는 이유 때문일 것입니다. 이 경우, 호출 할 수 raise있다 NOT 단지 객체를 제기 $!은 결과 제기 ... $!.exception(nil)이 경우에 될 일이, $!.

이 동작을 명확히하기 위해 다음 장난감 코드를 고려하십시오.

      class TestError < StandardError
        def initialize(message=nil)
          puts 'initialize'
          super
        end
        def exception(message=nil)
          puts 'exception'
          return self if message.nil? || message == self
          super
        end
      end

실행 (위에서 인용 한 @Andrew Grimm의 샘플과 동일) :

2.3.1 :071 > begin ; raise TestError, 'message' ; rescue => e ; puts e ; end

결과 :

initialize
message

따라서 TestError는 initialized, rescued이고 메시지가 인쇄되었습니다. 여태까지는 그런대로 잘됐다. 두 번째 테스트 (위에 인용 된 @BoosterStage의 샘플과 유사) :

2.3.1 :073 > begin ; raise TestError.new('foo'), 'bar' ; rescue => e ; puts e ; end

다소 놀라운 결과 :

initialize
exception
bar

A는 그래서 TestErrorinitialize후 '갑'과 거라고하지만, #raise호출 한 #exception첫 번째 인수 (의 인스턴스 TestError)와 '바'의 메시지 전달 의 두 번째 인스턴스를 생성하는 TestError궁극적 제기됩니다 것입니다 .

TIL.

또한, @Sim처럼, 내가하고 매우 어떤 원래 역 추적 컨텍스트를 유지하지만, 대신 자신과 같은 사용자 지정 오류 처리기를 구현하는 방법에 대한 우려 raise_with_new_message, 루비는 Exception#cause내 뒤를 가지고 : 나는 오류를 잡을 할 때마다, 도메인 특정 오류에 싸서 그런 다음 오류 발생 #cause시키면 도메인 별 오류가 발생 하면 원래 역 추적을 사용할 수 있습니다 .

이 모든 것의 요점은 @Andrew Grimm처럼 더 많은 맥락에서 오류를 제기하고 싶다는 것입니다. 특히 네트워크 관련 오류 모드가 많을 수있는 내 앱의 특정 지점에서만 도메인 관련 오류를 발생시키고 싶습니다. 그런 다음 내 앱의 최상위 수준에서 도메인 오류를 처리하기 위해 내 오류보고를 수행 할 수 있으며 #cause"근본 원인"에 도달 할 때까지 재귀 적 으로 호출하여 로깅 /보고에 필요한 모든 컨텍스트를 얻을 수 있습니다.

다음과 같이 사용합니다.

class BaseDomainError < StandardError
  attr_reader :extra
  def initialize(message = nil, extra = nil)
    super(message)
    @extra = extra
  end
end
class ServerDomainError < BaseDomainError; end

그런 다음 Faraday와 같은 것을 사용하여 원격 REST 서비스를 호출하는 경우 가능한 모든 오류를 도메인 별 오류로 래핑하고 추가 정보를 전달할 수 있습니다 (이 스레드의 원래 질문이라고 생각합니다).

class ServiceX
  def initialize(foo)
    @foo = foo
  end
  def get_data(args)
    begin
      # This method is not defined and calling it will raise an error
      make_network_call_to_service_x(args)
    rescue StandardError => e
      raise ServerDomainError.new('error calling service x', binding)
    end
  end
end

Yeah, that's right: I literally just realized I can set the extra info to the current binding to grab all local vars defined at the time the ServerDomainError is instantiated/raised. This test code:

begin
  ServiceX.new(:bar).get_data(a: 1, b: 2)
rescue
  puts $!.extra.receiver
  puts $!.extra.local_variables.join(', ')
  puts $!.extra.local_variable_get(:args)
  puts $!.extra.local_variable_get(:e)
  puts eval('self.instance_variables', $!.extra)
  puts eval('self.instance_variable_get(:@foo)', $!.extra)
end

will output:

exception
#<ServiceX:0x00007f9b10c9ef48>
args, e
{:a=>1, :b=>2}
undefined method `make_network_call_to_service_x' for #<ServiceX:0x00007f9b10c9ef48 @foo=:bar>
@foo
bar

Now a Rails controller calling ServiceX doesn't particularly need to know that ServiceX is using Faraday (or gRPC, or anything else), it just makes the call and handles BaseDomainError. Again: for logging purposes, a single handler at the top level can recursively log all the #causes of any caught errors, and for any BaseDomainError instances in the error chain it can also log the extra values, potentially including the local variables pulled from the encapsulated binding(s).

I hope this tour has been as useful for others as it was for me. I learned a lot.

UPDATE: Skiptrace looks like it adds the bindings to Ruby errors.

Also, see this other post for info about how the implementation of Exception#exception will clone the object (copying instance variables).


Here's what I ended up doing:

Exception.class_eval do
  def prepend_message(message)
    mod = Module.new do
      define_method :to_s do
        message + super()
      end
    end
    self.extend mod
  end

  def append_message(message)
    mod = Module.new do
      define_method :to_s do
        super() + message
      end
    end
    self.extend mod
  end
end

Examples:

strings = %w[a b c]
strings.each_with_index do |string, i|
  begin
    do_risky_operation(string)
  rescue
    raise $!.prepend_message "Problem with string number #{i}:"
  end
end
=> NoMethodError: Problem with string number 0:undefined method `do_risky_operation' for main:Object

and:

pry(main)> exception = 0/0 rescue $!
=> #<ZeroDivisionError: divided by 0>
pry(main)> exception = exception.append_message('. With additional info!')
=> #<ZeroDivisionError: divided by 0. With additional info!>
pry(main)> exception.message
=> "divided by 0. With additional info!"
pry(main)> exception.to_s
=> "divided by 0. With additional info!"
pry(main)> exception.inspect
=> "#<ZeroDivisionError: divided by 0. With additional info!>"

This is similar to Mark Rushakoff's answer but:

  1. Overrides to_s instead of message since by default message is defined as simply to_s (at least in Ruby 2.0 and 2.2 where I tested it)
  2. Calls extend for you instead of making the caller do that extra step.
  3. Uses define_method and a closure so that the local variable message can be referenced. When I tried using a class variable @@message, it warned, "warning: class variable access from toplevel" (See this question: "Since you're not creating a class with the class keyword, your class variable is being set on Object, not [your anonymous module]")

Features:

  • Easy to use
  • Reuses the same object (instead of creating a new instance of the class), so things like object identity, class, and backtrace are preserved
  • to_s, message, and inspect all respond appropriately
  • Can be used with an exception that is already stored in a variable; doesn't require you to re-raise anything (like the solution that involved passing the backtrace to raise: raise $!, …, $!.backtrace). This was important to me since the exception was passed in to my logging method, not something I had rescued myself.

ReferenceURL : https://stackoverflow.com/questions/2823748/how-do-i-add-information-to-an-exception-message-in-ruby

반응형