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,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,46 @@
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
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
25
+ super(method, route, response_type, groups, start_time, end_time)
26
+ end
27
+
28
+ def destination
29
+ 'routes-breakdowns'
30
+ end
31
+
32
+ def cargo
33
+ 'routes'
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ 'method' => method,
39
+ 'route' => route,
40
+ 'responseType' => response_type,
41
+ 'time' => @start_time_utc
42
+ }.delete_if { |_key, val| val.nil? }
43
+ end
44
+ end
45
+ # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
46
+ end
@@ -0,0 +1,155 @@
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 = AsyncSender.new(:put)
15
+ @payload = {}
16
+ @schedule_flush = nil
17
+ @mutex = Mutex.new
18
+ @filter_chain = FilterChain.new
19
+ @waiting = false
20
+ end
21
+
22
+ # @param [Hash] resource
23
+ # @see Airbrake.notify_query
24
+ # @see Airbrake.notify_request
25
+ def notify(resource)
26
+ promise = @config.check_configuration
27
+ return promise if promise.rejected?
28
+
29
+ promise = @config.check_performance_options(resource)
30
+ return promise if promise.rejected?
31
+
32
+ @filter_chain.refine(resource)
33
+ return if resource.ignored?
34
+
35
+ @mutex.synchronize do
36
+ update_payload(resource)
37
+ @flush_period > 0 ? schedule_flush : send(@payload, promise)
38
+ end
39
+
40
+ promise.resolve(:success)
41
+ end
42
+
43
+ # @see Airbrake.add_performance_filter
44
+ def add_filter(filter = nil, &block)
45
+ @filter_chain.add_filter(block_given? ? block : filter)
46
+ end
47
+
48
+ # @see Airbrake.delete_performance_filter
49
+ def delete_filter(filter_class)
50
+ @filter_chain.delete_filter(filter_class)
51
+ end
52
+
53
+ def close
54
+ @mutex.synchronize do
55
+ @schedule_flush.kill if @schedule_flush
56
+ @sender.close
57
+ logger.debug("#{LOG_LABEL} performance notifier closed")
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def update_payload(resource)
64
+ @payload[resource] ||= { total: Airbrake::Stat.new }
65
+ @payload[resource][:total].increment(resource.start_time, resource.end_time)
66
+
67
+ resource.groups.each do |name, ms|
68
+ @payload[resource][name] ||= Airbrake::Stat.new
69
+ @payload[resource][name].increment_ms(ms)
70
+ end
71
+ end
72
+
73
+ def schedule_flush
74
+ return if @payload.empty?
75
+
76
+ if @schedule_flush && @schedule_flush.status == 'sleep' && @waiting
77
+ begin
78
+ @schedule_flush.run
79
+ rescue ThreadError => exception
80
+ logger.error("#{LOG_LABEL}: error occurred while flushing: #{exception}")
81
+ end
82
+ end
83
+
84
+ @schedule_flush ||= spawn_timer
85
+ end
86
+
87
+ def spawn_timer
88
+ Thread.new do
89
+ loop do
90
+ if @payload.none?
91
+ @waiting = true
92
+ Thread.stop
93
+ @waiting = false
94
+ end
95
+
96
+ sleep(@flush_period)
97
+
98
+ payload = nil
99
+ @mutex.synchronize do
100
+ payload = @payload
101
+ @payload = {}
102
+ end
103
+
104
+ send(payload, Airbrake::Promise.new)
105
+ end
106
+ end
107
+ end
108
+
109
+ def send(payload, promise)
110
+ signature = "#{self.class.name}##{__method__}"
111
+ raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
112
+
113
+ logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
114
+
115
+ with_grouped_payload(payload) do |resource_hash, destination|
116
+ url = URI.join(
117
+ @config.host,
118
+ "api/v5/projects/#{@config.project_id}/#{destination}"
119
+ )
120
+ @sender.send(resource_hash, promise, url)
121
+ end
122
+
123
+ promise
124
+ end
125
+
126
+ def with_grouped_payload(raw_payload)
127
+ grouped_payload = raw_payload.group_by do |resource, _stats|
128
+ [resource.cargo, resource.destination]
129
+ end
130
+
131
+ grouped_payload.each do |(cargo, destination), resources|
132
+ payload = {}
133
+ payload[cargo] = serialize_resources(resources)
134
+ payload['environment'] = @config.environment if @config.environment
135
+
136
+ yield(payload, destination)
137
+ end
138
+ end
139
+
140
+ def serialize_resources(resources)
141
+ resources.map do |resource, stats|
142
+ resource_hash = resource.to_h.merge!(stats[:total].to_h)
143
+
144
+ if resource.groups.any?
145
+ group_stats = stats.reject { |name, _stat| name == :total }
146
+ resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
147
+ stat.to_h
148
+ end
149
+ end
150
+
151
+ resource_hash
152
+ end
153
+ end
154
+ end
155
+ 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,54 @@
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
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
26
+ super(method, route, query, func, file, line, start_time, end_time)
27
+ end
28
+
29
+ def destination
30
+ 'queries-stats'
31
+ end
32
+
33
+ def cargo
34
+ 'queries'
35
+ end
36
+
37
+ def groups
38
+ {}
39
+ end
40
+
41
+ def to_h
42
+ {
43
+ 'method' => method,
44
+ 'route' => route,
45
+ 'query' => query,
46
+ 'time' => @start_time_utc,
47
+ 'function' => func,
48
+ 'file' => file,
49
+ 'line' => line
50
+ }.delete_if { |_key, val| val.nil? }
51
+ end
52
+ # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
53
+ end
54
+ end