airbrake-ruby 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +515 -0
  3. data/lib/airbrake-ruby/async_sender.rb +80 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +54 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +125 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +155 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +54 -0
  38. data/lib/airbrake-ruby/request.rb +46 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/thread_pool.rb +128 -0
  45. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +115 -0
  48. data/lib/airbrake-ruby/version.rb +6 -0
  49. data/spec/airbrake_spec.rb +324 -0
  50. data/spec/async_sender_spec.rb +72 -0
  51. data/spec/backtrace_spec.rb +427 -0
  52. data/spec/benchmark_spec.rb +33 -0
  53. data/spec/code_hunk_spec.rb +115 -0
  54. data/spec/config/validator_spec.rb +184 -0
  55. data/spec/config_spec.rb +154 -0
  56. data/spec/deploy_notifier_spec.rb +48 -0
  57. data/spec/file_cache_spec.rb +34 -0
  58. data/spec/filter_chain_spec.rb +92 -0
  59. data/spec/filters/context_filter_spec.rb +23 -0
  60. data/spec/filters/dependency_filter_spec.rb +12 -0
  61. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  62. data/spec/filters/gem_root_filter_spec.rb +41 -0
  63. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  64. data/spec/filters/git_repository_filter.rb +61 -0
  65. data/spec/filters/git_revision_filter_spec.rb +126 -0
  66. data/spec/filters/keys_blacklist_spec.rb +225 -0
  67. data/spec/filters/keys_whitelist_spec.rb +194 -0
  68. data/spec/filters/root_directory_filter_spec.rb +39 -0
  69. data/spec/filters/sql_filter_spec.rb +262 -0
  70. data/spec/filters/system_exit_filter_spec.rb +14 -0
  71. data/spec/filters/thread_filter_spec.rb +277 -0
  72. data/spec/fixtures/notroot.txt +7 -0
  73. data/spec/fixtures/project_root/code.rb +221 -0
  74. data/spec/fixtures/project_root/empty_file.rb +0 -0
  75. data/spec/fixtures/project_root/long_line.txt +1 -0
  76. data/spec/fixtures/project_root/short_file.rb +3 -0
  77. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  78. data/spec/helpers.rb +9 -0
  79. data/spec/ignorable_spec.rb +14 -0
  80. data/spec/inspectable_spec.rb +45 -0
  81. data/spec/monotonic_time_spec.rb +12 -0
  82. data/spec/nested_exception_spec.rb +73 -0
  83. data/spec/notice_notifier/options_spec.rb +259 -0
  84. data/spec/notice_notifier_spec.rb +356 -0
  85. data/spec/notice_spec.rb +296 -0
  86. data/spec/performance_breakdown_spec.rb +12 -0
  87. data/spec/performance_notifier_spec.rb +491 -0
  88. data/spec/promise_spec.rb +197 -0
  89. data/spec/query_spec.rb +11 -0
  90. data/spec/request_spec.rb +11 -0
  91. data/spec/response_spec.rb +88 -0
  92. data/spec/spec_helper.rb +100 -0
  93. data/spec/stashable_spec.rb +23 -0
  94. data/spec/stat_spec.rb +47 -0
  95. data/spec/sync_sender_spec.rb +133 -0
  96. data/spec/tdigest_spec.rb +230 -0
  97. data/spec/thread_pool_spec.rb +158 -0
  98. data/spec/time_truncate_spec.rb +13 -0
  99. data/spec/timed_trace_spec.rb +125 -0
  100. data/spec/truncator_spec.rb +238 -0
  101. metadata +216 -0
