airbrake-ruby 4.13.3 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +23 -32
  3. data/lib/airbrake-ruby/async_sender.rb +1 -1
  4. data/lib/airbrake-ruby/config.rb +65 -13
  5. data/lib/airbrake-ruby/config/processor.rb +80 -0
  6. data/lib/airbrake-ruby/config/validator.rb +4 -0
  7. data/lib/airbrake-ruby/filter_chain.rb +15 -1
  8. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +2 -1
  9. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  10. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  11. data/lib/airbrake-ruby/filters/keys_filter.rb +5 -5
  12. data/lib/airbrake-ruby/notice_notifier.rb +6 -0
  13. data/lib/airbrake-ruby/performance_notifier.rb +1 -1
  14. data/lib/airbrake-ruby/remote_settings.rb +128 -0
  15. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  16. data/lib/airbrake-ruby/sync_sender.rb +1 -1
  17. data/lib/airbrake-ruby/thread_pool.rb +1 -0
  18. data/lib/airbrake-ruby/version.rb +1 -1
  19. data/spec/airbrake_spec.rb +71 -36
  20. data/spec/config/processor_spec.rb +223 -0
  21. data/spec/config/validator_spec.rb +18 -1
  22. data/spec/config_spec.rb +38 -6
  23. data/spec/filter_chain_spec.rb +27 -0
  24. data/spec/filters/git_last_checkout_filter_spec.rb +20 -3
  25. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +10 -10
  26. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +10 -10
  27. data/spec/filters/sql_filter_spec.rb +3 -3
  28. data/spec/notice_notifier/options_spec.rb +4 -4
  29. data/spec/performance_notifier_spec.rb +2 -2
  30. data/spec/remote_settings/settings_data_spec.rb +327 -0
  31. data/spec/remote_settings_spec.rb +212 -0
  32. data/spec/sync_sender_spec.rb +3 -1
  33. data/spec/thread_pool_spec.rb +25 -5
  34. metadata +20 -12
@@ -82,6 +82,12 @@ module Airbrake
82
82
  @context.merge!(context)
83
83
  end
84
84
 
85
+ # @return [Boolean]
86
+ # @since v4.14.0
87
+ def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
88
+ @filter_chain.includes?(filter_class)
89
+ end
90
+
85
91
  private
86
92
 
87
93
  def convert_to_exception(ex)
@@ -144,7 +144,7 @@ module Airbrake
144
144
 
145
145
  with_grouped_payload(payload) do |resource_hash, destination|
146
146
  url = URI.join(
147
- @config.host,
147
+ @config.apm_host,
148
148
  "api/v5/projects/#{@config.project_id}/#{destination}",
149
149
  )
150
150
 
