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 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