airbrake-ruby 4.13.3-java → 5.0.0.rc.1-java

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 (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