airbrake-ruby 4.15.0 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9737dfd8078f14a0150525e1c8b226bcaa5bb87e7627542d7a1a25a27d3c191d
4
- data.tar.gz: 653ff8929bc7199a77c59ead004167a38d214741157282d93e947c3d5ca8ecd6
3
+ metadata.gz: f8307b88b3240291a93fa8e8590997aca70032e424edff8f3ef2cd8c7220c4b3
4
+ data.tar.gz: 53a68e1f1bab4158d773593ad201dce948a0df60c0fd1b80a27451a96dc5596c
5
5
  SHA512:
6
- metadata.gz: 21d60bd1415bec1e0ed3b89891f5b35e56f019b09dcb50c05f1e55b2176a2976a566c95e0a3fc4cdce2601a1b4b0e786b1ddbcc67188da6df51bd5c94188e54d
7
- data.tar.gz: 3c3b2312b7945685c6199ecd21f23601b752e1d4234d35461db2f4c7cf0318c24ee263df2536e9cffb7e107002a5a5be10af1cf7eecc16ac65e07cd948f2c68e
6
+ metadata.gz: fa9bcb8eac54050e2065c850ce7056a219c7e6a3992bac5b2eeab36d95a39d861bfbd4a9e81ba6d377b55e32679723f013cbcbc213ab6f6a29715f544a719259
7
+ data.tar.gz: '08fdf62186815e85696acd66641f60bfeca556c2a3e30c0b7d7c49c3f02bdd208d795aceee9af65a4683773580682f8efa96a61434a716118a8a85346432d41f'
@@ -12,6 +12,9 @@ require 'airbrake-ruby/mergeable'
12
12
  require 'airbrake-ruby/grouppable'
13
13
  require 'airbrake-ruby/config'
14
14
  require 'airbrake-ruby/config/validator'
15
+ require 'airbrake-ruby/config/processor'
16
+ require 'airbrake-ruby/remote_settings/settings_data'
17
+ require 'airbrake-ruby/remote_settings'
15
18
  require 'airbrake-ruby/promise'
16
19
  require 'airbrake-ruby/thread_pool'
17
20
  require 'airbrake-ruby/sync_sender'
@@ -117,7 +120,15 @@ module Airbrake
117
120
  def configure
118
121
  yield config = Airbrake::Config.instance
119
122
  Airbrake::Loggable.instance = config.logger
120
- process_config_options(config)
123
+
124
+ config_processor = Airbrake::Config::Processor.new(config)
125
+
126
+ config_processor.process_blocklist(notice_notifier)
127
+ config_processor.process_allowlist(notice_notifier)
128
+
129
+ @remote_settings ||= config_processor.process_remote_configuration
130
+
131
+ config_processor.add_filters(notice_notifier)
121
132
  end
122
133
 
123
134
  # @since v4.2.3
@@ -260,8 +271,8 @@ module Airbrake
260
271
  # Airbrake.close
261
272
  # Airbrake.notify('App crashed!') #=> raises Airbrake::Error
262
273
  #
263
- # @return [void]
264
- # rubocop:disable Style/GuardClause, Style/IfUnlessModifier
274
+ # @return [nil]
275
+ # rubocop:disable Style/IfUnlessModifier, Metrics/CyclomaticComplexity
265
276
  def close
266
277
  if defined?(@notice_notifier) && @notice_notifier
267
278
  @notice_notifier.close
@@ -270,8 +281,14 @@ module Airbrake
270
281
  if defined?(@performance_notifier) && @performance_notifier
271
282
  @performance_notifier.close
272
283
  end
284
+
285
+ if defined?(@remote_settings) && @remote_settings
286
+ @remote_settings.stop_polling
287
+ end
288
+
289
+ nil
273
290
  end
274
- # rubocop:enable Style/GuardClause, Style/IfUnlessModifier
291
+ # rubocop:enable Style/IfUnlessModifier, Metrics/CyclomaticComplexity
275
292
 
276
293
  # Pings the Airbrake Deploy API endpoint about the occurred deploy.
277
294
  #
@@ -567,35 +584,6 @@ module Airbrake
567
584
  self.notice_notifier = NoticeNotifier.new
568
585
  self.deploy_notifier = DeployNotifier.new
569
586
  end
