Module: Failbot

Extended by:
Failbot, Compat
Included in:
Failbot
Defined in:
lib/failbot.rb,
lib/failbot/compat.rb,
lib/failbot/version.rb,
lib/failbot/haystack.rb,
lib/failbot/exit_hook.rb,
lib/failbot/middleware.rb,
lib/failbot/file_backend.rb,
lib/failbot/http_backend.rb,
lib/failbot/json_backend.rb,
lib/failbot/heroku_backend.rb,
lib/failbot/memory_backend.rb

Overview

This file exists so that the unhandled exception hook may easily be injected into programs that don’t register it themselves. It also provides a lightweight failbot interface that doesn’t bring in any other libraries until a report is made, which is useful for environments where boot time is important.

To use, set RUBYOPT or pass an -r argument to ruby:

RUBYOPT=rfailbot/exit_hook some-program.rb

Or:

ruby -rfailbot/exit_hook some-program.rb

Your program can also require this library instead of ‘failbot’ to minimize the amount of up-front processing required and automatically install the exit hook.

require 'failbot/exit_hook'

The ‘failbot’ lib is loaded in full the first time an actual report is made.

Defined Under Namespace

Modules: Compat Classes: FileBackend, HTTPBackend, Haystack, HerokuBackend, JSONBackend, MemoryBackend, Rescuer

Constant Summary collapse

VERSION =
"1.1.2"

Instance Attribute Summary collapse

Attributes included from Compat

#backend

Instance Method Summary collapse

Methods included from Compat

backend!, backend_name, cast, config, config_file, default_options, default_options=, environment, fail, haystack, raise_errors=, raise_errors?, report_errors=, report_errors?, setup_deprecated

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

Tap into any other method invocation on the Failbot module (especially report) and lazy load and configure everything the first time.



75
76
77
78
79
# File 'lib/failbot/exit_hook.rb', line 75

def method_missing(method, *args, &block)
  return super if @failbot_loaded
  require 'failbot'
  send(method, *args, &block)
end

Instance Attribute Details

#already_reportingObject

prevent recursive calls to Failbot.report!



44
45
46
# File 'lib/failbot.rb', line 44

def already_reporting
  @already_reporting
end

#instrumenterObject

Public: Set an instrumenter to be called when exceptions are reported.

class CustomInstrumenter
  def instrument(name, payload = {})
    warn "Exception: #{payload["class"]}\n#{payload.inspect}"
  end
end

Failbot.instrumenter = CustomInstrumenter

The instrumenter must conform to the ‘ActiveSupport::Notifications` interface, which defines `#instrument` and accepts:

name - the String name of the event (e.g. “report.failbot”) payload - a Hash of the exception context.



41
42
43
# File 'lib/failbot.rb', line 41

def instrumenter
  @instrumenter
end

Instance Method Details

#contextObject

Stack of context information to include in the next failbot report. These hashes are condensed down into one and included in the next report. Don’t mess with this structure directly - use the #push and #pop methods.



101
102
103
# File 'lib/failbot.rb', line 101

def context
  @context ||= [{'server' => hostname}]
end

#disable(&block) ⇒ Object

Public: Disable exception reporting. This is equivalent to calling ‘Failbot.setup(“FAILBOT_REPORT” => 0)`, but can be called after setup.

Failbot.disable do
  something_that_might_go_kaboom
end

block - an optional block to perform while reporting is disabled. If a block

is passed, reporting will be re-enabled after the block is called.


189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/failbot.rb', line 189

def disable(&block)
  original_report_errors = @report_errors
  @report_errors = false

  if block
    begin
      block.call
    ensure
      @report_errors = original_report_errors
    end
  end
end

#enableObject

Public: Enable exception reporting. Reporting is enabled by default, but this can be called if it is explicitly disabled by calling ‘Failbot.disable` or setting `FAILBOT_REPORTING => “0”` in `Failbot.setup`.



205
206
207
# File 'lib/failbot.rb', line 205

def enable
  @report_errors = true
end

#exception_info(e) ⇒ Object

Extract exception info into a simple Hash.

e - The exception object to turn into a Hash.

Returns a Hash including a ‘class’, ‘message’, ‘backtrace’, and ‘rollup’

keys. The rollup value is a MD5 hash of the exception class, file, and line
number and is used to group exceptions.


256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/failbot.rb', line 256

def exception_info(e)
  backtrace = Array(e.backtrace)[0, 500]

  res = {
    'class'      => e.class.to_s,
    'message'    => e.message,
    'backtrace'  => backtrace.join("\n"),
    'ruby'       => RUBY_DESCRIPTION,
    'rollup'     => Digest::MD5.hexdigest("#{e.class}#{backtrace[0]}"),
    'created_at' => Time.now.utc.iso8601(6)
  }

  if exception_context = (e.respond_to?(:failbot_context) && e.failbot_context)
    res.merge!(exception_context)
  end

  if original = (e.respond_to?(:original_exception) && e.original_exception)
    remote_backtrace  = []
    remote_backtrace << original.message
    if original.backtrace
      remote_backtrace.concat(Array(original.backtrace)[0,500])
    end
    res['remote_backtrace'] = remote_backtrace.join("\n")
  end

  res
end

#hostnameObject



292
293
294
# File 'lib/failbot.rb', line 292

def hostname
  @hostname ||= Socket.gethostname
end

#install_unhandled_exception_hook!Object

Installs an at_exit hook to report exceptions that raise all the way out of the stack and halt the interpreter. This is useful for catching boot time errors as well and even signal kills.

To use, call this method very early during the program’s boot to cover as much code as possible:

require 'failbot'
Failbot.install_unhandled_exception_hook!

Returns true when the hook was installed, nil when the hook had previously been installed by another component.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/failbot/exit_hook.rb', line 51

