airbrake-ruby 4.8.0 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
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