airbrake-ruby 4.8.0 → 5.2.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +132 -57
  3. data/lib/airbrake-ruby/async_sender.rb +7 -30
  4. data/lib/airbrake-ruby/backtrace.rb +8 -7
  5. data/lib/airbrake-ruby/benchmark.rb +1 -1
  6. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  7. data/lib/airbrake-ruby/config.rb +59 -15
  8. data/lib/airbrake-ruby/config/processor.rb +71 -0
  9. data/lib/airbrake-ruby/config/validator.rb +9 -3
  10. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  11. data/lib/airbrake-ruby/file_cache.rb +1 -1
  12. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  19. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  20. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  21. data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +7 -7
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +5 -4
  26. data/lib/airbrake-ruby/grouppable.rb +12 -0
  27. data/lib/airbrake-ruby/ignorable.rb +1 -0
  28. data/lib/airbrake-ruby/inspectable.rb +2 -2
  29. data/lib/airbrake-ruby/loggable.rb +1 -1
  30. data/lib/airbrake-ruby/mergeable.rb +12 -0
  31. data/lib/airbrake-ruby/monotonic_time.rb +5 -0
  32. data/lib/airbrake-ruby/notice.rb +7 -14
  33. data/lib/airbrake-ruby/notice_notifier.rb +11 -3
  34. data/lib/airbrake-ruby/performance_breakdown.rb +16 -10
  35. data/lib/airbrake-ruby/performance_notifier.rb +80 -58
  36. data/lib/airbrake-ruby/promise.rb +1 -0
  37. data/lib/airbrake-ruby/query.rb +20 -15
  38. data/lib/airbrake-ruby/queue.rb +65 -0
  39. data/lib/airbrake-ruby/remote_settings.rb +105 -0
  40. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  41. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  42. data/lib/airbrake-ruby/request.rb +14 -12
  43. data/lib/airbrake-ruby/stat.rb +26 -33
  44. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  45. data/lib/airbrake-ruby/tdigest.rb +43 -58
  46. data/lib/airbrake-ruby/thread_pool.rb +11 -1
  47. data/lib/airbrake-ruby/truncator.rb +10 -4
  48. data/lib/airbrake-ruby/version.rb +11 -1
  49. data/spec/airbrake_spec.rb +206 -71
  50. data/spec/async_sender_spec.rb +3 -12
  51. data/spec/backtrace_spec.rb +44 -44
  52. data/spec/code_hunk_spec.rb +11 -11
  53. data/spec/config/processor_spec.rb +143 -0
  54. data/spec/config/validator_spec.rb +23 -6
  55. data/spec/config_spec.rb +40 -14
  56. data/spec/deploy_notifier_spec.rb +2 -2
  57. data/spec/filter_chain_spec.rb +28 -1
  58. data/spec/filters/dependency_filter_spec.rb +1 -1
  59. data/spec/filters/gem_root_filter_spec.rb +9 -9
  60. data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
  61. data/spec/filters/git_repository_filter.rb +1 -1
  62. data/spec/filters/git_revision_filter_spec.rb +10 -10
  63. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
  64. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
  65. data/spec/filters/root_directory_filter_spec.rb +9 -9
  66. data/spec/filters/sql_filter_spec.rb +58 -60
  67. data/spec/filters/system_exit_filter_spec.rb +1 -1
  68. data/spec/filters/thread_filter_spec.rb +32 -30
  69. data/spec/fixtures/project_root/code.rb +9 -9
  70. data/spec/loggable_spec.rb +17 -0
  71. data/spec/monotonic_time_spec.rb +11 -0
  72. data/spec/notice_notifier/options_spec.rb +17 -17
  73. data/spec/notice_notifier_spec.rb +20 -20
  74. data/spec/notice_spec.rb +6 -6
  75. data/spec/performance_breakdown_spec.rb +0 -1
  76. data/spec/performance_notifier_spec.rb +220 -73
  77. data/spec/query_spec.rb +1 -1
  78. data/spec/queue_spec.rb +18 -0
  79. data/spec/remote_settings/callback_spec.rb +143 -0
  80. data/spec/remote_settings/settings_data_spec.rb +348 -0
  81. data/spec/remote_settings_spec.rb +187 -0
  82. data/spec/request_spec.rb +1 -3
  83. data/spec/response_spec.rb +8 -8
  84. data/spec/spec_helper.rb +6 -6
  85. data/spec/stat_spec.rb +2 -12
  86. data/spec/sync_sender_spec.rb +14 -12
  87. data/spec/tdigest_spec.rb +7 -7
  88. data/spec/thread_pool_spec.rb +39 -10
  89. data/spec/timed_trace_spec.rb +1 -1
  90. data/spec/truncator_spec.rb +12 -12
  91. metadata +32 -14
