airbrake-ruby 4.13.3-java → 5.0.0-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 (66) 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/backtrace.rb +6 -5
  5. data/lib/airbrake-ruby/config.rb +37 -13
  6. data/lib/airbrake-ruby/config/processor.rb +84 -0
  7. data/lib/airbrake-ruby/config/validator.rb +6 -0
  8. data/lib/airbrake-ruby/file_cache.rb +1 -1
  9. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  10. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  11. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  12. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +3 -3
  13. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  14. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  15. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  16. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  17. data/lib/airbrake-ruby/filters/keys_filter.rb +26 -18
  18. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  19. data/lib/airbrake-ruby/filters/sql_filter.rb +4 -4
  20. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  21. data/lib/airbrake-ruby/filters/thread_filter.rb +2 -0
  22. data/lib/airbrake-ruby/ignorable.rb +1 -0
  23. data/lib/airbrake-ruby/notice.rb +1 -8
  24. data/lib/airbrake-ruby/notice_notifier.rb +7 -0
  25. data/lib/airbrake-ruby/performance_breakdown.rb +1 -6
  26. data/lib/airbrake-ruby/performance_notifier.rb +2 -15
  27. data/lib/airbrake-ruby/promise.rb +1 -0
  28. data/lib/airbrake-ruby/query.rb +1 -6
  29. data/lib/airbrake-ruby/queue.rb +1 -8
  30. data/lib/airbrake-ruby/remote_settings.rb +145 -0
  31. data/lib/airbrake-ruby/remote_settings/settings_data.rb +120 -0
  32. data/lib/airbrake-ruby/request.rb +1 -8
  33. data/lib/airbrake-ruby/stat.rb +1 -12
  34. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  35. data/lib/airbrake-ruby/tdigest.rb +2 -0
  36. data/lib/airbrake-ruby/thread_pool.rb +2 -0
  37. data/lib/airbrake-ruby/truncator.rb +8 -2
  38. data/lib/airbrake-ruby/version.rb +11 -1
  39. data/spec/airbrake_spec.rb +71 -36
  40. data/spec/backtrace_spec.rb +26 -26
  41. data/spec/code_hunk_spec.rb +2 -2
  42. data/spec/config/processor_spec.rb +209 -0
  43. data/spec/config/validator_spec.rb +18 -1
  44. data/spec/config_spec.rb +13 -6
  45. data/spec/filter_chain_spec.rb +27 -0
  46. data/spec/filters/gem_root_filter_spec.rb +4 -4
  47. data/spec/filters/git_last_checkout_filter_spec.rb +20 -3
  48. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +11 -10
  49. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +20 -10
  50. data/spec/filters/root_directory_filter_spec.rb +4 -4
  51. data/spec/filters/sql_filter_spec.rb +5 -5
  52. data/spec/notice_notifier/options_spec.rb +6 -6
  53. data/spec/notice_notifier_spec.rb +2 -2
  54. data/spec/notice_spec.rb +1 -1
  55. data/spec/performance_breakdown_spec.rb +0 -12
  56. data/spec/performance_notifier_spec.rb +2 -27
  57. data/spec/query_spec.rb +1 -11
  58. data/spec/queue_spec.rb +1 -13
  59. data/spec/remote_settings/settings_data_spec.rb +365 -0
  60. data/spec/remote_settings_spec.rb +230 -0
  61. data/spec/request_spec.rb +1 -13
  62. data/spec/spec_helper.rb +4 -4
  63. data/spec/stat_spec.rb +0 -9
  64. data/spec/sync_sender_spec.rb +3 -1
  65. data/spec/thread_pool_spec.rb +25 -5
  66. metadata +22 -14
@@ -18,6 +18,7 @@ module Airbrake
18
18
  # @macro call_filter
19
19
  def call(notice)
20
20
  return if notice[:context].key?(:repository)
21
+
21
22
  attach_repository(notice)
22
23
  end
