airbrake-ruby 4.6.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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +513 -0
  3. data/lib/airbrake-ruby/async_sender.rb +142 -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 +48 -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 +104 -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 +45 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +125 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +53 -0
  38. data/lib/airbrake-ruby/request.rb +45 -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/time_truncate.rb +17 -0
  45. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  46. data/lib/airbrake-ruby/truncator.rb +115 -0
  47. data/lib/airbrake-ruby/version.rb +6 -0
  48. data/spec/airbrake_spec.rb +324 -0
  49. data/spec/async_sender_spec.rb +155 -0
  50. data/spec/backtrace_spec.rb +427 -0
  51. data/spec/benchmark_spec.rb +33 -0
  52. data/spec/code_hunk_spec.rb +115 -0
  53. data/spec/config/validator_spec.rb +184 -0
  54. data/spec/config_spec.rb +154 -0
  55. data/spec/deploy_notifier_spec.rb +48 -0
  56. data/spec/file_cache.rb +36 -0
  57. data/spec/filter_chain_spec.rb +92 -0
  58. data/spec/filters/context_filter_spec.rb +23 -0
  59. data/spec/filters/dependency_filter_spec.rb +12 -0
  60. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  61. data/spec/filters/gem_root_filter_spec.rb +41 -0
  62. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  63. data/spec/filters/git_repository_filter.rb +61 -0
  64. data/spec/filters/git_revision_filter_spec.rb +126 -0
  65. data/spec/filters/keys_blacklist_spec.rb +225 -0
  66. data/spec/filters/keys_whitelist_spec.rb +194 -0
  67. data/spec/filters/root_directory_filter_spec.rb +39 -0
  68. data/spec/filters/sql_filter_spec.rb +219 -0
  69. data/spec/filters/system_exit_filter_spec.rb +14 -0
  70. data/spec/filters/thread_filter_spec.rb +277 -0
  71. data/spec/fixtures/notroot.txt +7 -0
  72. data/spec/fixtures/project_root/code.rb +221 -0
  73. data/spec/fixtures/project_root/empty_file.rb +0 -0
  74. data/spec/fixtures/project_root/long_line.txt +1 -0
  75. data/spec/fixtures/project_root/short_file.rb +3 -0
  76. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  77. data/spec/helpers.rb +9 -0
  78. data/spec/ignorable_spec.rb +14 -0
  79. data/spec/inspectable_spec.rb +45 -0
  80. data/spec/monotonic_time_spec.rb +12 -0
  81. data/spec/nested_exception_spec.rb +73 -0
  82. data/spec/notice_notifier_spec.rb +356 -0
  83. data/spec/notice_notifier_spec/options_spec.rb +259 -0
  84. data/spec/notice_spec.rb +296 -0
  85. data/spec/performance_breakdown_spec.rb +12 -0
  86. data/spec/performance_notifier_spec.rb +435 -0
  87. data/spec/promise_spec.rb +197 -0
  88. data/spec/query_spec.rb +11 -0
  89. data/spec/request_spec.rb +11 -0
  90. data/spec/response_spec.rb +88 -0
  91. data/spec/spec_helper.rb +100 -0
  92. data/spec/stashable_spec.rb +23 -0
  93. data/spec/stat_spec.rb +47 -0
  94. data/spec/sync_sender_spec.rb +133 -0
  95. data/spec/tdigest_spec.rb +230 -0
  96. data/spec/time_truncate_spec.rb +13 -0
  97. data/spec/timed_trace_spec.rb +125 -0
  98. data/spec/truncator_spec.rb +238 -0
  99. metadata +213 -0