570
-
571
- private
572
-
573
- # rubocop:disable Metrics/AbcSize
574
- def process_config_options(config)
575
- if config.blocklist_keys.any?
576
- blocklist = Airbrake::Filters::KeysBlocklist.new(config.blocklist_keys)
577
- notice_notifier.add_filter(blocklist)
578
- end
579
-
580
- if config.allowlist_keys.any?
581
- allowlist = Airbrake::Filters::KeysAllowlist.new(config.allowlist_keys)
582
- notice_notifier.add_filter(allowlist)
583
- end
584
-
585
- return unless config.root_directory
586
-
587
- [
588
- Airbrake::Filters::RootDirectoryFilter,
589
- Airbrake::Filters::GitRevisionFilter,
590
- Airbrake::Filters::GitRepositoryFilter,
591
- Airbrake::Filters::GitLastCheckoutFilter,
592
- ].each do |filter|
593
- next if notice_notifier.has_filter?(filter)
594
-
595
- notice_notifier.add_filter(filter.new(config.root_directory))
596
- end
597
- end
598
- # rubocop:enable Metrics/AbcSize
599
587
  end
600
588
  end
601
589
  # rubocop:enable Metrics/ModuleLength
@@ -16,7 +16,7 @@ module Airbrake
16
16
  #
17
17
  # @param [Hash] payload Whatever needs to be sent
18
18
  # @return [Airbrake::Promise]
19
- def send(payload, promise, endpoint = @config.endpoint)
19
+ def send(payload, promise, endpoint = @config.error_endpoint)
20
20
  unless thread_pool << [payload, promise, endpoint]