23
24
 
@@ -39,6 +40,7 @@ module Airbrake
39
40
  end
40
41
 
41
42
  return unless @repository
43
+
42
44
  notice[:context][:repository] = @repository
43
45
  end
44
46
 
@@ -46,6 +48,7 @@ module Airbrake
46
48
 
47
49
  def detect_git_version
48
50
  return unless which('git')
51
+
49
52
  Gem::Version.new(`git --version`.split[2])
50
53
  end
51
54
 
@@ -30,6 +30,7 @@ module Airbrake
30
30
 
31
31
  @revision = find_revision
32
32
  return unless @revision
33
+
33
34
  notice[:context][:revision] = @revision
34
35
  end
35
36
 
@@ -41,6 +42,7 @@ module Airbrake
41
42
 
42
43
  head = File.read(head_path)
43
44
  return head unless head.start_with?(PREFIX)
45
+
44
46
  head = head.chomp[PREFIX.size..-1]
45
47
 
46
48
  ref_path = File.join(@git_path, head)
@@ -4,7 +4,7 @@ module Airbrake
4
4
  # notice, but specified keys.
5
5
  #
6
6
  # @example
7
- # filter = Airbrake::Filters::KeysBlacklist.new(
7
+ # filter = Airbrake::Filters::KeysAllowlist.new(
8
8
  # [:email, /credit/i, 'password']
9
9
  # )
10
10
  # airbrake.add_filter(filter)
@@ -22,9 +22,9 @@ module Airbrake
22
22
  # # email: 'john@example.com',
23
23
  # # account_id: 42 }
24
24
  #
25
- # @see KeysBlacklist
25
+ # @see KeysBlocklist
26
26
  # @see KeysFilter
27
- class KeysWhitelist
27
+ class KeysAllowlist
28
28
  include KeysFilter
29
29
 
30
30
  def initialize(*)
@@ -4,7 +4,7 @@ module Airbrake
4
4
  # list of parameters in the payload of a notice.
5
5
  #
6
6
  # @example
7
- # filter = Airbrake::Filters::KeysBlacklist.new(
7
+ # filter = Airbrake::Filters::KeysBlocklist.new(
8
8
  # [:email, /credit/i, 'password']
9
9
  # )
10
10
  # airbrake.add_filter(filter)
@@ -22,10 +22,10 @@ module Airbrake
22
22
  # # email: '[Filtered]',
23
23
  # # credit_card: '[Filtered]' }
24
24
  #
25
- # @see KeysWhitelist
25
+ # @see KeysAllowlist
26
26
  # @see KeysFilter
27
27
  # @api private
28
- class KeysBlacklist
28
+ class KeysBlocklist
29
29
  include KeysFilter
30
30
 
31
31
  def initialize(*)
@@ -7,8 +7,8 @@ module Airbrake
7
7
  # class that includes this module must implement.
8
8
  #
9
9
  # @see Notice
10
- # @see KeysWhitelist
11
- # @see KeysBlacklist
10
+ # @see KeysAllowlist
11
+ # @see KeysBlocklist
12
12
  # @api private
13
13
  module KeysFilter
14
14
  # @return [String] The label to replace real values of filtered payload
@@ -19,11 +19,11 @@ module Airbrake
19
19
  VALID_PATTERN_CLASSES = [String, Symbol, Regexp].freeze
20
20
 
21
21
  # @return [Array<Symbol>] parts of a Notice's payload that can be modified
22
- # by blacklist/whitelist filters
22
+ # by blocklist/allowlist filters
23
23
  FILTERABLE_KEYS = %i[environment session params].freeze
24
24
 
25
25
  # @return [Array<Symbol>] parts of a Notice's *context* payload that can
