failbot 2.3.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/failbot.rb +37 -84
- data/lib/failbot/backtrace.rb +64 -0
- data/lib/failbot/exception_format/haystack.rb +94 -0
- data/lib/failbot/exception_format/structured.rb +48 -0
- data/lib/failbot/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72511c74e8a4c9b1dd06d9408c0c926568382e8e72cd3e3a28b7d262835e9aef
|
4
|
+
data.tar.gz: 649f8f43daedeb3044cc1457b09948b9f354db737e73e5b512a1ad2f90850684
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95a20c76e4bee8e8517065af5f3d9325cf6773bcbb6f08f6a41e479ac41359927fe0c00aedd5dbd3fe43c1259d2cce44bd809c3b5aba913f7664844555101581
|
7
|
+
data.tar.gz: 163ca9571024669fa96a6ccfca233b4f81e23e4fe1e4b80a69c837dfd618eda50b20b8a3fa50e3cb99d053cd287448b2156d58816bfd72a22c455fcd53a113d2
|
data/lib/failbot.rb
CHANGED
@@ -9,6 +9,8 @@ require "uri"
|
|
9
9
|
require 'failbot/version'
|
10
10
|
require "failbot/compat"
|
11
11
|
require "failbot/sensitive_data_scrubber"
|
12
|
+
require "failbot/exception_format/haystack"
|
13
|
+
require "failbot/exception_format/structured"
|
12
14
|
|
13
15
|
# Failbot asynchronously takes exceptions and reports them to the
|
14
16
|
# exception logger du jour. Keeps the main app from failing or lagging if
|
@@ -52,6 +54,26 @@ module Failbot
|
|
52
54
|
# stubbed methods are replaced properly.
|
53
55
|
require 'failbot/exit_hook'
|
54
56
|
|
57
|
+
EXCEPTION_DETAIL = 'exception_detail'
|
58
|
+
|
59
|
+
# Enumerates the available exception formats this gem supports. The original
|
60
|
+
# format and the default is :haystack and the newer format is :structured
|
61
|
+
EXCEPTION_FORMATS = {
|
62
|
+
:haystack => ExceptionFormat::Haystack,
|
63
|
+
:structured => ExceptionFormat::Structured
|
64
|
+
}
|
65
|
+
|
66
|
+
# Set the current exception format.
|
67
|
+
def self.exception_format=(identifier)
|
68
|
+
@exception_formatter = EXCEPTION_FORMATS.fetch(identifier) do
|
69
|
+
fail ArgumentError, "#{identifier} is not an available exception_format (want one of #{EXCEPTION_FORMATS.keys})"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Default to the original exception format used by haystack unless
|
74
|
+
# apps opt in to the newer version.
|
75
|
+
self.exception_format = :haystack
|
76
|
+
|
55
77
|
# Public: Setup the backend for reporting exceptions.
|
56
78
|
def setup(settings={}, default_context={})
|
57
79
|
deprecated_settings = %w[
|
@@ -95,6 +117,11 @@ module Failbot
|
|
95
117
|
# allows overriding the 'app' value to send to single haystack bucket.
|
96
118
|
# used primarily on ghe.io.
|
97
119
|
@app_override = settings["FAILBOT_APP_OVERRIDE"]
|
120
|
+
|
121
|
+
# Support setting exception_format from ENV/settings
|
122
|
+
if settings["FAILBOT_EXCEPTION_FORMAT"]
|
123
|
+
self.exception_format = settings["FAILBOT_EXCEPTION_FORMAT"].to_sym
|
124
|
+
end
|
98
125
|
end
|
99
126
|
|
100
127
|
# Bring in deprecated methods
|
@@ -348,6 +375,14 @@ module Failbot
|
|
348
375
|
value
|
349
376
|
when String, true, false
|
350
377
|
value.to_s
|
378
|
+
when Array
|
379
|
+
if key == EXCEPTION_DETAIL
|
380
|
+
# special-casing for the exception_detail key, which is allowed to
|
381
|
+
# be an array with a specific structure.
|
382
|
+
value
|
383
|
+
else
|
384
|
+
value.inspect
|
385
|
+
end
|
351
386
|
else
|
352
387
|
value.inspect
|
353
388
|
end
|
@@ -360,21 +395,9 @@ module Failbot
|
|
360
395
|
#
|
361
396
|
# e - The exception object to turn into a Hash.
|
362
397
|
#
|
363
|
-
# Returns a Hash
|
398
|
+
# Returns a Hash.
|
364
399
|
def exception_info(e)
|
365
|
-
|
366
|
-
|
367
|
-
res = {
|
368
|
-
'class' => e.class.to_s,
|
369
|
-
'message' => e.message,
|
370
|
-
'backtrace' => backtrace.join("\n"),
|
371
|
-
'ruby' => RUBY_DESCRIPTION,
|
372
|
-
'created_at' => Time.now.utc.iso8601(6)
|
373
|
-
}
|
374
|
-
|
375
|
-
if cause = pretty_print_cause(e)
|
376
|
-
res['cause'] = cause
|
377
|
-
end
|
400
|
+
res = @exception_formatter.call(e)
|
378
401
|
|
379
402
|
if exception_context = (e.respond_to?(:failbot_context) && e.failbot_context)
|
380
403
|
res.merge!(exception_context)
|
@@ -421,76 +444,6 @@ module Failbot
|
|
421
444
|
end
|
422
445
|
end
|
423
446
|
|
424
|
-
# We'll include this many nested Exception#cause objects in the needle
|
425
|
-
# context. We limit the number of objects to prevent excessive recursion and
|
426
|
-
# large needle contexts.
|
427
|
-
MAXIMUM_CAUSE_DEPTH = 2
|
428
|
-
|
429
|
-
# Pretty-print Exception#cause (and nested causes) for inclusion in needle
|
430
|
-
# context
|
431
|
-
#
|
432
|
-
# e - The Exception object whose #cause should be printed
|
433
|
-
# depth - Integer number of Exception#cause objects to descend into.
|
434
|
-
#
|
435
|
-
# Returns a String.
|
436
|
-
def pretty_print_cause(e, depth = MAXIMUM_CAUSE_DEPTH)
|
437
|
-
return unless depth > 0
|
438
|
-
|
439
|
-
causes = []
|
440
|
-
|
441
|
-
current = e
|
442
|
-
depth.times do
|
443
|
-
pretty_cause = pretty_print_one_cause(current)
|
444
|
-
break unless pretty_cause
|
445
|
-
causes << pretty_cause
|
446
|
-
current = current.cause
|
447
|
-
end
|
448
|
-
|
449
|
-
return if causes.empty?
|
450
|
-
|
451
|
-
result = causes.join("\n\nCAUSED BY:\n\n")
|
452
|
-
|
453
|
-
if current.respond_to?(:cause) && current.cause
|
454
|
-
result << "\n\nFurther #cause backtraces were omitted\n"
|
455
|
-
end
|
456
|
-
|
457
|
-
result
|
458
|
-
end
|
459
|
-
|
460
|
-
# Pretty-print a single Exception#cause
|
461
|
-
#
|
462
|
-
# e - The Exception object whose #cause should be printed
|
463
|
-
#
|
464
|
-
# Returns a String.
|
465
|
-
def pretty_print_one_cause(e)
|
466
|
-
return unless e.respond_to?(:cause)
|
467
|
-
cause = e.cause
|
468
|
-
return unless cause
|
469
|
-
|
470
|
-
result = "#{cause.class.name}: #{cause.message}\n"
|
471
|
-
|
472
|
-
# Find where the cause's backtrace differs from the child exception's.
|
473
|
-
backtrace = Array(e.backtrace)
|
474
|
-
cause_backtrace = Array(cause.backtrace)
|
475
|
-
index = -1
|
476
|
-
min_index = [backtrace.size, cause_backtrace.size].min * -1
|
477
|
-
just_in_case = -5000
|
478
|
-
|
479
|
-
while index > min_index && backtrace[index] == cause_backtrace[index] && index >= just_in_case
|
480
|
-
index -= 1
|
481
|
-
end
|
482
|
-
|
483
|
-
# Add on a few common frames to make it clear where the backtraces line up.
|
484
|
-
index += 3
|
485
|
-
index = -1 if index >= 0
|
486
|
-
|
487
|
-
cause_backtrace[0..index].each do |line|
|
488
|
-
result << "\t#{line}\n"
|
489
|
-
end
|
490
|
-
|
491
|
-
result
|
492
|
-
end
|
493
|
-
|
494
447
|
extend self
|
495
448
|
|
496
449
|
# If the library was lazy loaded due to failbot/exit_hook.rb and a delayed
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Failbot
|
2
|
+
# A simple parser to extract structure from ruby backtraces.
|
3
|
+
class Backtrace
|
4
|
+
# Raised when a line fails parsing.
|
5
|
+
ParseError = Class.new(StandardError)
|
6
|
+
|
7
|
+
attr_reader :frames
|
8
|
+
|
9
|
+
def self.parse(backtrace)
|
10
|
+
fail ArgumentError, "expected Array, got #{backtrace.class}" unless backtrace.is_a?(Array)
|
11
|
+
|
12
|
+
frames = backtrace.map do |frame|
|
13
|
+
Frame.parse(frame)
|
14
|
+
end
|
15
|
+
|
16
|
+
new(frames)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(frames)
|
20
|
+
@frames = frames
|
21
|
+
end
|
22
|
+
|
23
|
+
# A parsed stack frame.
|
24
|
+
class Frame
|
25
|
+
# Regex matching the components of a ruby stack frame as they
|
26
|
+
# are printed in a backtrace.
|
27
|
+
|
28
|
+
# Regex adapted from sentry's parser:
|
29
|
+
# https://github.com/getsentry/raven-ruby/blob/2e4378d95dae95a31e3386b2b94c8520649c6876/lib/raven/backtrace.rb#L10-L14
|
30
|
+
FRAME_FORMAT = /
|
31
|
+
\A
|
32
|
+
\s*
|
33
|
+
([^:]+ | <.*>): # file_name
|
34
|
+
(\d+) # line_number
|
35
|
+
(?: :in \s `([^']+)')? # method
|
36
|
+
\z
|
37
|
+
/x
|
38
|
+
|
39
|
+
attr_reader :file_name, :line_number, :method
|
40
|
+
|
41
|
+
# Returns a Frame given backtrace component string or raises
|
42
|
+
# ParseError if it's malformed.
|
43
|
+
def self.parse(unparsed_line)
|
44
|
+
match = unparsed_line.match(FRAME_FORMAT)
|
45
|
+
if match
|
46
|
+
file_name, line_number, method = match.captures
|
47
|
+
new(file_name, line_number, method)
|
48
|
+
else
|
49
|
+
raise ParseError, "unable to parse #{unparsed_line.inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(file_name, line_number, method)
|
54
|
+
@file_name = file_name
|
55
|
+
@line_number = line_number
|
56
|
+
@method = method
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
"#{file_name}:#{line_number}:in `#{method}'"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Failbot
|
2
|
+
module ExceptionFormat
|
3
|
+
# The format recognized by haystack.
|
4
|
+
class Haystack
|
5
|
+
|
6
|
+
# Format an exception.
|
7
|
+
def self.call(e)
|
8
|
+
res = {
|
9
|
+
'class' => e.class.to_s,
|
10
|
+
'message' => e.message,
|
11
|
+
'backtrace' => Array(e.backtrace)[0,500].join("\n"),
|
12
|
+
'ruby' => RUBY_DESCRIPTION,
|
13
|
+
'created_at' => Time.now.utc.iso8601(6)
|
14
|
+
}
|
15
|
+
|
16
|
+
if cause = pretty_print_cause(e)
|
17
|
+
res['cause'] = cause
|
18
|
+
end
|
19
|
+
|
20
|
+
res
|
21
|
+
end
|
22
|
+
|
23
|
+
# We'll include this many nested Exception#cause objects in the needle
|
24
|
+
# context. We limit the number of objects to prevent excessive recursion and
|
25
|
+
# large needle contexts.
|
26
|
+
MAXIMUM_CAUSE_DEPTH = 2
|
27
|
+
|
28
|
+
# Pretty-print Exception#cause (and nested causes) for inclusion in needle
|
29
|
+
# context
|
30
|
+
#
|
31
|
+
# e - The Exception object whose #cause should be printed
|
32
|
+
# depth - Integer number of Exception#cause objects to descend into.
|
33
|
+
#
|
34
|
+
# Returns a String.
|
35
|
+
def self.pretty_print_cause(e, depth = MAXIMUM_CAUSE_DEPTH)
|
36
|
+
return unless depth > 0
|
37
|
+
|
38
|
+
causes = []
|
39
|
+
|
40
|
+
current = e
|
41
|
+
depth.times do
|
42
|
+
pretty_cause = pretty_print_one_cause(current)
|
43
|
+
break unless pretty_cause
|
44
|
+
causes << pretty_cause
|
45
|
+
current = current.cause
|
46
|
+
end
|
47
|
+
|
48
|
+
return if causes.empty?
|
49
|
+
|
50
|
+
result = causes.join("\n\nCAUSED BY:\n\n")
|
51
|
+
|
52
|
+
if current.respond_to?(:cause) && current.cause
|
53
|
+
result << "\n\nFurther #cause backtraces were omitted\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
# Pretty-print a single Exception#cause
|
60
|
+
#
|
61
|
+
# e - The Exception object whose #cause should be printed
|
62
|
+
#
|
63
|
+
# Returns a String.
|
64
|
+
def self.pretty_print_one_cause(e)
|
65
|
+
return unless e.respond_to?(:cause)
|
66
|
+
cause = e.cause
|
67
|
+
return unless cause
|
68
|
+
|
69
|
+
result = "#{cause.class.name}: #{cause.message}\n"
|
70
|
+
|
71
|
+
# Find where the cause's backtrace differs from the child exception's.
|
72
|
+
backtrace = Array(e.backtrace)
|
73
|
+
cause_backtrace = Array(cause.backtrace)
|
74
|
+
index = -1
|
75
|
+
min_index = [backtrace.size, cause_backtrace.size].min * -1
|
76
|
+
just_in_case = -5000
|
77
|
+
|
78
|
+
while index > min_index && backtrace[index] == cause_backtrace[index] && index >= just_in_case
|
79
|
+
index -= 1
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add on a few common frames to make it clear where the backtraces line up.
|
83
|
+
index += 3
|
84
|
+
index = -1 if index >= 0
|
85
|
+
|
86
|
+
cause_backtrace[0..index].each do |line|
|
87
|
+
result << "\t#{line}\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "failbot/backtrace"
|
4
|
+
|
5
|
+
module Failbot
|
6
|
+
module ExceptionFormat
|
7
|
+
# A newer exception format , based on the one sentry uses. Aside from
|
8
|
+
# different names and locations for things, the notable difference from
|
9
|
+
# haystack is that backtrace data has more structure.
|
10
|
+
class Structured
|
11
|
+
EMPTY_ARRAY = [].freeze
|
12
|
+
|
13
|
+
# Format an exception.
|
14
|
+
def self.call(e)
|
15
|
+
message = e.message.to_s
|
16
|
+
stacktrace = begin
|
17
|
+
formatted_backtrace(e) || EMPTY_ARRAY
|
18
|
+
rescue Backtrace::ParseError
|
19
|
+
message += "\nUnable to parse backtrace! Don't put non-backtrace text in Exception#backtrace please!\n#{e.backtrace}"
|
20
|
+
EMPTY_ARRAY
|
21
|
+
end
|
22
|
+
{
|
23
|
+
"exception_detail" => [ # TODO Once supported in failbotg, this should be an array of
|
24
|
+
{ # hashes generated from subsequent calls to Exception#cause.
|
25
|
+
"type" => e.class.to_s,
|
26
|
+
"value" => message,
|
27
|
+
"stacktrace" => stacktrace
|
28
|
+
}
|
29
|
+
],
|
30
|
+
"ruby" => RUBY_DESCRIPTION,
|
31
|
+
"created_at" => Time.now.utc.iso8601(6)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.formatted_backtrace(e)
|
36
|
+
if e.backtrace
|
37
|
+
Backtrace.parse(e.backtrace).frames.reverse.map do |line|
|
38
|
+
{
|
39
|
+
"filename" => line.file_name,
|
40
|
+
"lineno" => line.line_number,
|
41
|
+
"function" => line.method
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/failbot/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: failbot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- "@rtomayko"
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2020-
|
13
|
+
date: 2020-03-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake
|
@@ -90,8 +90,11 @@ extensions: []
|
|
90
90
|
extra_rdoc_files: []
|
91
91
|
files:
|
92
92
|
- lib/failbot.rb
|
93
|
+
- lib/failbot/backtrace.rb
|
93
94
|
- lib/failbot/compat.rb
|
94
95
|
- lib/failbot/console_backend.rb
|
96
|
+
- lib/failbot/exception_format/haystack.rb
|
97
|
+
- lib/failbot/exception_format/structured.rb
|
95
98
|
- lib/failbot/exit_hook.rb
|
96
99
|
- lib/failbot/failbot.yml
|
97
100
|
- lib/failbot/file_backend.rb
|
@@ -122,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
125
|
- !ruby/object:Gem::Version
|
123
126
|
version: 1.3.6
|
124
127
|
requirements: []
|
125
|
-
rubygems_version: 3.
|
128
|
+
rubygems_version: 3.0.3
|
126
129
|
signing_key:
|
127
130
|
specification_version: 4
|
128
131
|
summary: Deliver exceptions to Haystack
|