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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d59eaaa4b282ed08208605e95c1016cf129cc2bc8d8a877d9be047aac0a7a98
4
- data.tar.gz: 08e20096fdc2ca17e6decb2a0528d0170d4dd836f627f1ef79de5062ff972684
3
+ metadata.gz: 72511c74e8a4c9b1dd06d9408c0c926568382e8e72cd3e3a28b7d262835e9aef
4
+ data.tar.gz: 649f8f43daedeb3044cc1457b09948b9f354db737e73e5b512a1ad2f90850684
5
5
  SHA512:
6
- metadata.gz: 26da2ec068e6e273b984bf9c8ff5d231182914bff9a3bb0861e13854f1a43a8bab3587ea8b12531b4d2d41c2f98e71fa94e7880fa1ca0237fca4634eb46f04d0
7
- data.tar.gz: ca25afcbfd0e45c8cf23218b4cc695347f9f39294e943a6402f4b698ff3dbeb22f0179276f7d691e323c9a6b733de034f88c9e9b3d3c57582ec1688be81c606b
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 including a 'class', 'message', 'backtrace' keys.
398
+ # Returns a Hash.
364
399
  def exception_info(e)
365
- backtrace = Array(e.backtrace)[0, 500]
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
@@ -1,3 +1,3 @@
1
1
  module Failbot
2
- VERSION = "2.3.3"
2
+ VERSION = "2.4.0"
3
3
  end
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.3.3
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-01-15 00:00:00.000000000 Z
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.1.2
128
+ rubygems_version: 3.0.3
126
129
  signing_key:
127
130
  specification_version: 4
128
131
  summary: Deliver exceptions to Haystack