def install_unhandled_exception_hook!
  # only install the hook once, even when called from multiple locations
  return if @unhandled_exception_hook_installed

  # the $! is set when the interpreter is exiting due to an exception
  at_exit do
    boom = $!
    if boom && !@raise_errors && !boom.is_a?(SystemExit)
      report(boom, 'argv' => ([$0]+ARGV).join(" "), 'halting' => true)
    end
  end

  @unhandled_exception_hook_installed = true
end

#loggerObject



284
285
286
# File 'lib/failbot.rb', line 284

def logger
  @logger ||= Logger.new($stderr)
end

#logger=(logger) ⇒ Object



288
289
290
# File 'lib/failbot.rb', line 288

def logger=(logger)
  @logger = logger
end

#popObject

Remove the last info hash from the context stack.



120
121
122
# File 'lib/failbot.rb', line 120

def pop
  context.pop if context.size > 1
end

#push(info = {}) ⇒ Object

Add info to be sent in the next failbot report, should one occur.

info - Hash of name => value pairs to include in the exception report. block - When given, the info is removed from the current context after the

block is executed.

Returns the value returned by the block when given; otherwise, returns nil.



112
113
114
115
116
117
# File 'lib/failbot.rb', line 112

def push(info={})
  context.push(info)
  yield if block_given?
ensure
  pop if block_given?
end

#report(e, other = {}) ⇒ Object

Public: Sends an exception to the exception tracking service along with a hash of custom attributes to be included with the report. When the raise_errors option is set, this method raises the exception instead of reporting to the exception tracking service.

e - The Exception object. Must respond to #message and #backtrace. other - Hash of additional attributes to include with the report.

Examples

begin
  my_code
rescue => e
  Failbot.report(e, :user => current_user)
end

Returns nothing.



146
147
148
149
150
151
152
153
# File 'lib/failbot.rb', line 146

def report(e, other = {})
  if @raise_errors
    squash_context(exception_info(e), other) # surface problems squashing
    raise e
  else
    report!(e, other)
  end
end

#report!(e, other = {}) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/failbot.rb', line 155

def report!(e, other = {})
  return unless @report_errors
  data = squash_context(exception_info(e), other)

  if already_reporting
    logger.warn "FAILBOT: asked to report while reporting! #{data.inspect}" rescue nil
    logger.warn e.message rescue nil
    logger.warn e.backtrace.join("\n") rescue nil
    return
  end

  self.already_reporting = true
  backend.report(data)
  instrument("report.failbot", data)
rescue Object => i
  # don't fail for any reason
  logger.debug "FAILBOT: #{data.inspect}" rescue nil
  logger.debug e.message rescue nil
  logger.debug e.backtrace.join("\n") rescue nil
  logger.debug i.message rescue nil
  logger.debug i.backtrace.join("\n") rescue nil
ensure
  self.already_reporting = false
end

#reportsObject

Public: exceptions that were reported. Only available when using the memory and file backends.

Returns an Array of exceptions data Hash.



213
214
215
# File 'lib/failbot.rb', line 213

def reports
  backend.reports
end

#reset!Object

Reset the context stack to a pristine state.



125
126
127
# File 'lib/failbot.rb', line 125

def reset!
  @context = [context[0]]
end

#setup(settings = {}, default_context = {}) ⇒ Object

Shim into Failbot.setup and store config information off for the first time a real method is invoked.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/failbot.rb', line 55

def setup(settings={}, default_context={})
  deprecated_settings = %w[
    backend host port haystack
    raise_errors
  ]

  if settings.empty? ||
    settings.keys.any? { |key| deprecated_settings.include?(key) }
    warn "%s Deprecated Failbot.setup usage. See %s for details." % [
      caller[0], "https://github.com/github/failbot"
    ]
    return setup_deprecated(settings)
  end

  if default_context.respond_to?(:to_hash) && !default_context.to_hash.empty?
    context[0] = default_context.to_hash
  end

  self.backend =
    case (name = settings["FAILBOT_BACKEND"])
    when "memory"
      Failbot::MemoryBackend.new
    when "file"
      Failbot::FileBackend.new(settings["FAILBOT_BACKEND_FILE_PATH"])
    when "http"
      Failbot::HTTPBackend.new(URI(settings["FAILBOT_HAYSTACK_URL"]))
    when 'json'
      Failbot::JSONBackend.new(settings["FAILBOT_BACKEND_JSON_HOST"], settings["FAILBOT_BACKEND_JSON_PORT"])
    else
      raise ArgumentError, "Unknown backend: #{name.inspect}"
    end

  @raise_errors  = !settings["FAILBOT_RAISE"].to_s.empty?
  @report_errors = settings["FAILBOT_REPORT"] != "0"

  # allows overriding the 'app' value to send to single haystack bucket.
  # used primarily on ghe.io.
  @app_override = settings["FAILBOT_APP_OVERRIDE"]
end

#squash_context(*other) ⇒ Object

Combines all context hashes into a single hash converting non-standard data types in values to strings, then combines the result with a custom info hash provided in the other argument.

other - Optional array of hashes to also squash in on top of the context

stack hashes.

Returns a Hash with all keys and values.



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/failbot.rb', line 225

def squash_context(*other)
  merged = {}
  (context + other).each do |hash|
    hash.each do |key, value|
      value = (value.call rescue nil) if value.kind_of?(Proc)
      merged[key.to_s] =
        case value
        when Time
          value.iso8601
        when Date
          value.strftime("%F") # equivalent to %Y-%m-%d
        when Numeric
          value
        when String, true, false
          value.to_s
        else
          value.inspect
        end
    end
  end
  merged["app"] = @app_override if @app_override
  merged
end