Class: RightSupport::Notifier::Utility::BacktraceDecoder

Inherits:
Object
  • Object
show all
Defined in:
lib/right_support/notifiers/utilities/backtrace_decoder.rb

Overview

decoder for ruby-formatted backtraces.

Defined Under Namespace

Classes: Frame

Constant Summary collapse

DEFAULT_BACKTRACE_LIMIT =

default limit on size of decoded stack trace arrays.

10
MAX_BACKTRACE_LIMIT =

hard limit on size of decoded stack trace arrays.

255
WALK_LIMIT =

limit on error walk when following cause chain.

32
BACKTRACE_REGEXP =

regular expression used to decode a line of ruby backtrace.

/^(.*):(\d+)?:in `(.*)'$/
ELLIPSIS =

ellipsis used for limited trace.

'...'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ BacktraceDecoder

Returns a new instance of BacktraceDecoder.

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :backtrace_offset (Integer)

    as number of stack frames to skip initially. default = 0.

  • :backtrace_limit (Integer)

    as maximum number of stack frames to decode or negative for all. default = 10.

  • :path_blacklist (String|Array)

    for unwanted paths in backtrace. the blacklisted path substring causes the frame to be omitted when it appears anywhere in the traced path. default = none.

  • :root_path (String)

    for application to be removed from decoded file paths. default = <working directory>



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 68

def initialize(options = {})
  options = {
    backtrace_offset: 0,
    backtrace_limit: DEFAULT_BACKTRACE_LIMIT,
    path_blacklist: nil
  }.merge(options)
  @backtrace_offset = Integer(options[:backtrace_offset])
  @backtrace_limit = Integer(options[:backtrace_limit])
  if @backtrace_limit < 0 || @backtrace_limit > MAX_BACKTRACE_LIMIT
    @backtrace_limit = MAX_BACKTRACE_LIMIT
  end
  @path_blacklist = Array(options[:path_blacklist])

  # resolve current application root path.
  @root_path = ::File.expand_path(options[:root_path] || ::Dir.pwd) + '/'
  @root_parent_path = ::File.dirname(@root_path) + '/'

  # resolve current 'lib/ruby' path.
  @ruby_lib_path = ::File.expand_path(::RbConfig::CONFIG['rubylibprefix']) + '/'
  @ruby_lib_parent_path = ::File.dirname(@ruby_lib_path) + '/'
end

Instance Attribute Details

#backtrace_limitObject (readonly)

Returns the value of attribute backtrace_limit.



41
42
43
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 41

def backtrace_limit
  @backtrace_limit
end

#backtrace_offsetObject (readonly)

Returns the value of attribute backtrace_offset.



41
42
43
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 41

def backtrace_offset
  @backtrace_offset
end

#path_blacklistObject (readonly)

Returns the value of attribute path_blacklist.



41
42
43
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 41

def path_blacklist
  @path_blacklist
end

#root_pathObject (readonly)

Returns the value of attribute root_path.



41
42
43
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 41

def root_path
  @root_path
end

Class Method Details

.format_frames(frames) ⇒ String

formats the given frames for display on the console.

Parameters:

  • frames (Array)

    to format

Returns:

  • (String)

    formatted frames



243
244
245
246
247
248
249
250
251
252
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 243

def self.format_frames(frames)
  frames.map do |frame|
    if frame.function == ELLIPSIS
      line = ELLIPSIS
    else
      line = frame
    end
    "  #{line}"  # indent and .to_s
  end.join("\n")
end

Instance Method Details

#callerArray

Returns the current frames for the caller with limit, filters, etc.

Returns:

  • (Array)

    the current frames for the caller with limit, filters, etc.



188
189
190
191
192
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 188

def caller
  # note that Kernel.caller always omits the immediate frame so that the
  # caller of this method is at the top of the trace.
  decode(::Kernel.caller)
end

#decode(trace) ⇒ Array

decodes lines of backtrace in string form into their component parts.

borrowed originally from:

Parameters:

  • trace (Array)

    from error.backtrace or Kernel.caller or empty or nil

Returns:

  • (Array)

    frames of decoded trace or empty

See Also:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 98

def decode(trace)
  frames = []
  trace ||= []
  trace = trace[@backtrace_offset..-1] if @backtrace_offset > 0
  seen_root_path = false
  trace.each do |t|
    has_root_path = false
    omit = false
    file = nil
    line = 0
    function = nil
    if m = BACKTRACE_REGEXP.match(t)
      file = m[1]
      line = Integer(m[2])
      function = m[3]
      @path_blacklist.each do |pbl|
        if file.include?(pbl)
          omit = true
          break
        end
      end
      unless omit
        # remove base path from the frame file path for simplicity and because
        # the absolute root path is only meaningful on the file system where
        # the code is running. the root path basename (i.e. the application
        # directory name) is kept for display.
        if file.start_with?(@root_path)
          file = file[@root_parent_path.length..-1]
          has_root_path = true
        elsif file.start_with?(@ruby_lib_path)
          # remove absoluteness of the lib/ruby path for the same reason.
          file = file[@ruby_lib_parent_path.length..-1]
        end

        # remove everything before '/gems/' on the assumption that the
        # rubygems or '/vendor/bundle/.../gems/' prefixes are only noise that
        # makes the trace harder to read for a human. '/gems/' may also appear
        # more than once so use the last found.
        #
        # also note that vendored gitted gems and their binstubs can appear
        # directly under the '<app root>/vendor/bundle|cache/' directory and
        # so are not explicitly under a '/gems/' directory.
        founder = nil
        founder_offset = nil
        %w(
          /gems/
          /vendor/cache/
          /vendor/bundle/
        ).each do |finder|
          if founder_offset = file.rindex(finder)
            founder = finder
            break
          end
        end
        if founder
          file = file[founder_offset + founder.length..-1]

          # note that vendored gems are also technically on the 'root path'
          # but we do not want to include them in the 'seen root path'
          # logic because we want to see the trace back to real app code.
          has_root_path = false
        end
      end
    else
      # show a failure message for other patterns such as java plugins for
      # ruby and nonsense only because we have no known use cases.
      # FIX: support any needed patterns.
      file = t
      function = '<< trace decoder error >>'
    end
    unless omit
      # enforce the backtrace limit when configured *unless* we have not yet
      # seen the root path (i.e. the application root). in this case we want
      # to keep walking the backtrace until we have shown at least one frame
      # that references the application. otherwise the backtrace may not be
      # useful for debugging purposes.
      done = seen_root_path && @backtrace_limit >= 0 && frames.size >= @backtrace_limit

      # show at least one full application frame before appending ellipsis.
      seen_root_path ||= has_root_path

      # set an ellipsis as function name of last frame when over limit.
      frames << Frame.new(file, line, done ? ELLIPSIS : function)
      break if done
    end
  end
  frames
end

#walk_error(error, options = {}) {|cause| ... } ⇒ Array

walks the error to find the backtrace closest to the root cause.

borrowed originally from:

Parameters:

  • error (Exception)

    to walk

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :raw_trace (TrueClass|FalseClass)

    true to return the raw backtrace that was closest to cause, false to decode it (default).

Yields:

  • (cause)

    yields each walked error without decoding its backtrace.

Yield Parameters:

  • error (Exception)

    in cause chain that is currently being walked.

Returns:

  • (Array)

    tuple of [cause, trace_or_frames]

See Also:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/right_support/notifiers/utilities/backtrace_decoder.rb', line 208

def walk_error(error, options={}, &callback)
  options = {
    raw_trace: false
  }.merge(options)
  cause = error
  trace = cause.backtrace || ::Kernel.caller[1..-1]
  callback.call(cause) if callback
  counter = 0
  while cause.respond_to?(:cause)
    next_cause = cause.cause
    if next_cause && next_cause != cause
      cause = next_cause
      callback.call(cause) if callback
      trace = cause.backtrace if cause.backtrace

      # sanity check that the error is wrapped an unbelievable number of
      # times or that the cause != next_cause logic has failed somehow; no
      # infinite loops.
      counter += 1
      break if counter >= WALK_LIMIT
    else
      break
    end
  end
  unless options[:raw_trace]
    trace = decode(trace)
  end
  [cause, trace]
end