@@ -5,24 +5,30 @@ module Airbrake
5
5
  # @see Airbrake.notify_breakdown
6
6
  # @api public
7
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
8
+ # rubocop:disable Metrics/ParameterLists
9
+ class PerformanceBreakdown
12
10
  include HashKeyable
13
11
  include Ignorable
14
12
  include Stashable
13
+ include Mergeable
14
+
15
+ attr_accessor :method, :route, :response_type, :groups, :timing, :time
15
16
 
16
17
  def initialize(
17
18
  method:,
18
19
  route:,
19
20
  response_type:,
20
21
  groups:,
21
- start_time:,
22
- end_time: Time.now
22
+ timing: nil,
23
+ time: Time.now
23
24
  )
24
- @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
25
- super(method, route, response_type, groups, start_time, end_time)
25
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
26
+ @method = method
27
+ @route = route
28
+ @response_type = response_type
29
+ @groups = groups
30
+ @timing = timing
31
+ @time = time
26
32
  end
27
33
 
28
34
  def destination
@@ -38,9 +44,9 @@ module Airbrake
38
44
  'method' => method,
39
45
  'route' => route,
40
46
  'responseType' => response_type,
41
- 'time' => @start_time_utc
47
+ 'time' => @time_utc,
42
48
  }.delete_if { |_key, val| val.nil? }
43
49
  end
44
50
  end
45
- # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
51
+ # rubocop:enable Metrics/ParameterLists
46
52
  end
@@ -4,6 +4,7 @@ module Airbrake
4
4
  #
5
5
  # @api public
6
6
  # @since v3.2.0
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class PerformanceNotifier
8
9
  include Inspectable
9
10
  include Loggable
@@ -11,33 +12,29 @@ module Airbrake
11
12
  def initialize
12
13
  @config = Airbrake::Config.instance
13
14
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
- @sender = AsyncSender.new(:put)
15
- @payload = {}
15
+ @async_sender = AsyncSender.new(:put)
16
+ @sync_sender = SyncSender.new(:put)
16
17
  @schedule_flush = nil
17
- @mutex = Mutex.new
18
18
  @filter_chain = FilterChain.new
19
- @waiting = false
19
+
20
+ @payload = {}.extend(MonitorMixin)
21
+ @has_payload = @payload.new_cond
20
22
  end
21
23
 
22
24
  # @param [Hash] resource
23
25
  # @see Airbrake.notify_query
24
26
  # @see Airbrake.notify_request
25
27
  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)
28
+ @payload.synchronize do
29
+ send_resource(resource, sync: false)
38
30
  end
31
+ end
39
32
 
40
- promise.resolve(:success)
33
+ # @param [Hash] resource
34
+ # @since v4.10.0
35
+ # @see Airbrake.notify_queue_sync
36
+ def notify_sync(resource)
37
+ send_resource(resource, sync: true).value
41
38
  end
42
39
 
43
40
  # @see Airbrake.add_performance_filter
@@ -51,73 +48,97 @@ module Airbrake
51
48
  end
52
49
 
53
50
  def close
54
- @mutex.synchronize do
51
+ @payload.synchronize do
55
52
  @schedule_flush.kill if @schedule_flush
56
- @sender.close
53
+ @async_sender.close
57
54
  logger.debug("#{LOG_LABEL} performance notifier closed")
58
55
  end
59
56
  end
60
57
 
61
58
  private
62
59
 
63
- def update_payload(resource)
64
- @payload[resource] ||= { total: Airbrake::Stat.new }
65
- @payload[resource][:total].increment(resource.start_time, resource.end_time)
60
+ def schedule_flush
61
+ @schedule_flush ||= Thread.new do
62
+ loop do
63
+ @payload.synchronize do
64
+ @last_flush_time ||= MonotonicTime.time_in_s
66
65
 
67
- resource.groups.each do |name, ms|
68
- @payload[resource][name] ||= Airbrake::Stat.new
69
- @payload[resource][name].increment_ms(ms)
66
+ while (MonotonicTime.time_in_s - @last_flush_time) < @flush_period
67
+ @has_payload.wait(@flush_period)
68
+ end
69
+
70
+ if @payload.none?
71
+ @last_flush_time = nil
72
+ next
73
+ end
74
+
75
+ send(@async_sender, @payload, Airbrake::Promise.new)
76
+ @payload.clear
77
+ end
78
+ end
70
79
  end
71
80
  end
72
81
 
73
- def schedule_flush
74
- return if @payload.empty?
82
+ def send_resource(resource, sync:)
83
+ promise = check_configuration(resource)
84
+ return promise if promise.rejected?
75
85
 
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
86
+ @filter_chain.refine(resource)
87
+ if resource.ignored?
88
+ return Promise.new.reject("#{resource.class} was ignored by a filter")
82
89
  end