@@ -0,0 +1,128 @@
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
+ # When {#poll} is called, it will try to load remote settings from disk, so
12
+ # that it doesn't wait on the result from the API call.
13
+ #
14
+ # When {#stop_polling} is called, the current config will be dumped to disk.
15
+ #
16
+ # @since ?.?.?
17
+ # @api private
18
+ class RemoteSettings
19
+ include Airbrake::Loggable
20
+
21
+ # @return [String] the path to the persistent config
22
+ CONFIG_DUMP_PATH = File.join(
23
+ File.expand_path(__dir__),
24
+ '../../config/config.json',
25
+ ).freeze
26
+
27
+ # Polls remote config of the given project.
28
+ #
29
+ # @param [Integer] project_id
30
+ # @yield [data]
31
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
32
+ # @return [Airbrake::RemoteSettings]
33
+ def self.poll(project_id, &block)
34
+ new(project_id, &block).poll
35
+ end
36
+
37
+ # @param [Integer] project_id
38
+ # @yield [data]
39
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
40
+ def initialize(project_id, &block)
41
+ @data = SettingsData.new(project_id, {})
42
+ @block = block
43
+ @poll = nil
44
+ end
45
+
46
+ # Polls remote config of the given project in background. Loads local config
47
+ # first (if exists).
48
+ #
49
+ # @return [self]
50
+ def poll
51
+ @poll ||= Thread.new do
52
+ begin
53
+ load_config
54
+ rescue StandardError => ex
55
+ logger.error("#{LOG_LABEL} config loading failed: #{ex}")
56
+ end
57
+
58
+ @block.call(@data)
59
+
60
+ loop do
61
+ @block.call(@data.merge!(fetch_config))
62
+ sleep(@data.interval)
63
+ end
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ # Stops the background poller thread. Dumps current config to disk.
70
+ #
71
+ # @return [void]
72
+ def stop_polling
73
+ @poll.kill if @poll
74
+
75
+ begin
76
+ dump_config
77
+ rescue StandardError => ex
78
+ logger.error("#{LOG_LABEL} config dumping failed: #{ex}")
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def fetch_config
85
+ response = nil
86
+ begin
87
+ response = Net::HTTP.get(URI(@data.config_route))
88
+ rescue StandardError => ex
89
+ logger.error(ex)
90
+ return {}
91
+ end
92
+
93
+ # AWS S3 API returns XML when request is not valid. In this case we just
94
+ # print the returned body and exit the method.
95
+ if response.start_with?('<?xml ')
96
+ logger.error(response)
97
+ return {}
98
+ end
99
+
100
+ json = nil
101
+ begin
102
+ json = JSON.parse(response)
103
+ rescue JSON::ParserError => ex
104
+ logger.error(ex)
105
+ return {}
106
+ end
107
+
108
+ json
109
+ end
110
+
111
+ def load_config
112
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
113
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
114
+
115
+ return unless File.exist?(CONFIG_DUMP_PATH)
116
+
117
+ config = File.read(CONFIG_DUMP_PATH)
118
+ @data.merge!(JSON.parse(config))
119
+ end
120
+
121
+ def dump_config
122
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
123
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
124
+
125
+ File.write(CONFIG_DUMP_PATH, JSON.dump(@data.to_h))
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,116 @@
1
+ module Airbrake
2
+ class RemoteSettings
3
+ # SettingsData is a container, which wraps JSON payload returned by the
4
+ # remote settings API. It exposes the payload via convenient methods and
5
+ # also ensures that in case some data from the payload is missing, a default
6
+ # value would be returned instead.
7
+ #
8
+ # @example
9
+ # # Create the object and pass initial data (empty hash).
10
+ # settings_data = SettingsData.new({})
11
+ #
12
+ # settings_data.interval #=> 600
13
+ #
14
+ # @since ?.?.?
15
+ # @api private
16
+ class SettingsData
17
+ # @return [Integer] how frequently we should poll the config API
18
+ DEFAULT_INTERVAL = 600
19
+
20
+ # @return [String] API version of the S3 API to poll
21
+ API_VER = '2020-06-18'.freeze
22
+
23
+ # @return [String] what URL to poll
24
+ CONFIG_ROUTE_PATTERN =
25
+ 'https://%<bucket>s.s3.amazonaws.com/' \
26
+ "#{API_VER}/config/%<project_id>s/config.json".freeze
27
+
28
+ # @return [Hash{Symbol=>String}] the hash of all supported settings where
29
+ # the value is the name of the setting returned by the API
30
+ SETTINGS = {
31
+ errors: 'errors',
32
+ apm: 'apm',
33
+ }.freeze
34
+
35
+ # @param [Integer] project_id
36
+ # @param [Hash{String=>Object}] data
37
+ def initialize(project_id, data)
38
+ @project_id = project_id
39
+ @data = data
40
+ end
41
+
42
+ # Merges the given +hash+ with internal data.
43
+ #
44
+ # @param [Hash{String=>Object}] hash
45
+ # @return [self]
46
+ def merge!(hash)
47
+ @data.merge!(hash)
48
+
49
+ self
50
+ end
51
+
52
+ # @return [Integer] how frequently we should poll for the config
53
+ def interval
54
+ return DEFAULT_INTERVAL if !@data.key?('poll_sec') || !@data['poll_sec']
55
+
56
+ @data['poll_sec'] > 0 ? @data['poll_sec'] : DEFAULT_INTERVAL
57
+ end
58
+
59
+ # @return [String] where the config is stored on S3.
60
+ def config_route
61
+ if !@data.key?('config_route') || !@data['config_route']
62
+ return format(
63
+ CONFIG_ROUTE_PATTERN,
64
+ bucket: 'staging-notifier-configs',
65
+ project_id: @project_id,
66
+ )
67
+ end
68
+
69
+ @data['config_route']
70
+ end
71
+
72
+ # @return [Boolean] whether error notifications are enabled
73
+ def error_notifications?
74
+ return true unless (s = find_setting(SETTINGS[:errors]))
75
+
76
+ s['enabled']
77
+ end
78
+
79
+ # @return [Boolean] whether APM is enabled
80
+ def performance_stats?
81
+ return true unless (s = find_setting(SETTINGS[:apm]))
82
+
83
+ s['enabled']
84
+ end
85
+
86
+ # @return [String, nil] the host, which provides the API endpoint to which
87
+ # exceptions should be sent
88
+ def error_host
89
+ return unless (s = find_setting(SETTINGS[:errors]))
90
+
91
+ s['endpoint']
92
+ end
93
+
94
+ # @return [String, nil] the host, which provides the API endpoint to which
95
+ # APM data should be sent
96
+ def apm_host
97
+ return unless (s = find_setting(SETTINGS[:apm]))
98
+
99
+ s['endpoint']
100
+ end
101
+
102
+ # @return [Hash{String=>Object}] raw representation of JSON payload
103
+ def to_h
104
+ @data.dup
105
+ end
106
+
107
+ private
108
+
109
+ def find_setting(name)
110
+ return unless @data.key?('settings')
111
+
112
+ @data['settings'].find { |s| s['name'] == name }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -23,7 +23,7 @@ module Airbrake
23
23
  # @param [#to_json] data