@@ -0,0 +1,134 @@
1
+ module Airbrake
2
+ # NoticeNotifier is reponsible for sending notices to Airbrake. It supports
3
+ # synchronous and asynchronous delivery.
4
+ #
5
+ # @see Airbrake::Config The list of options
6
+ # @since v1.0.0
7
+ # @api public
8
+ class NoticeNotifier
9
+ # @return [Array<Class>] filters to be executed first
10
+ DEFAULT_FILTERS = [
11
+ Airbrake::Filters::SystemExitFilter,
12
+ Airbrake::Filters::GemRootFilter
13
+
14
+ # Optional filters (must be included by users):
15
+ # Airbrake::Filters::ThreadFilter
16
+ ].freeze
17
+
18
+ include Inspectable
19
+ include Loggable
20
+
21
+ def initialize
22
+ @config = Airbrake::Config.instance
23
+ @context = {}
24
+ @filter_chain = FilterChain.new
25
+ @async_sender = AsyncSender.new
26
+ @sync_sender = SyncSender.new
27
+
28
+ DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
29
+
30
+ add_filter(Airbrake::Filters::ContextFilter.new(@context))
31
+ add_filter(Airbrake::Filters::ExceptionAttributesFilter.new)
32
+ end
33
+
34
+ # @see Airbrake.notify
35
+ def notify(exception, params = {}, &block)
36
+ send_notice(exception, params, default_sender, &block)
37
+ end
38
+
39
+ # @see Airbrake.notify_sync
40
+ def notify_sync(exception, params = {}, &block)
41
+ send_notice(exception, params, @sync_sender, &block).value
42
+ end
43
+
44
+ # @see Airbrake.add_filte
45
+ def add_filter(filter = nil, &block)
46
+ @filter_chain.add_filter(block_given? ? block : filter)
47
+ end
48
+
49
+ # @see Airbrake.delete_filter
50
+ def delete_filter(filter_class)
51
+ @filter_chain.delete_filter(filter_class)
52
+ end
53
+
54
+ # @see Airbrake.build_notice
55
+ def build_notice(exception, params = {})
56
+ if @async_sender.closed?
57
+ raise Airbrake::Error,
58
+ "attempted to build #{exception} with closed Airbrake instance"
59
+ end
60
+
61
+ if exception.is_a?(Airbrake::Notice)
62
+ exception[:params].merge!(params)
63
+ exception
64
+ else
65
+ Notice.new(convert_to_exception(exception), params.dup)
66
+ end
67
+ end
68
+
69
+ # @see Airbrake.close
70
+ def close
71
+ @async_sender.close
72
+ end
73
+
74
+ # @see Airbrake.configured?
75
+ def configured?
76
+ @config.valid?
77
+ end
78
+
79
+ # @see Airbrake.merge_context
80
+ def merge_context(context)
81
+ @context.merge!(context)
82
+ end
83
+
84
+ private
85
+
86
+ def convert_to_exception(ex)
87
+ if ex.is_a?(Exception) || Backtrace.java_exception?(ex)
88
+ # Manually created exceptions don't have backtraces, so we create a fake
89
+ # one, whose first frame points to the place where Airbrake was called
90
+ # (normally via `notify`).
91
+ ex.set_backtrace(clean_backtrace) unless ex.backtrace
92
+ return ex
93
+ end
94
+
95
+ e = RuntimeError.new(ex.to_s)
96
+ e.set_backtrace(clean_backtrace)
97
+ e
98
+ end
99
+
100
+ def send_notice(exception, params, sender)
101
+ promise = @config.check_configuration
102
+ return promise if promise.rejected?
103
+
104
+ notice = build_notice(exception, params)
105
+ yield notice if block_given?
106
+ @filter_chain.refine(notice)
107
+
108
+ promise = Airbrake::Promise.new
109
+ return promise.reject("#{notice} was marked as ignored") if notice.ignored?
110
+
111
+ sender.send(notice, promise)
112
+ end
113
+
114
+ def default_sender
115
+ return @async_sender if @async_sender.has_workers?
116
+
117
+ logger.warn(
118
+ "#{LOG_LABEL} falling back to sync delivery because there are no " \
119
+ "running async workers"
120
+ )
121
+ @sync_sender
122
+ end
123
+
124
+ def clean_backtrace
125
+ caller_copy = Kernel.caller
126
+ clean_bt = caller_copy.drop_while { |frame| frame.include?('/lib/airbrake') }
127
+
128
+ # If true, then it's likely an internal library error. In this case return
129
+ # at least some backtrace to simplify debugging.
130
+ return caller_copy if clean_bt.empty?
131
+ clean_bt
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,45 @@
1
+ module Airbrake
2
+ # PerformanceBreakdown holds data that shows how much time a request spent
3
+ # doing certaing subtasks such as (DB querying, view rendering, etc).
4
+ #
5
+ # @see Airbrake.notify_breakdown
6
+ # @api public
7
+ # @since v4.2.0
8
+ # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
9
+ PerformanceBreakdown = Struct.new(
10
+ :method, :route, :response_type, :groups, :start_time, :end_time
11
+ ) do
12
+ include HashKeyable
13
+ include Ignorable
14
+ include Stashable
15
+
16
+ def initialize(
17
+ method:,
18
+ route:,
19
+ response_type:,
20
+ groups:,
21
+ start_time:,
22
+ end_time: Time.now
23
+ )
24
+ super(method, route, response_type, groups, start_time, end_time)
25
+ end
26
+
27
+ def destination
28
+ 'routes-breakdowns'
29
+ end
30
+
31
+ def cargo
32
+ 'routes'
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ 'method' => method,
38
+ 'route' => route,
39
+ 'responseType' => response_type,
40
+ 'time' => TimeTruncate.utc_truncate_minutes(start_time)
41
+ }.delete_if { |_key, val| val.nil? }
42
+ end
43
+ end
44
+ # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
45
+ end
@@ -0,0 +1,125 @@
1
+ module Airbrake
2
+ # QueryNotifier aggregates information about SQL queries and periodically sends
3
+ # collected data to Airbrake.
4
+ #
5
+ # @api public
6
+ # @since v3.2.0
7
+ class PerformanceNotifier
8
+ include Inspectable
9
+ include Loggable
10
+
11
+ def initialize
12
+ @config = Airbrake::Config.instance
13
+ @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
+ @sender = SyncSender.new(:put)
15
+ @payload = {}
16
+ @schedule_flush = nil
17
+ @mutex = Mutex.new
18
+ @filter_chain = FilterChain.new
19
+ end
20
+
21
+ # @param [Hash] resource
22
+ # @see Airbrake.notify_query
23
+ # @see Airbrake.notify_request
24
+ def notify(resource)
25
+ promise = @config.check_configuration
26
+ return promise if promise.rejected?
27
+
28
+ promise = @config.check_performance_options(resource)
29
+ return promise if promise.rejected?
30
+
31
+ @filter_chain.refine(resource)
32
+ return if resource.ignored?
33
+
34
+ @mutex.synchronize do
35
+ update_payload(resource)
36
+ @flush_period > 0 ? schedule_flush(promise) : send(@payload, promise)
37
+ end
38
+
39
+ promise
40
+ end
41
+
42
+ # @see Airbrake.add_performance_filter
43
+ def add_filter(filter = nil, &block)
44
+ @filter_chain.add_filter(block_given? ? block : filter)
45
+ end
46
+
47
+ # @see Airbrake.delete_performance_filter
48
+ def delete_filter(filter_class)
49
+ @filter_chain.delete_filter(filter_class)
50
+ end
51
+
52
+ private
53
+
54
+ def update_payload(resource)
55
+ @payload[resource] ||= { total: Airbrake::Stat.new }
56
+ @payload[resource][:total].increment(resource.start_time, resource.end_time)
57
+
58
+ resource.groups.each do |name, ms|
59
+ @payload[resource][name] ||= Airbrake::Stat.new
60
+ @payload[resource][name].increment_ms(ms)
61
+ end
62
+ end
63
+
64
+ def schedule_flush(promise)
65
+ @schedule_flush ||= Thread.new do
66
+ sleep(@flush_period)
67
+
68
+ payload = nil
69
+ @mutex.synchronize do
70
+ payload = @payload
71
+ @payload = {}
72
+ @schedule_flush = nil
73
+ end
74
+
75
+ send(payload, promise)
76
+ end
77
+ end
78
+
79
+ def send(payload, promise)
80
+ signature = "#{self.class.name}##{__method__}"
81
+ raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
82
+
83
+ logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
84
+
85
+ with_grouped_payload(payload) do |resource_hash, destination|
86
+ url = URI.join(
87
+ @config.host,
88
+ "api/v5/projects/#{@config.project_id}/#{destination}"
89
+ )
90
+ @sender.send(resource_hash, promise, url)
91
+ end
92
+
93
+ promise
94
+ end
95
+
96
+ def with_grouped_payload(raw_payload)
97
+ grouped_payload = raw_payload.group_by do |resource, _stats|
98
+ [resource.cargo, resource.destination]
99
+ end
100
+
101
+ grouped_payload.each do |(cargo, destination), resources|
102
+ payload = {}
103
+ payload[cargo] = serialize_resources(resources)
104
+ payload['environment'] = @config.environment if @config.environment
105
+
106
+ yield(payload, destination)
107
+ end
108
+ end
109
+
110
+ def serialize_resources(resources)
111
+ resources.map do |resource, stats|
112
+ resource_hash = resource.to_h.merge!(stats[:total].to_h)
113
+
114
+ if resource.groups.any?
115
+ group_stats = stats.reject { |name, _stat| name == :total }
116
+ resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
117
+ stat.to_h
118
+ end
119
+ end
120
+
121
+ resource_hash
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,109 @@
1
+ module Airbrake
2
+ # Represents a simplified promise object (similar to promises found in
3
+ # JavaScript), which allows chaining callbacks that are executed when the
4
+ # promise is either resolved or rejected.
5
+ #
6
+ # @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
7
+ # @see https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/promise.rb
8
+ # @since v1.7.0
9
+ class Promise
10
+ def initialize
11
+ @on_resolved = []
12
+ @on_rejected = []
13
+ @value = {}
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ # Attaches a callback to be executed when the promise is resolved.
18
+ #
19
+ # @example
20
+ # Airbrake::Promise.new.then { |response| puts response }
21
+ # #=> {"id"=>"00054415-8201-e9c6-65d6-fc4d231d2871",
22
+ # # "url"=>"http://localhost/locate/00054415-8201-e9c6-65d6-fc4d231d2871"}
23
+ #
24
+ # @yield [response]
25
+ # @yieldparam response [Hash<String,String>] Contains the `id` & `url` keys
26
+ # @return [self]
27
+ def then(&block)
28
+ @mutex.synchronize do
29
+ if @value.key?('ok')
30
+ yield(@value['ok'])
31
+ return self
32
+ end
33
+
34
+ @on_resolved << block
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ # Attaches a callback to be executed when the promise is rejected.
41
+ #
42
+ # @example
43
+ # Airbrake::Promise.new.rescue { |error| raise error }
44
+ #
45
+ # @yield [error] The error message from the API
46
+ # @yieldparam error [String]
47
+ # @return [self]
48
+ def rescue(&block)
49
+ @mutex.synchronize do
50
+ if @value.key?('error')
51
+ yield(@value['error'])
52
+ return self
53
+ end
54
+
55
+ @on_rejected << block
56
+ end
57
+
58
+ self
59
+ end
60
+
61
+ # @example
62
+ # Airbrake::Promise.new.resolve('id' => '123')
63
+ #
64
+ # @param reason [Object]
65
+ # @return [self]
66
+ def resolve(reason = 'resolved')
67
+ @mutex.synchronize do
68
+ @value['ok'] = reason
69
+ @on_resolved.each { |callback| callback.call(reason) }
70
+ end
71
+
72
+ self
73
+ end
74
+
75
+ # @example
76
+ # Airbrake::Promise.new.reject('Something went wrong')
77
+ #
78
+ # @param reason [String]
79
+ # @return [self]
80
+ def reject(reason = 'rejected')
81
+ @mutex.synchronize do
82
+ @value['error'] = reason
83
+ @on_rejected.each { |callback| callback.call(reason) }
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ # @return [Boolean]
90
+ def rejected?
91
+ @value.key?('error')
92
+ end
93
+
94
+ # @return [Boolean]
95
+ def resolved?
96
+ @value.key?('ok')
97
+ end
98
+
99
+ # @return [Hash<String,String>] either successful response containing the
100
+ # +id+ key or unsuccessful response containing the +error+ key
101
+ # @note This is a non-blocking call!
102
+ # @todo Get rid of this method and use an accessor. The resolved guard is
103
+ # needed for compatibility but it shouldn't exist in the future
104
+ def value
105
+ return @value['ok'] if resolved?
106
+ @value
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,53 @@
1
+ module Airbrake
2
+ # Query holds SQL query data that powers SQL query collection.
3
+ #
4
+ # @see Airbrake.notify_query
5
+ # @api public
6
+ # @since v3.2.0
7
+ # rubocop:disable Metrics/ParameterLists, Metrics/BlockLength
8
+ Query = Struct.new(
9
+ :method, :route, :query, :func, :file, :line, :start_time, :end_time
10
+ ) do
11
+ include HashKeyable
12
+ include Ignorable
13
+ include Stashable
14
+
15
+ def initialize(
16
+ method:,
17
+ route:,
18
+ query:,
19
+ func: nil,
20
+ file: nil,
21
+ line: nil,
22
+ start_time:,
23
+ end_time: Time.now
24
+ )
25
+ super(method, route, query, func, file, line, start_time, end_time)
26
+ end
27
+
28
+ def destination
29
+ 'queries-stats'
30
+ end
31
+
32
+ def cargo
33
+ 'queries'
34
+ end
35
+
36
+ def groups
37
+ {}
38
+ end
39
+
40
+ def to_h
41
+ {
42
+ 'method' => method,
43
+ 'route' => route,
44
+ 'query' => query,
45
+ 'time' => TimeTruncate.utc_truncate_minutes(start_time),
46
+ 'function' => func,
47
+ 'file' => file,
48
+ 'line' => line
49
+ }.delete_if { |_key, val| val.nil? }
50
+ end
51
+ # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
52
+ end
53
+ end