airbrake-ruby 4.15.0-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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae5dd62f478034e980ccfbc26b0eb53c7696a81f784a0b50643693b7f0d55548
4
- data.tar.gz: 653ff8929bc7199a77c59ead004167a38d214741157282d93e947c3d5ca8ecd6
3
+ metadata.gz: 6a60c7ee3f35357ecbe72d0bdedb89a61b248101ac58d866166e5daa1e2f79eb
4
+ data.tar.gz: 53a68e1f1bab4158d773593ad201dce948a0df60c0fd1b80a27451a96dc5596c
5
5
  SHA512:
6
- metadata.gz: 4f264aa26bed243459d9f5c9eb8827d24ae05d1f9d5cd03416b4e10612e4df1782b2e6ef3cc170db523798e594e1d7d1765a3e9d59fbf81b7955729db9f16cf5
7
- data.tar.gz: 3c3b2312b7945685c6199ecd21f23601b752e1d4234d35461db2f4c7cf0318c24ee263df2536e9cffb7e107002a5a5be10af1cf7eecc16ac65e07cd948f2c68e
6
+ metadata.gz: 8d14d8843ea3d8c8923b654d5c1110fbefb83b40b63a15c930b5243de81f5ecd594d8e08be64a8242884793a6203f535d16088ebbbef3328d519f8cfd440d19e
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