21
21
  return promise.reject(
22
22
  "AsyncSender has reached its capacity of #{@config.queue_size}",
@@ -4,6 +4,7 @@ module Airbrake
4
4
  #
5
5
  # @api public
6
6
  # @since v1.0.0
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class Config
8
9
  # @return [Integer] the project identificator. This value *must* be set.
9
10
  # @api public
@@ -46,6 +47,17 @@ module Airbrake
46
47
  # @api public
47
48
  attr_accessor :host
48
49
 
50
+ # @since v?.?.?
51
+ alias error_host host
52
+ # @since v?.?.?
53
+ alias error_host= host=
54
+
55
+ # @return [String] the host, which provides the API endpoint to which
56
+ # APM data should be sent
57
+ # @api public
58
+ # @since v?.?.?
59
+ attr_accessor :apm_host
60
+
49
61
  # @return [String, Pathname] the working directory of your project
50
62
  # @api public
51
63
  attr_accessor :root_directory
@@ -113,6 +125,18 @@ module Airbrake
113
125
  # @since v4.12.0
114
126
  attr_accessor :job_stats
115
127
 
128
+ # @return [Boolean] true if the library should send error reports to
129
+ # Airbrake, false otherwise
130
+ # @api public
131
+ # @since ?.?.?
132
+ attr_accessor :error_notifications
133
+
134
+ # @note Not for public use!
135
+ # @return [Boolean]
136
+ # @api private
137
+ # @since ?.?.?
138
+ attr_accessor :__remote_configuration
139
+
116
140
  class << self
117
141
  # @return [Config]
118
142
  attr_writer :instance
@@ -134,7 +158,8 @@ module Airbrake
134
158
  self.logger = ::Logger.new(File::NULL).tap { |l| l.level = Logger::WARN }
135
159
  self.project_id = user_config[:project_id]
136
160
  self.project_key = user_config[:project_key]
137
- self.host = 'https://api.airbrake.io'
161
+ self.error_host = 'https://api.airbrake.io'
162
+ self.apm_host = 'https://api.airbrake.io'
138
163
 
139
164
  self.ignore_environments = []
140
165
 
@@ -153,6 +178,8 @@ module Airbrake
153
178
  self.performance_stats_flush_period = 15
154
179
  self.query_stats = true
155
180
  self.job_stats = true
181
+ self.error_notifications = true
182
+ self.__remote_configuration = false
156
183
 
157
184
  merge(user_config)
158
185
  end
@@ -176,14 +203,14 @@ module Airbrake
176
203
  self.allowlist_keys = keys
177
204
  end
178
205
 
179
- # The full URL to the Airbrake Notice API. Based on the +:host+ option.
206
+ # The full URL to the Airbrake Notice API. Based on the +:error_host+ option.
180
207
  # @return [URI] the endpoint address
181
- def endpoint
182
- @endpoint ||=
208
+ def error_endpoint
209
+ @error_endpoint ||=
183
210
  begin
184
- self.host = ('https://' << host) if host !~ %r{\Ahttps?://}
211
+ self.error_host = ('https://' << error_host) if error_host !~ %r{\Ahttps?://}
185
212
  api = "api/v3/projects/#{project_id}/notices"
186
- URI.join(host, api)
213
+ URI.join(error_host, api)
187
214
  end
188
215
  end
189
216
 
@@ -261,4 +288,5 @@ module Airbrake
261
288
  raise Airbrake::Error, "unknown option '#{option}'"
262
289
  end
263
290
  end
291
+ # rubocop:enable Metrics/ClassLength
264
292
  end
@@ -0,0 +1,80 @@
1
+ module Airbrake
2
+ class Config
3
+ # Processor is a helper class, which is responsible for setting default
4
+ # config values, default notifier filters and remote configuration changes.
5
+ #
6
+ # @since v?.?.?
7
+ # @api private
8
+ class Processor
9
+ # @param [Airbrake::Config] config
10
+ # @return [Airbrake::Config::Processor]
11
+ def self.process(config)
12
+ new(config).process
13
+ end
14
+
15
+ # @param [Airbrake::Config] config
16
+ def initialize(config)
17
+ @config = config
18
+ @blocklist_keys = @config.blocklist_keys
19
+ @allowlist_keys = @config.allowlist_keys
20
+ @project_id = @config.project_id
21
+ end
22
+
23
+ # @param [Airbrake::NoticeNotifier] notifier
24
+ # @return [void]
25
+ def process_blocklist(notifier)
26
+ return if @blocklist_keys.none?
27
+
28
+ blocklist = Airbrake::Filters::KeysBlocklist.new(@blocklist_keys)
29
+ notifier.add_filter(blocklist)
30
+ end
31
+
32
+ # @param [Airbrake::NoticeNotifier] notifier
33
+ # @return [void]
34
+ def process_allowlist(notifier)
35
+ return if @allowlist_keys.none?
36
+
37
+ allowlist = Airbrake::Filters::KeysAllowlist.new(@allowlist_keys)
38
+ notifier.add_filter(allowlist)
39
+ end
40
+
41
+ # @return [Airbrake::RemoteSettings]
42
+ def process_remote_configuration
43
+ return if !@project_id || !@config.__remote_configuration
44
+
45
+ RemoteSettings.poll(@project_id, &method(:poll_callback))
46
+ end
47
+
48
+ # @param [Airbrake::NoticeNotifier] notifier
49
+ # @return [void]
50
+ def add_filters(notifier)
51
+ return unless @config.root_directory
52
+
53
+ [
54
+ Airbrake::Filters::RootDirectoryFilter,
55
+ Airbrake::Filters::GitRevisionFilter,
56
+ Airbrake::Filters::GitRepositoryFilter,
57
+ Airbrake::Filters::GitLastCheckoutFilter,
58
+ ].each do |filter|
59
+ next if notifier.has_filter?(filter)
60
+
61
+ notifier.add_filter(filter.new(@config.root_directory))
62
+ end
63
+ end
64
+
65
+ # @param [Airbrake::RemoteSettings::SettingsData] data
66
+ # @return [void]
67
+ def poll_callback(data)
68
+ @config.logger.debug(
69
+ "#{LOG_LABEL} applying remote settings: #{data.to_h}",
70
+ )
71
+
72
+ @config.error_host = data.error_host if data.error_host
73
+ @config.apm_host = data.apm_host if data.apm_host
74
+
75
+ @config.error_notifications = data.error_notifications?
76
+ @config.performance_stats = data.performance_stats?
77
+ end
78
+ end
79
+ end
80
+ end
@@ -44,6 +44,10 @@ module Airbrake
44
44
  def check_notify_ability(config)
45
45
  promise = Airbrake::Promise.new
46
46
 
47
+ unless config.error_notifications
48
+ return promise.reject('error notifications are disabled')
49
+ end
50
+
47
51
  if ignored_environment?(config)
48
52
  return promise.reject(
49
53
  "current environment '#{config.environment}' is ignored",
@@ -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