@@ -0,0 +1,23 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Skip over SystemExit exceptions, because they're just noise.
4
+ # @api private
5
+ class SystemExitFilter
6
+ # @return [String]
7
+ SYSTEM_EXIT_TYPE = 'SystemExit'.freeze
8
+
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize
13
+ @weight = 130
14
+ end
15
+
16
+ # @macro call_filter
17
+ def call(notice)
18
+ return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
19
+ notice.ignore!
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,92 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches thread & fiber local variables along with general thread
4
+ # information.
5
+ # @api private
6
+ class ThreadFilter
7
+ # @return [Integer]
8
+ attr_reader :weight
9
+
10
+ # @return [Array<Class>] the list of classes that can be safely converted
11
+ # to JSON
12
+ SAFE_CLASSES = [
13
+ NilClass,
14
+ TrueClass,
15
+ FalseClass,
16
+ String,
17
+ Symbol,
18
+ Regexp,
19
+ Numeric
20
+ ].freeze
21
+
22
+ # Variables starting with this prefix are not attached to a notice.
23
+ # @see https://github.com/airbrake/airbrake-ruby/issues/229
24
+ # @return [String]
25
+ IGNORE_PREFIX = '_'.freeze
26
+
27
+ def initialize
28
+ @weight = 110
29
+ end
30
+
31
+ # @macro call_filter
32
+ def call(notice)
33
+ th = Thread.current
34
+ thread_info = {}
35
+
36
+ if (vars = thread_variables(th)).any?
37
+ thread_info[:thread_variables] = vars
38
+ end
39
+
40
+ if (vars = fiber_variables(th)).any?
41
+ thread_info[:fiber_variables] = vars
42
+ end
43
+
44
+ # Present in Ruby 2.3+.
45
+ if th.respond_to?(:name) && (name = th.name)
46
+ thread_info[:name] = name
47
+ end
48
+
49
+ add_thread_info(th, thread_info)
50
+
51
+ notice[:params][:thread] = thread_info
52
+ end
53
+
54
+ private
55
+
56
+ def thread_variables(th)
57
+ th.thread_variables.map.with_object({}) do |var, h|
58
+ next if var.to_s.start_with?(IGNORE_PREFIX)
59
+ h[var] = sanitize_value(th.thread_variable_get(var))
60
+ end
61
+ end
62
+
63
+ def fiber_variables(th)
64
+ th.keys.map.with_object({}) do |key, h|
65
+ next if key.to_s.start_with?(IGNORE_PREFIX)
66
+ h[key] = sanitize_value(th[key])
67
+ end
68
+ end
69
+
70
+ def add_thread_info(th, thread_info)
71
+ thread_info[:self] = th.inspect
72
+ thread_info[:group] = th.group.list.map(&:inspect)
73
+ thread_info[:priority] = th.priority
74
+
75
+ thread_info[:safe_level] = th.safe_level unless Airbrake::JRUBY
76
+ end
77
+
78
+ def sanitize_value(value)
79
+ return value if SAFE_CLASSES.any? { |klass| value.is_a?(klass) }
80
+
81
+ case value
82
+ when Array
83
+ value = value.map { |elem| sanitize_value(elem) }
84
+ when Hash
85
+ Hash[value.map { |k, v| [k, sanitize_value(v)] }]
86
+ else
87
+ value.to_s
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,37 @@
1
+ module Airbrake
2
+ # HashKeyable allows instances of the class to be used as a Hash key in a
3
+ # consistent manner.
4
+ #
5
+ # The class that includes it must implement *to_h*, which defines properties
6
+ # that all of the instances must share in order to produce the same {#hash}.
7
+ #
8
+ # @example
9
+ # class C
10
+ # include Airbrake::HashKeyable
11
+ #
12
+ # def initialize(key)
13
+ # @key = key
14
+ # end
15
+ #
16
+ # def to_h
17
+ # { 'key' => @key }
18
+ # end
19
+ # end
20
+ #
21
+ # h = {}
22
+ # h[C.new('key1')] = 1
23
+ # h[C.new('key1')] #=> 1
24
+ # h[C.new('key2')] #=> nil
25
+ module HashKeyable
26
+ # @param [Object] other
27
+ # @return [Boolean]
28
+ def eql?(other)
29
+ other.is_a?(self.class) && other.hash == hash
30
+ end
31
+
32
+ # @return [Integer]
33
+ def hash
34
+ to_h.hash
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ module Airbrake
2
+ # Ignorable contains methods that allow the includee to be ignored.
3
+ #
4
+ # @example
5
+ # class A
6
+ # include Airbrake::Ignorable
7
+ # end
8
+ #
9
+ # a = A.new
10
+ # a.ignore!
11
+ # a.ignored? #=> true
12
+ #
13
+ # @since v3.2.0
14
+ # @api private
15
+ module Ignorable
16
+ attr_accessor :ignored
17
+
18
+ # Checks whether the instance was ignored.
19
+ # @return [Boolean]
20
+ # @see #ignore!
21
+ # rubocop:disable Style/DoubleNegation
22
+ def ignored?
23
+ !!ignored
24
+ end
25
+ # rubocop:enable Style/DoubleNegation
26
+
27
+ # Ignores an instance. Ignored instances must never reach the Airbrake
28
+ # dashboard.
29
+ # @return [void]
30
+ # @see #ignored?
31
+ def ignore!
32
+ self.ignored = true
33
+ end
34
+
35
+ private
36
+
37
+ # A method that is meant to be used as a guard.
38
+ # @raise [Airbrake::Error] when instance is ignored
39
+ def raise_if_ignored
40
+ return unless ignored?
41
+ raise Airbrake::Error, "cannot access ignored #{self.class}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ module Airbrake
2
+ # Inspectable provides custom inspect methods that reduce clutter printed in
3
+ # REPLs for notifier objects. These custom methods display only essential
4
+ # information such as project id/key and filters.
5
+ #
6
+ # @since v3.2.6
7
+ # @api private
8
+ module Inspectable
9
+ # @return [String] inspect output template
10
+ INSPECT_TEMPLATE =
11
+ "#<%<classname>s:0x%<id>s project_id=\"%<project_id>s\" " \
12
+ "project_key=\"%<project_key>s\" " \
13
+ "host=\"%<host>s\" filter_chain=%<filter_chain>s>".freeze
14
+
15
+ # @return [String] customized inspect to lessen the amount of clutter
16
+ def inspect
17
+ format(
18
+ INSPECT_TEMPLATE,
19
+ classname: self.class.name,
20
+ id: (object_id << 1).to_s(16).rjust(16, '0'),
21
+ project_id: @config.project_id,
22
+ project_key: @config.project_key,
23
+ host: @config.host,
24
+ filter_chain: @filter_chain.inspect
25
+ )
26
+ end
27
+
28
+ # @return [String] {#inspect} for PrettyPrint
29
+ def pretty_print(q)
30
+ q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
31
+ q.text(
32
+ "project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
33
+ "host=\"#{@config.host}\" filter_chain="
34
+ )
35
+ q.pp(@filter_chain)
36
+ q.text('>')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ module Airbrake
2
+ # Loggable is included into any class that wants to be able to log.
3
+ #
4
+ # By default, Loggable defines a null logger that doesn't do anything. You are
5
+ # supposed to overwrite it via the {instance} method before calling {logger}.
6
+ #
7
+ # @example
8
+ # class A
9
+ # include Loggable
10
+ #
11
+ # def initialize
12
+ # logger.debug('Initialized A')
13
+ # end
14
+ # end
15
+ #
16
+ # @since v4.0.0
17
+ # @api private
18
+ module Loggable
19
+ class << self
20
+ # @return [Logger]
21
+ attr_writer :instance
22
+
23
+ # @return [Logger]
24
+ def instance
25
+ @instance ||= ::Logger.new(File::NULL)
26
+ end
27
+ end
28
+
29
+ # @return [Logger] standard Ruby logger object
30
+ def logger
31
+ Loggable.instance
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Airbrake
2
+ # MonotonicTime is a helper for getting monotonic time suitable for
3
+ # performance measurements. It guarantees that the time is strictly linearly
4
+ # increasing (unlike realtime).
5
+ #
6
+ # @example
7
+ # MonotonicTime.time_in_ms #=> 287138801.144576
8
+ #
9
+ # @see http://pubs.opengroup.org/onlinepubs/9699919799/functions/clock_getres.html
10
+ # @since v4.2.4
11
+ # @api private
12
+ module MonotonicTime
13
+ class << self
14
+ # @return [Integer] current monotonic time in milliseconds
15
+ def time_in_ms
16
+ time_in_nanoseconds / (10.0**6)
17
+ end
18
+
19
+ private
20
+
21
+ if defined?(Process::CLOCK_MONOTONIC)
22
+
23
+ def time_in_nanoseconds
24
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
25
+ end
26
+
27
+ elsif RUBY_ENGINE == 'jruby'
28
+
29
+ def time_in_nanoseconds
30
+ java.lang.System.nanoTime
31
+ end
32
+
33
+ else
34
+
35
+ def time_in_nanoseconds
36
+ time = Time.now
37
+ time.to_i * (10**9) + time.nsec
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ module Airbrake
2
+ # A class that is capable of unwinding nested exceptions and representing them
3
+ # as JSON-like hash.
4
+ #
5
+ # @api private
6
+ # @since v1.0.4
7
+ class NestedException
8
+ # @return [Integer] the maximum number of nested exceptions that a notice
9
+ # can unwrap. Exceptions that have a longer cause chain will be ignored
10
+ MAX_NESTED_EXCEPTIONS = 3
11
+
12
+ def initialize(exception)
13
+ @exception = exception
14
+ end
15
+
16
+ def as_json
17
+ unwind_exceptions.map do |exception|
18
+ { type: exception.class.name,
19
+ message: exception.message,
20
+ backtrace: Backtrace.parse(exception) }
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def unwind_exceptions
27
+ exception_list = []
28
+ exception = @exception
29
+
30
+ while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
31
+ exception_list << exception
32
+ exception = (exception.cause if exception.respond_to?(:cause))
33
+ end
34
+
35
+ exception_list
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,162 @@
1
+ module Airbrake
2
+ # Represents a chunk of information that is meant to be either sent to
3
+ # Airbrake or ignored completely.
4
+ #
5
+ # @since v1.0.0
6
+ class Notice
7
+ # @return [Hash{Symbol=>String}] the information about the notifier library
8
+ NOTIFIER = {
9
+ name: 'airbrake-ruby'.freeze,
10
+ version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
+ url: 'https://github.com/airbrake/airbrake-ruby'.freeze
12
+ }.freeze
13
+
14
+ # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
+ # Context tab in the dashboard
16
+ CONTEXT = {
17
+ os: RUBY_PLATFORM,
18
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
+ notifier: NOTIFIER
20
+ }.freeze
21
+
22
+ # @return [Integer] the maxium size of the JSON payload in bytes
23
+ MAX_NOTICE_SIZE = 64000
24
+
25
+ # @return [Integer] the maximum size of hashes, arrays and strings in the
26
+ # notice.
27
+ PAYLOAD_MAX_SIZE = 10000
28
+
29
+ # @return [Array<StandardError>] the list of possible exceptions that might
30
+ # be raised when an object is converted to JSON
31
+ JSON_EXCEPTIONS = [
32
+ IOError,
33
+ NotImplementedError,
34
+ JSON::GeneratorError,
35
+ Encoding::UndefinedConversionError
36
+ ].freeze
37
+
38
+ # @return [Array<Symbol>] the list of keys that can be be overwritten with
39
+ # {Airbrake::Notice#[]=}
40
+ WRITABLE_KEYS = %i[notifier context environment session params].freeze
41
+
42
+ # @return [Array<Symbol>] parts of a Notice's payload that can be modified
43
+ # by the truncator
44
+ TRUNCATABLE_KEYS = %i[errors environment session params].freeze
45
+
46
+ # @return [String] the name of the host machine
47
+ HOSTNAME = Socket.gethostname.freeze
48
+
49
+ # @return [String]
50
+ DEFAULT_SEVERITY = 'error'.freeze
51
+
52
+ include Ignorable
53
+ include Loggable
54
+ include Stashable
55
+
56
+ # @api private
57
+ def initialize(exception, params = {})
58
+ @config = Airbrake::Config.instance
59
+ @payload = {
60
+ errors: NestedException.new(exception).as_json,
61
+ context: context,
62
+ environment: {
63
+ program_name: $PROGRAM_NAME
64
+ },
65
+ session: {},
66
+ params: params
67
+ }
68
+ @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
69
+
70
+ stash[:exception] = exception
71
+ end
72
+
73
+ # Converts the notice to JSON. Calls +to_json+ on each object inside
74
+ # notice's payload. Truncates notices, JSON representation of which is
75
+ # bigger than {MAX_NOTICE_SIZE}.
76
+ #
77
+ # @return [Hash{String=>String}, nil]
78
+ # @api private
79
+ def to_json
80
+ loop do
81
+ begin
82
+ json = @payload.to_json
83
+ rescue *JSON_EXCEPTIONS => ex
84
+ logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
85
+ else
86
+ return json if json && json.bytesize <= MAX_NOTICE_SIZE
87
+ end
88
+
89
+ break if truncate == 0
90
+ end
91
+ end
92
+
93
+ # Reads a value from notice's payload.
94
+ #
95
+ # @return [Object]
96
+ # @raise [Airbrake::Error] if the notice is ignored
97
+ def [](key)
98
+ raise_if_ignored
99
+ @payload[key]
100
+ end
101
+
102
+ # Writes a value to the payload hash. Restricts unrecognized writes.
103
+ #
104
+ # @example
105
+ # notice[:params][:my_param] = 'foobar'
106
+ #
107
+ # @return [void]
108
+ # @raise [Airbrake::Error] if the notice is ignored
109
+ # @raise [Airbrake::Error] if the +key+ is not recognized
110
+ # @raise [Airbrake::Error] if the root value is not a Hash
111
+ def []=(key, value)
112
+ raise_if_ignored
113
+
114
+ unless WRITABLE_KEYS.include?(key)
115
+ raise Airbrake::Error,
116
+ ":#{key} is not recognized among #{WRITABLE_KEYS}"
117
+ end
118
+
119
+ unless value.respond_to?(:to_hash)
120
+ raise Airbrake::Error, "Got #{value.class} value, wanted a Hash"
121
+ end
122
+
123
+ @payload[key] = value.to_hash
124
+ end
125
+
126
+ private
127
+
128
+ def context
129
+ {
130
+ version: @config.app_version,
131
+ versions: @config.versions,
132
+ # We ensure that root_directory is always a String, so it can always be
133
+ # converted to JSON in a predictable manner (when it's a Pathname and in
134
+ # Rails environment, it converts to unexpected JSON).
135
+ rootDirectory: @config.root_directory.to_s,
136
+ environment: @config.environment,
137
+
138
+ # Make sure we always send hostname.
139
+ hostname: HOSTNAME,
140
+
141
+ severity: DEFAULT_SEVERITY
142
+ }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
143
+ end
144
+
145
+ def truncate
146
+ TRUNCATABLE_KEYS.each do |key|
147
+ @payload[key] = @truncator.truncate(@payload[key])
148
+ end
149
+
150
+ new_max_size = @truncator.reduce_max_size
151
+ if new_max_size == 0
152
+ logger.error(
153
+ "#{LOG_LABEL} truncation failed. File an issue at " \
154
+ "https://github.com/airbrake/airbrake-ruby " \
155
+ "and attach the following payload: #{@payload}"
156
+ )
157
+ end
158
+
159
+ new_max_size
160
+ end
161
+ end
162
+ end