24
24
  # @param [URI::HTTPS] endpoint
25
25
  # @return [Hash{String=>String}] the parsed HTTP response
26
- def send(data, promise, endpoint = @config.endpoint)
26
+ def send(data, promise, endpoint = @config.error_endpoint)
27
27
  return promise if rate_limited_ip?(promise)
28
28
 
29
29
  response = nil
@@ -83,6 +83,7 @@ module Airbrake
83
83
 
84
84
  if @pid != Process.pid && @workers.list.empty?
85
85
  @pid = Process.pid
86
+ @workers = ThreadGroup.new
86
87
  spawn_workers
87
88
  end
88
89
 
@@ -2,5 +2,5 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '4.13.3'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '5.0.0.rc.1'.freeze
6
6
  end
@@ -1,4 +1,13 @@
1
1
  RSpec.describe Airbrake do
2
+ let(:remote_settings) { instance_double(Airbrake::RemoteSettings) }
3
+
4
+ before do
5
+ allow(Airbrake::RemoteSettings).to receive(:poll).and_return(remote_settings)
6
+ allow(remote_settings).to receive(:stop_polling)
7
+ end
8
+
9
+ after { described_class.instance_variable_set(:@remote_settings, nil) }
10
+
2
11
  it "gets initialized with a performance notifier" do
3
12
  expect(described_class.performance_notifier).not_to be_nil
4
13
  end
@@ -85,43 +94,55 @@ RSpec.describe Airbrake do
85
94
  expect(described_class.notice_notifier).not_to receive(:add_filter)
86
95
  10.times { described_class.configure {} }
87
96
  end
97
+
98
+ it "appends some default filters" do
99
+ allow(described_class.notice_notifier).to receive(:add_filter)
100
+ expect(described_class.notice_notifier).to receive(:add_filter).with(
101
+ an_instance_of(Airbrake::Filters::RootDirectoryFilter),
102
+ )
103
+
104
+ described_class.configure do |c|
105
+ c.project_id = 1
106
+ c.project_key = '2'
107
+ end
108
+ end
88
109
  end
89
110
 
90
- context "when blacklist_keys gets configured" do
91
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
111
+ context "when blocklist_keys gets configured" do
112
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
92
113
 
93
- it "adds blacklist filter" do
94
- expect(Airbrake.notice_notifier).to receive(:add_filter)
95
- .with(an_instance_of(Airbrake::Filters::KeysBlacklist))
96
- described_class.configure { |c| c.blacklist_keys = %w[password] }
114
+ it "adds blocklist filter" do
115
+ expect(described_class.notice_notifier).to receive(:add_filter)
116
+ .with(an_instance_of(Airbrake::Filters::KeysBlocklist))
117
+ described_class.configure { |c| c.blocklist_keys = %w[password] }
97
118
  end
98
119
 
99
- it "initializes blacklist with specified parameters" do
100
- expect(Airbrake::Filters::KeysBlacklist).to receive(:new).with(%w[password])
101
- described_class.configure { |c| c.blacklist_keys = %w[password] }
120
+ it "initializes blocklist with specified parameters" do
121
+ expect(Airbrake::Filters::KeysBlocklist).to receive(:new).with(%w[password])
122
+ described_class.configure { |c| c.blocklist_keys = %w[password] }
102
123
  end