26
- # be modified by blacklist/whitelist filters
26
+ # be modified by blocklist/allowlist filters
27
27
  FILTERABLE_CONTEXT_KEYS = %i[
28
28
  user
29
29
 
@@ -42,7 +42,7 @@ module Airbrake
42
42
  # @return [Integer]
43
43
  attr_reader :weight
44
44
 
45
- # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
45
+ # Creates a new KeysBlocklist or KeysAllowlist filter that uses the given
46
46
  # +patterns+ for filtering a notice's payload.
47
47
  #
48
48
  # @param [Array<String,Regexp,Symbol>] patterns
@@ -64,10 +64,14 @@ module Airbrake
64
64
  validate_patterns
65
65
  end
66
66
 
67
- FILTERABLE_KEYS.each { |key| filter_hash(notice[key]) }
67
+ FILTERABLE_KEYS.each do |key|
68
+ notice[key] = filter_hash(notice[key])
69
+ end
70
+
68
71
  FILTERABLE_CONTEXT_KEYS.each { |key| filter_context_key(notice, key) }
69
72
 
70
73
  return unless notice[:context][:url]
74
+
71
75
  filter_url(notice)
72
76
  end
73
77
 
@@ -81,26 +85,26 @@ module Airbrake
81
85
  def filter_hash(hash)
82
86
  return hash unless hash.is_a?(Hash)
83
87
 
88
+ hash_copy = hash.dup
89
+
84
90
  hash.each_key do |key|
85
91
  if should_filter?(key.to_s)
86
- hash[key] = FILTERED
87
- elsif hash[key].is_a?(Hash)
88
- filter_hash(hash[key])
92
+ hash_copy[key] = FILTERED
93
+ elsif hash_copy[key].is_a?(Hash)
94
+ hash_copy[key] = filter_hash(hash_copy[key])
89
95
  elsif hash[key].is_a?(Array)
90
- hash[key].each { |h| filter_hash(h) }
96
+ hash_copy[key].each_with_index do |h, i|
97
+ hash_copy[key][i] = filter_hash(h)
98
+ end
91
99
  end
92
100
  end
101
+
102
+ hash_copy
93
103
  end
94
104
 
95
105
  def filter_url_params(url)
96
106
  url.query = Hash[URI.decode_www_form(url.query)].map do |key, val|
97
- # Ruby < 2.2 raises InvalidComponentError if the query contains
98
- # invalid characters, so be sure to escape individual components.
99
- if should_filter?(key)
100
- "#{URI.encode_www_form_component(key)}=[Filtered]"
101
- else
102
- "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(val)}"
103
- end
107
+ should_filter?(key) ? "#{key}=[Filtered]" : "#{key}=#{val}"
104
108
  end.join('&')
105
109
 
106
110
  url.to_s
@@ -114,6 +118,7 @@ module Airbrake
114
118
  end
115
119
 
116
120
  return unless url.query
121
+
117
122
  notice[:context][:url] = filter_url_params(url)
118
123
  end
119
124
 
@@ -122,6 +127,7 @@ module Airbrake
122
127
 
123
128
  @patterns = @patterns.flat_map do |pattern|
124
129
  next(pattern) unless pattern.respond_to?(:call)
130
+
125
131
  pattern.call
126
132
  end
127
133
  end
@@ -142,7 +148,9 @@ module Airbrake
142
148
  def filter_context_key(notice, key)
143
149
  return unless notice[:context][key]
144
150
  return if notice[:context][key] == FILTERED
145
- return filter_hash(notice[:context][key]) unless should_filter?(key)
151
+ unless should_filter?(key)
152
+ return notice[:context][key] = filter_hash(notice[:context][key])
153
+ end
146
154
 
147
155
  notice[:context][key] = FILTERED
148
156
  end
@@ -19,6 +19,7 @@ module Airbrake
19
19
  notice[:errors].each do |error|
20
20
  error[:backtrace].each do |frame|
21
21
  next unless (file = frame[:file])
22
+
22
23
  file.sub!(/\A#{@root_directory}/, PROJECT_ROOT_LABEL)
23
24
  end
24
25
  end
@@ -23,7 +23,7 @@ module Airbrake
23
23
 
24
24
  # @return [Hash{Symbol=>Regexp}] matchers for certain features of SQL
25
25
  ALL_FEATURES = {
26
- # rubocop:disable Metrics/LineLength
26
+ # rubocop:disable Layout/LineLength
27
27
  single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
28
28
  double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
29
29
  dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
@@ -33,13 +33,13 @@ module Airbrake
33
33
  hexadecimal_literals: /0x[0-9a-fA-F]+/,
34
34
  comments: /(?:#|--).*?(?=\r|\n|$)/i,
35
35
  multi_line_comments: %r{/\*(?:[^/]|/[^*])*?(?:\*/|/\*.*)},
36
- oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/
37
- # rubocop:enable Metrics/LineLength
36
+ oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/,
37
+ # rubocop:enable Layout/LineLength
38
38
  }.freeze
39
39
 
40
40
  # @return [Regexp] the regexp that is applied after the feature regexps
41
41
  # were used
42
- POST_FILTER = /(?<=[values|in ]\().+(?=\))/i
42
+ POST_FILTER = /(?<=[values|in ]\().+(?=\))/i.freeze
43
43
 
44
44
  # @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
45
45
  # to a certain dialect
@@ -16,6 +16,7 @@ module Airbrake
16
16
  # @macro call_filter
17
17
  def call(notice)
18
18
  return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
19
+
19
20
  notice.ignore!
20
21
  end
21
22
  end
@@ -56,6 +56,7 @@ module Airbrake
56
56
  def thread_variables(th)
57
57
  th.thread_variables.map.with_object({}) do |var, h|
58
58
  next if var.to_s.start_with?(IGNORE_PREFIX)
59
+
59
60
  h[var] = sanitize_value(th.thread_variable_get(var))
60
61
  end
61
62
  end
@@ -63,6 +64,7 @@ module Airbrake
63
64
  def fiber_variables(th)
64
65
  th.keys.map.with_object({}) do |key, h|
65
66
  next if key.to_s.start_with?(IGNORE_PREFIX)
67
+
66
68
  h[key] = sanitize_value(th[key])
67
69
  end
68
70
  end
@@ -38,6 +38,7 @@ module Airbrake
38
38
  # @raise [Airbrake::Error] when instance is ignored
39
39
  def raise_if_ignored
40
40
  return unless ignored?
41
+
41
42
  raise Airbrake::Error, "cannot access ignored #{self.class}"
42
43
  end
43
44
  end
@@ -4,19 +4,12 @@ module Airbrake
4
4
  #
5
5
  # @since v1.0.0
6
6
  class Notice
7
- # @return [Hash{Symbol=>String}] the information about the notifier library
8
- NOTIFIER = {
9
- name: 'airbrake-ruby'.freeze,
10
- version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
- url: 'https://github.com/airbrake/airbrake-ruby'.freeze,
12
- }.freeze
13
-
14
7
  # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
8
  # Context tab in the dashboard
16
9
  CONTEXT = {
17
10
  os: RUBY_PLATFORM,
18
11
  language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
- notifier: NOTIFIER,
12
+ notifier: Airbrake::NOTIFIER_INFO,
20
13
  }.freeze
21
14
 
22
15
  # @return [Integer] the maxium size of the JSON payload in bytes
@@ -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)
@@ -129,6 +135,7 @@ module Airbrake
129
135
  # If true, then it's likely an internal library error. In this case return
130
136
  # at least some backtrace to simplify debugging.
131
137
  return caller_copy if clean_bt.empty?
138
+
132
139
  clean_bt
133
140
  end
134
141
  end
@@ -12,16 +12,13 @@ module Airbrake
12
12
  include Stashable
13
13
  include Mergeable
14
14
 
15
- attr_accessor :method, :route, :response_type, :groups, :start_time,
16
- :end_time, :timing, :time
15
+ attr_accessor :method, :route, :response_type, :groups, :timing, :time
17
16
 
18
17
  def initialize(
19
18
  method:,
20
19
  route:,
21
20
  response_type:,
22
21
  groups:,
23
- start_time: Time.now,
24
- end_time: start_time + 1,
25
22
  timing: nil,
26
23
  time: Time.now
27
24
  )
@@ -30,8 +27,6 @@ module Airbrake
30
27
  @route = route
31
28
  @response_type = response_type
32
29
  @groups = groups
33
- @start_time = start_time
34
- @end_time = end_time
35
30
  @timing = timing
36
31
  @time = time
37
32
  end
@@ -104,7 +104,7 @@ module Airbrake
104
104
  @payload[resource] = { total: Airbrake::Stat.new }
105
105
  end
106
106
 
107
- update_total(resource, @payload[resource][:total])
107
+ @payload[resource][:total].increment_ms(resource.timing)
108
108
 
109
109
  resource.groups.each do |name, ms|
110
110
  @payload[resource][name] ||= Airbrake::Stat.new
@@ -112,19 +112,6 @@ module Airbrake
112
112
  end
113
113
  end
114
114
 
115
- def update_total(resource, total)
116
- if resource.timing
117
- total.increment_ms(resource.timing)
118
- else
119
- loc = caller_locations(6..6).first
120
- Kernel.warn(
121
- "#{loc.path}:#{loc.lineno}: warning: :start_time and :end_time are " \
122
- "deprecated. Use :timing & :time instead",
123
- )
124
- total.increment(resource.start_time, resource.end_time)
125
- end
126
- end
127
-
128
115
  def check_configuration(resource)
129
116
  promise = @config.check_configuration
130
117
  return promise if promise.rejected?
@@ -144,7 +131,7 @@ module Airbrake
144
131
 
145
132
  with_grouped_payload(payload) do |resource_hash, destination|
146
133
  url = URI.join(
147
- @config.host,
134
+ @config.apm_host,
148
135
  "api/v5/projects/#{@config.project_id}/#{destination}",
149
136
  )
150
137
 
@@ -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
@@ -12,8 +12,7 @@ module Airbrake
12
12
  include Mergeable
13
13
  include Grouppable
14
14
 
15
- attr_accessor :method, :route, :query, :func, :file, :line, :start_time,
16
- :end_time, :timing, :time
15
+ attr_accessor :method, :route, :query, :func, :file, :line, :timing, :time
17
16
 
18
17
  def initialize(
19
18
  method:,
@@ -22,8 +21,6 @@ module Airbrake
22
21
  func: nil,
23
22
  file: nil,
24
23
  line: nil,
25
- start_time: Time.now,
26
- end_time: start_time + 1,
27
24
  timing: nil,
28
25
  time: Time.now
29
26
  )
@@ -34,8 +31,6 @@ module Airbrake
34
31
  @func = func
35
32
  @file = file
36
33
  @line = line
37
- @start_time = start_time
38
- @end_time = end_time
39
34
  @timing = timing
40
35
  @time = time
41
36
  end
@@ -4,21 +4,17 @@ module Airbrake
4
4
  # @see Airbrake.notify_queue
5
5
  # @api public
6
6
  # @since v4.9.0
7
- # rubocop:disable Metrics/ParameterLists
8
7
  class Queue
9
8
  include HashKeyable
10
9
  include Ignorable
11
10
  include Stashable
12
11
 
13
- attr_accessor :queue, :error_count, :groups, :start_time, :end_time,
14
- :timing, :time
12
+ attr_accessor :queue, :error_count, :groups, :timing, :time
15
13
 
16
14
  def initialize(
17
15
  queue:,
18
16
  error_count:,
19
17
  groups: {},
20
- start_time: Time.now,
21
- end_time: start_time + 1,
22
18
  timing: nil,
23
19
  time: Time.now
24
20
  )
@@ -26,8 +22,6 @@ module Airbrake
26
22
  @queue = queue
27
23
  @error_count = error_count
28
24
  @groups = groups
29
- @start_time = start_time
30
- @end_time = end_time
31
25
  @timing = timing
32
26
  @time = time
33
27
  end
@@ -68,5 +62,4 @@ module Airbrake
68
62
  ''
69
63
  end
70
64
  end
71
- # rubocop:enable Metrics/ParameterLists
72
65
  end
@@ -0,0 +1,145 @@
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 5.0.0
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
+ # @return [Hash{Symbol=>String}] metadata to be attached to every GET
28
+ # request
29
+ QUERY_PARAMS = URI.encode_www_form(
30
+ notifier_name: Airbrake::NOTIFIER_INFO[:name],
31
+ notifier_version: Airbrake::NOTIFIER_INFO[:version],
32
+ os: RUBY_PLATFORM,
33
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
34
+ ).freeze
35
+
36
+ # Polls remote config of the given project.
37
+ #
38
+ # @param [Integer] project_id
39
+ # @param [String] host
40
+ # @yield [data]
41
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
42
+ # @return [Airbrake::RemoteSettings]
43
+ def self.poll(project_id, host, &block)
44
+ new(project_id, host, &block).poll
45
+ end
46
+
47
+ # @param [Integer] project_id
48
+ # @yield [data]
49
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
50
+ def initialize(project_id, host, &block)
51
+ @data = SettingsData.new(project_id, {})
52
+ @host = host
53
+ @block = block
54
+ @poll = nil
55
+ end
56
+
57
+ # Polls remote config of the given project in background. Loads local config
58
+ # first (if exists).
59
+ #
60
+ # @return [self]
61
+ def poll
62
+ @poll ||= Thread.new do
63
+ begin
64
+ load_config
65
+ rescue StandardError => ex
66
+ logger.error("#{LOG_LABEL} config loading failed: #{ex}")
67
+ end
68
+
69
+ @block.call(@data)
70
+
71
+ loop do
72
+ @block.call(@data.merge!(fetch_config))
73
+ sleep(@data.interval)
74
+ end
75
+ end
76
+
77
+ self
78
+ end
79
+
80
+ # Stops the background poller thread. Dumps current config to disk.
81
+ #
82
+ # @return [void]
83
+ def stop_polling
84
+ @poll.kill if @poll
85
+
86
+ begin
87
+ dump_config
88
+ rescue StandardError => ex
89
+ logger.error("#{LOG_LABEL} config dumping failed: #{ex}")
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def fetch_config
96
+ response = nil
97
+ begin
98
+ response = Net::HTTP.get(build_config_uri)
99
+ rescue StandardError => ex
100
+ logger.error(ex)
101
+ return {}
102
+ end
103
+
104
+ # AWS S3 API returns XML when request is not valid. In this case we just
105
+ # print the returned body and exit the method.
106
+ if response.start_with?('<?xml ')
107
+ logger.error(response)
108
+ return {}
109
+ end
110
+
111
+ json = nil
112
+ begin
113
+ json = JSON.parse(response)
114
+ rescue JSON::ParserError => ex
115
+ logger.error(ex)
116
+ return {}
117
+ end
118
+
119
+ json
120
+ end
121
+
122
+ def build_config_uri
123
+ uri = URI(@data.config_route(@host))
124
+ uri.query = QUERY_PARAMS
125
+ uri
126
+ end
127
+
128
+ def load_config
129
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
130
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
131
+
132
+ return unless File.exist?(CONFIG_DUMP_PATH)
133
+
134
+ config = File.read(CONFIG_DUMP_PATH)
135
+ @data.merge!(JSON.parse(config))
136
+ end
137
+
138
+ def dump_config
139
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
140
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
141
+
142
+ File.write(CONFIG_DUMP_PATH, JSON.dump(@data.to_h))
143
+ end
144
+ end
145
+ end