83
90
 
84
- @schedule_flush ||= spawn_timer
91
+ update_payload(resource)
92
+ if sync || @flush_period == 0
93
+ send(@sync_sender, @payload, promise)
94
+ else
95
+ @has_payload.signal
96
+ schedule_flush
97
+ end
85
98
  end
86
99
 
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)
100
+ def update_payload(resource)
101
+ if (total_stat = @payload[resource])
102
+ @payload.key(total_stat).merge(resource)
103
+ else
104
+ @payload[resource] = { total: Airbrake::Stat.new }
105
+ end
97
106
 
98
- payload = nil
99
- @mutex.synchronize do
100
- payload = @payload
101
- @payload = {}
102
- end
107
+ @payload[resource][:total].increment_ms(resource.timing)
103
108
 
104
- send(payload, Airbrake::Promise.new)
105
- end
109
+ resource.groups.each do |name, ms|
110
+ @payload[resource][name] ||= Airbrake::Stat.new
111
+ @payload[resource][name].increment_ms(ms)
106
112
  end
107
113
  end
108
114
 
109
- def send(payload, promise)
110
- signature = "#{self.class.name}##{__method__}"
111
- raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
115
+ def check_configuration(resource)
116
+ promise = @config.check_configuration
117
+ return promise if promise.rejected?
118
+
119
+ promise = @config.check_performance_options(resource)
120
+ return promise if promise.rejected?
112
121
 
113
- logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
122
+ if resource.timing && resource.timing == 0
123
+ return Promise.new.reject(':timing cannot be zero')
124
+ end
125
+
126
+ Promise.new
127
+ end
128
+
129
+ def send(sender, payload, promise)
130
+ raise "payload cannot be empty. Race?" if payload.none?
114
131
 
115
132
  with_grouped_payload(payload) do |resource_hash, destination|
116
133
  url = URI.join(
117
- @config.host,
118
- "api/v5/projects/#{@config.project_id}/#{destination}"
134
+ @config.apm_host,
135
+ "api/v5/projects/#{@config.project_id}/#{destination}",
119
136
  )
120
- @sender.send(resource_hash, promise, url)
137
+
138
+ logger.debug do
139
+ "#{LOG_LABEL} #{self.class.name}##{__method__}: #{resource_hash}"
140
+ end
141
+ sender.send(resource_hash, promise, url)
121
142
  end
122
143
 
123
144
  promise
@@ -152,4 +173,5 @@ module Airbrake
152
173
  end
153
174
  end
154
175
  end
176
+ # rubocop:enable Metrics/ClassLength
155
177
  end
@@ -103,6 +103,7 @@ module Airbrake
103
103
  # needed for compatibility but it shouldn't exist in the future
104
104
  def value
105
105
  return @value['ok'] if resolved?
106
+
106
107
  @value
107
108
  end
108
109
  end
@@ -4,13 +4,15 @@ module Airbrake
4
4
  # @see Airbrake.notify_query
5
5
  # @api public
6
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
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Query
11
9
  include HashKeyable
12
10
  include Ignorable
13
11
  include Stashable
12
+ include Mergeable
13
+ include Grouppable
14
+
15
+ attr_accessor :method, :route, :query, :func, :file, :line, :timing, :time
14
16
 
15
17
  def initialize(
16
18
  method:,
@@ -19,11 +21,18 @@ module Airbrake
19
21
  func: nil,
20
22
  file: nil,
21
23
  line: nil,
22
- start_time:,
23
- end_time: Time.now
24
+ timing: nil,
25
+ time: Time.now
24
26
  )
25
- @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
26
- super(method, route, query, func, file, line, start_time, end_time)
27
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
28
+ @method = method
29
+ @route = route
30
+ @query = query
31
+ @func = func
32
+ @file = file
33
+ @line = line
34
+ @timing = timing
35
+ @time = time
27
36
  end
28
37
 
29
38
  def destination
@@ -34,21 +43,17 @@ module Airbrake
34
43
  'queries'
35
44
  end
36
45
 
37
- def groups
38
- {}
39
- end
40
-
41
46
  def to_h
42
47
  {
43
48
  'method' => method,
44
49
  'route' => route,
45
50
  'query' => query,
46
- 'time' => @start_time_utc,
51
+ 'time' => @time_utc,
47
52
  'function' => func,
48
53
  'file' => file,
49
- 'line' => line
54
+ 'line' => line,
50
55
  }.delete_if { |_key, val| val.nil? }
51
56
  end
52
- # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
57
+ # rubocop:enable Metrics/ParameterLists
53
58
  end
54
59
  end