103
124
  end
104
125
 
105
- context "when whitelist_keys gets configured" do
106
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
126
+ context "when allowlist_keys gets configured" do
127
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
107
128
 
108
- it "adds whitelist filter" do
109
- expect(Airbrake.notice_notifier).to receive(:add_filter)
110
- .with(an_instance_of(Airbrake::Filters::KeysWhitelist))
111
- described_class.configure { |c| c.whitelist_keys = %w[banana] }
129
+ it "adds allowlist filter" do
130
+ expect(described_class.notice_notifier).to receive(:add_filter)
131
+ .with(an_instance_of(Airbrake::Filters::KeysAllowlist))
132
+ described_class.configure { |c| c.allowlist_keys = %w[banana] }
112
133
  end
113
134
 
114
- it "initializes whitelist with specified parameters" do
115
- expect(Airbrake::Filters::KeysWhitelist).to receive(:new).with(%w[banana])
116
- described_class.configure { |c| c.whitelist_keys = %w[banana] }
135
+ it "initializes allowlist with specified parameters" do
136
+ expect(Airbrake::Filters::KeysAllowlist).to receive(:new).with(%w[banana])
137
+ described_class.configure { |c| c.allowlist_keys = %w[banana] }
117
138
  end
118
139
  end
119
140
 
120
141
  context "when root_directory gets configured" do
121
- before { allow(Airbrake.notice_notifier).to receive(:add_filter) }
142
+ before { allow(described_class.notice_notifier).to receive(:add_filter) }
122
143
 
123
144
  it "adds root directory filter" do
124
- expect(Airbrake.notice_notifier).to receive(:add_filter)
145
+ expect(described_class.notice_notifier).to receive(:add_filter)
125
146
  .with(an_instance_of(Airbrake::Filters::RootDirectoryFilter))
126
147
  described_class.configure { |c| c.root_directory = '/my/path' }
127
148
  end
@@ -133,7 +154,7 @@ RSpec.describe Airbrake do
133
154
  end
134
155
 
135
156
  it "adds git revision filter" do
136
- expect(Airbrake.notice_notifier).to receive(:add_filter)
157
+ expect(described_class.notice_notifier).to receive(:add_filter)
137
158
  .with(an_instance_of(Airbrake::Filters::GitRevisionFilter))
138
159
  described_class.configure { |c| c.root_directory = '/my/path' }
139
160
  end
@@ -145,7 +166,7 @@ RSpec.describe Airbrake do
145
166
  end
146
167
 
147
168
  it "adds git repository filter" do
148
- expect(Airbrake.notice_notifier).to receive(:add_filter)
169
+ expect(described_class.notice_notifier).to receive(:add_filter)
149
170
  .with(an_instance_of(Airbrake::Filters::GitRepositoryFilter))
150
171
  described_class.configure { |c| c.root_directory = '/my/path' }
151
172
  end
@@ -157,7 +178,7 @@ RSpec.describe Airbrake do
157
178
  end
158
179
 
159
180
  it "adds git last checkout filter" do
160
- expect(Airbrake.notice_notifier).to receive(:add_filter)
181
+ expect(described_class.notice_notifier).to receive(:add_filter)
161
182
  .with(an_instance_of(Airbrake::Filters::GitLastCheckoutFilter))
162
183
  described_class.configure { |c| c.root_directory = '/my/path' }
163
184
  end
@@ -170,7 +191,7 @@ RSpec.describe Airbrake do
170
191
  end
171
192
  end
172
193
 
173
- describe "#notify_request" do
194
+ describe ".notify_request" do
174
195
  context "when :stash key is not provided" do
175
196
  it "doesn't add anything to the stash of the request" do
176
197
  expect(described_class.performance_notifier).to receive(:notify) do |request|
@@ -205,7 +226,7 @@ RSpec.describe Airbrake do
205
226
  end
206
227
  end
207
228
 
208
- describe "#notify_request_sync" do
229
+ describe ".notify_request_sync" do
209
230
  it "notifies request synchronously" do
210
231
  expect(described_class.performance_notifier).to receive(:notify_sync)
211
232
 
@@ -221,7 +242,7 @@ RSpec.describe Airbrake do
221
242
  end
222
243
  end
223
244
 
