failbot 2.3.3 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|