@@ -0,0 +1,65 @@
1
+ module Airbrake
2
+ # Queue represents a queue (worker).
3
+ #
4
+ # @see Airbrake.notify_queue
5
+ # @api public
6
+ # @since v4.9.0
7
+ class Queue
8
+ include HashKeyable
9
+ include Ignorable
10
+ include Stashable
11
+
12
+ attr_accessor :queue, :error_count, :groups, :timing, :time
13
+
14
+ def initialize(
15
+ queue:,
16
+ error_count:,
17
+ groups: {},
18
+ timing: nil,
19
+ time: Time.now
20
+ )
21
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
22
+ @queue = queue
23
+ @error_count = error_count
24
+ @groups = groups
25
+ @timing = timing
26
+ @time = time
27
+ end
28
+
29
+ def destination
30
+ 'queues-stats'
31
+ end
32
+
33
+ def cargo
34
+ 'queues'
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ 'queue' => queue,
40
+ 'errorCount' => error_count,
41
+ 'time' => @time_utc,
42
+ }
43
+ end
44
+
45
+ def hash
46
+ {
47
+ 'queue' => queue,
48
+ 'time' => @time_utc,
49
+ }.hash
50
+ end
51
+
52
+ def merge(other)
53
+ self.error_count += other.error_count
54
+ end
55
+
56
+ # Queues don't have routes, but we want to define this to make sure our
57
+ # filter API is consistent (other models define this property)
58
+ #
59
+ # @return [String] empty route
60
+ # @see https://github.com/airbrake/airbrake-ruby/pull/537
61
+ def route
62
+ ''
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,105 @@
1
+ module Airbrake
2
+ # RemoteSettings polls the remote config of the passed project at fixed
3
+ # intervals. The fetched config is yielded as a callback parameter so that the
4
+ # invoker can define read config values.
5
+ #
6
+ # @example Disable/enable error notifications based on the remote value
7
+ # RemoteSettings.poll do |data|
8
+ # config.error_notifications = data.error_notifications?
9
+ # end
10
+ #
11
+ # @since v5.0.0
12
+ # @api private
13
+ class RemoteSettings
14
+ include Airbrake::Loggable
15
+
16
+ # @return [Hash{Symbol=>String}] metadata to be attached to every GET
17
+ # request
18
+ QUERY_PARAMS = URI.encode_www_form(
19
+ notifier_name: Airbrake::NOTIFIER_INFO[:name],
20
+ notifier_version: Airbrake::NOTIFIER_INFO[:version],
21
+ os: RUBY_PLATFORM,
22
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
23
+ ).freeze
24
+
25
+ # @return [String]
26
+ HTTP_OK = '200'.freeze
27
+
28
+ # Polls remote config of the given project.
29
+ #
30
+ # @param [Integer] project_id
31
+ # @param [String] host
32
+ # @yield [data]
33
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
34
+ # @return [Airbrake::RemoteSettings]
35
+ def self.poll(project_id, host, &block)
36
+ new(project_id, host, &block).poll
37
+ end
38
+
39
+ # @param [Integer] project_id
40
+ # @yield [data]
41
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
42
+ def initialize(project_id, host, &block)
43
+ @data = SettingsData.new(project_id, {})
44
+ @host = host
45
+ @block = block
46
+ @poll = nil
47
+ end
48
+
49
+ # Polls remote config of the given project in background.
50
+ #
51
+ # @return [self]
52
+ def poll
53
+ @poll ||= Thread.new do
54
+ @block.call(@data)
55
+
56
+ loop do
57
+ @block.call(@data.merge!(fetch_config))
58
+ sleep(@data.interval)
59
+ end
60
+ end
61
+
62
+ self
63
+ end
64
+
65
+ # Stops the background poller thread.
66
+ #
67
+ # @return [void]
68
+ def stop_polling
69
+ @poll.kill if @poll
70
+ end
71
+
72
+ private
73
+
74
+ def fetch_config
75
+ response = nil
76
+ begin
77
+ response = Net::HTTP.get_response(build_config_uri)
78
+ rescue StandardError => ex
79
+ logger.error(ex)
80
+ return {}
81
+ end
82
+
83
+ unless response.code == HTTP_OK
84
+ logger.error(response.body)
85
+ return {}
86
+ end
87
+
88
+ json = nil
89
+ begin
90
+ json = JSON.parse(response.body)
91
+ rescue JSON::ParserError => ex
92
+ logger.error(ex)
93
+ return {}
94
+ end
95
+
96
+ json
97
+ end
98
+
99
+ def build_config_uri
100
+ uri = URI(@data.config_route(@host))
101
+ uri.query = QUERY_PARAMS
102
+ uri
103
+ end
104
+ end
105
+ end