224
- describe "#notify_query" do
245
+ describe ".notify_query" do
225
246
  context "when :stash key is not provided" do
226
247
  it "doesn't add anything to the stash of the query" do
227
248
  expect(described_class.performance_notifier).to receive(:notify) do |query|
@@ -256,7 +277,7 @@ RSpec.describe Airbrake do
256
277
  end
257
278
  end
258
279
 
259
- describe "#notify_query_sync" do
280
+ describe ".notify_query_sync" do
260
281
  it "notifies query synchronously" do
261
282
  expect(described_class.performance_notifier).to receive(:notify_sync)
262
283
 
@@ -272,7 +293,7 @@ RSpec.describe Airbrake do
272
293
  end
273
294
  end
274
295
 
275
- describe "#notify_performance_breakdown" do
296
+ describe ".notify_performance_breakdown" do
276
297
  context "when :stash key is not provided" do
277
298
  it "doesn't add anything to the stash of the performance breakdown" do
278
299
  expect(described_class.performance_notifier).to receive(:notify) do |query|
@@ -310,7 +331,7 @@ RSpec.describe Airbrake do
310
331
  end
311
332
  end
312
333
 
313
- describe "#notify_performance_breakdown_sync" do
334
+ describe ".notify_performance_breakdown_sync" do
314
335
  it "notifies performance breakdown synchronously" do
315
336
  expect(described_class.performance_notifier).to receive(:notify_sync)
316
337
 
@@ -327,7 +348,7 @@ RSpec.describe Airbrake do
327
348
  end
328
349
  end
329
350
 
330
- describe "#notify_queue" do
351
+ describe ".notify_queue" do
331
352
  context "when :stash key is not provided" do
332
353
  it "doesn't add anything to the stash of the queue" do
333
354
  expect(described_class.performance_notifier).to receive(:notify) do |queue|
@@ -358,7 +379,7 @@ RSpec.describe Airbrake do
358
379
  end
359
380
  end
360
381
 
361
- describe "#notify_queue_sync" do
382
+ describe ".notify_queue_sync" do
362
383
  it "notifies queue synchronously" do
363
384
  expect(described_class.performance_notifier).to receive(:notify_sync)
364
385
 
@@ -392,33 +413,47 @@ RSpec.describe Airbrake do
392
413
  end
393
414
 
394
415
  describe ".close" do
395
- after { Airbrake.reset }
416
+ after { described_class.reset }
396
417
 
397
418
  context "when notice_notifier is defined" do
398
419
  it "gets closed" do
399
- expect(Airbrake.notice_notifier).to receive(:close)
420
+ expect(described_class.notice_notifier).to receive(:close)
400
421
  end
401
422
  end
402
423
 
403
424
  context "when notice_notifier is undefined" do
404
425
  it "doesn't get closed (because it wasn't initialized)" do
405
- Airbrake.instance_variable_set(:@notice_notifier, nil)
426
+ described_class.instance_variable_set(:@notice_notifier, nil)
406
427
  expect_any_instance_of(Airbrake::NoticeNotifier).not_to receive(:close)
407
428
  end
408
429
  end
409
430
 
410
431
  context "when performance_notifier is defined" do
411
432
  it "gets closed" do
412
- expect(Airbrake.performance_notifier).to receive(:close)
433
+ expect(described_class.performance_notifier).to receive(:close)
413
434
  end
414
435
  end
415
436
 
416
437
  context "when perforance_notifier is undefined" do
417
438
  it "doesn't get closed (because it wasn't initialized)" do
418
- Airbrake.instance_variable_set(:@performance_notifier, nil)
439
+ described_class.instance_variable_set(:@performance_notifier, nil)
419
440
  expect_any_instance_of(Airbrake::PerformanceNotifier)
420
441
  .not_to receive(:close)
421
442
  end
422
443
  end
444
+
445
+ context "when remote settings are defined" do
446
+ it "stops polling" do
447
+ described_class.instance_variable_set(:@remote_settings, remote_settings)
448
+ expect(remote_settings).to receive(:stop_polling)
449
+ end
450
+ end
451
+
452
+ context "when remote settings are undefined" do
453
+ it "doesn't stop polling (because they weren't initialized)" do
454
+ described_class.instance_variable_set(:@remote_settings, nil)
455
+ expect(remote_settings).not_to receive(:stop_polling)
456
+ end
457
+ end
423
458
  end
424
459
  end