airbrake-ruby 6.1.2 → 6.2.0

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: bf054506d2927e93c5bb6d6050ff0b13dd6cef15c901243ad1e7f4871a4f8568
4
- data.tar.gz: b054ea5189464b1ebf822812cb7192893fbf84ef085b57fdfca9ead3d3361c6d
3
+ metadata.gz: 957248cf16866b1a8c873216c0179a79cc39189758f364254ae6534cde41ebda
4
+ data.tar.gz: d579021d4b619b7307643559ca1a0d19f22782305b3a91a56c25d6e393402949
5
5
  SHA512:
6
- metadata.gz: 3469e65f834026e9c67c0200d2eca1786f8ea566b620f0ebc4175687a1d8b1ce41563460edc4a0be622e0d2dc93eade382fca1770bcb31b3f221dc188f5e4362
7
- data.tar.gz: ee6edd6c1a279ba33ca4e898198aff4147e07be36feb9aa165fd076668499a08cc41d8fc5158dff09ee9fc7d5732f0e5c87b4e3bc1ca57d39485651d0431082a
6
+ metadata.gz: 367780539773c946676d19ef6e76ed4b98abf8009f4d3c8715837b07dafaaa1bee207bc01fac0b04765bf7889a22d0ca3802462e3d0c062cc708fb9b016b103b
7
+ data.tar.gz: 1deb29c06f902730deb860150354da8c8a781cf5082daeb5305c47b33d6c6221d5d35fdc6ad2311d4dff22923cc0b015e6e7ceac756365709dc69987e90d7326
@@ -5,20 +5,18 @@ module Airbrake
5
5
  # @api private
6
6
  # @since v1.0.0
7
7
  class AsyncSender
8
- include Loggable
9
-
10
8
  def initialize(method = :post, name = 'async-sender')
11
9
  @config = Airbrake::Config.instance
12
- @method = method
10
+ @sync_sender = SyncSender.new(method)
13
11
  @name = name
14
12
  end
15
13
 
16
14
  # Asynchronously sends a notice to Airbrake.
17
15
  #
18
- # @param [Hash] payload Whatever needs to be sent
16
+ # @param [Airbrake::Notice] payload Whatever needs to be sent
19
17
  # @return [Airbrake::Promise]
20
- def send(payload, promise, endpoint = @config.error_endpoint)
21
- unless thread_pool << [payload, promise, endpoint]
18
+ def send(notice, promise, endpoint = @config.error_endpoint)
19
+ unless thread_pool << [notice, promise, endpoint]
22
20
  return promise.reject(
23
21
  "AsyncSender has reached its capacity of #{@config.queue_size}",
24
22
  )
@@ -29,6 +27,7 @@ module Airbrake
29
27
 
30
28
  # @return [void]
31
29
  def close
30
+ @sync_sender.close
32
31
  thread_pool.close
33
32
  end
34
33
 
@@ -45,15 +44,12 @@ module Airbrake
45
44
  private
46
45
 
47
46
  def thread_pool
48
- @thread_pool ||= begin
49
- sender = SyncSender.new(@method)
50
- ThreadPool.new(
51
- name: @name,
52
- worker_size: @config.workers,
53
- queue_size: @config.queue_size,
54
- block: proc { |args| sender.send(*args) },
55
- )
56
- end
47
+ @thread_pool ||= ThreadPool.new(
48
+ name: @name,
49
+ worker_size: @config.workers,
50
+ queue_size: @config.queue_size,
51
+ block: proc { |args| @sync_sender.send(*args) },
52
+ )
57
53
  end
58
54
  end
59
55
  end
@@ -0,0 +1,123 @@
1
+ module Airbrake
2
+ # Backlog accepts notices and APM events and synchronously sends them in the
3
+ # background at regular intervals. The backlog is a queue of data that failed
4
+ # to be sent due to some error. In a nutshell, it's a retry mechanism.
5
+ #
6
+ # @api private
7
+ # @since v6.2.0
8
+ class Backlog
9
+ include Loggable
10
+
11
+ # @return [Integer] how many records to keep in the backlog
12
+ BACKLOG_SIZE = 100
13
+
14
+ # @return [Integer] flush period in seconds
15
+ TWO_MINUTES = 60 * 2
16
+
17
+ def initialize(sync_sender, flush_period = TWO_MINUTES)
18
+ @sync_sender = sync_sender
19
+ @flush_period = flush_period
20
+ @queue = SizedQueue.new(BACKLOG_SIZE).extend(MonitorMixin)
21
+ @has_backlog_data = @queue.new_cond
22
+ @schedule_flush = nil
23
+
24
+ @seen = Set.new
25
+ end
26
+
27
+ # Appends data to the backlog. Once appended, the flush schedule will
28
+ # start. Chainable.
29
+ #
30
+ # @example
31
+ # backlog << [{ 'data' => 1 }, 'https://airbrake.io/api']
32
+ #
33
+ # @param [Array<#to_json, String>] data An array of two elements, where the
34
+ # first element is the data we are sending and the second element is the
35
+ # URL that we are sending to
36
+ # @return [self]
37
+ def <<(data)
38
+ @queue.synchronize do
39
+ return self if @seen.include?(data)
40
+
41
+ @seen << data
42
+
43
+ begin
44
+ @queue.push(data, true)
45
+ rescue ThreadError
46
+ logger.error("#{LOG_LABEL} Airbrake::Backlog full")
47
+ return self
48
+ end
49
+
50
+ @has_backlog_data.signal
51
+ schedule_flush
52
+
53
+ self
54
+ end
55
+ end
56
+
57
+ # Closes all the resources that this sender has allocated.
58
+ #
59
+ # @return [void]
60
+ # @since v6.2.0
61
+ def close
62
+ @queue.synchronize do
63
+ if @schedule_flush
64
+ @schedule_flush.kill
65
+ logger.debug("#{LOG_LABEL} Airbrake::Backlog closed")
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def schedule_flush
73
+ @schedule_flush ||= Thread.new do
74
+ loop do
75
+ @queue.synchronize do
76
+ wait
77
+ next if @queue.empty?
78
+
79
+ flush
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def wait
86
+ @has_backlog_data.wait(@flush_period) while time_elapsed < @flush_period
87
+ @last_flush = nil
88
+ end
89
+
90
+ def time_elapsed
91
+ MonotonicTime.time_in_s - last_flush
92
+ end
93
+
94
+ def last_flush
95
+ @last_flush ||= MonotonicTime.time_in_s
96
+ end
97
+
98
+ def flush
99
+ unless @queue.empty?
100
+ logger.debug(
101
+ "#{LOG_LABEL} Airbrake::Backlog flushing #{@queue.size} messages",
102
+ )
103
+ end
104
+
105
+ failed = 0
106
+
107
+ until @queue.empty?
108
+ data, endpoint = @queue.pop
109
+ promise = Airbrake::Promise.new
110
+ @sync_sender.send(data, promise, endpoint)
111
+ failed += 1 if promise.rejected?
112
+ end
113
+
114
+ if failed > 0
115
+ logger.debug(
116
+ "#{LOG_LABEL} Airbrake::Backlog #{failed} messages were not flushed",
117
+ )
118
+ end
119
+
120
+ @seen.clear
121
+ end
122
+ end
123
+ end
@@ -136,6 +136,12 @@ module Airbrake
136
136
  # @since v5.2.0
137
137
  attr_accessor :remote_config
138
138
 
139
+ # @return [Boolean] true if the library should keep a backlog of failed
140
+ # notices or APM events and retry them after an interval, false otherwise
141
+ # @api public
142
+ # @since v6.2.0
143
+ attr_accessor :backlog
144
+
139
145
  class << self
140
146
  # @return [Config]
141
147
  attr_writer :instance
@@ -180,6 +186,7 @@ module Airbrake
180
186
  self.job_stats = true
181
187
  self.error_notifications = true
182
188
  self.remote_config = true
189
+ self.backlog = true
183
190
 
184
191
  merge(user_config)
185
192
  end
@@ -68,6 +68,7 @@ module Airbrake
68
68
 
69
69
  # @see Airbrake.close
70
70
  def close
71
+ @sync_sender.close
71
72
  @async_sender.close
72
73
  end
73
74
 
@@ -50,6 +50,7 @@ module Airbrake
50
50
  def close
51
51
  @payload.synchronize do
52
52
  @schedule_flush.kill if @schedule_flush
53
+ @sync_sender.close
53
54
  @async_sender.close
54
55
  end
55
56
  end
@@ -8,9 +8,56 @@ module Airbrake
8
8
  # @return [Integer] the limit of the response body
9
9
  TRUNCATE_LIMIT = 100
10
10
 
11
+ # @return [Integer] HTTP code returned when the server cannot or will not
12
+ # process the request due to something that is perceived to be a client
13
+ # error
14
+ # @since v6.2.0
15
+ BAD_REQUEST = 400
16
+
17
+ # @return [Integer] HTTP code returned when client request has not been
18
+ # completed because it lacks valid authentication credentials for the
19
+ # requested resource
20
+ # @since v6.2.0
21
+ UNAUTHORIZED = 401
22
+
23
+ # @return [Integer] HTTP code returned when the server understands the
24
+ # request but refuses to authorize it
25
+ # @since v6.2.0
26
+ FORBIDDEN = 403
27
+
28
+ # @return [Integer] HTTP code returned when the server would like to shut
29
+ # down this unused connection
30
+ # @since v6.2.0
31
+ REQUEST_TIMEOUT = 408
32
+
33
+ # @return [Integer] HTTP code returned when there's a request conflict with
34
+ # the current state of the target resource
35
+ # @since v6.2.0
36
+ CONFLICT = 409
37
+
38
+ # @return [Integer]
39
+ # @since v6.2.0
40
+ ENHANCE_YOUR_CALM = 420
41
+
11
42
  # @return [Integer] HTTP code returned when an IP sends over 10k/min notices
12
43
  TOO_MANY_REQUESTS = 429
13
44
 
45
+ # @return [Integer] HTTP code returned when the server encountered an
46
+ # unexpected condition that prevented it from fulfilling the request
47
+ # @since v6.2.0
48
+ INTERNAL_SERVER_ERROR = 500
49
+
50
+ # @return [Integer] HTTP code returened when the server, while acting as a
51
+ # gateway or proxy, received an invalid response from the upstream server
52
+ # @since v6.2.0
53
+ BAD_GATEWAY = 502
54
+
55
+ # @return [Integer] HTTP code returened when the server, while acting as a
56
+ # gateway or proxy, did not get a response in time from the upstream
57
+ # server that it needed in order to complete the request
58
+ # @since v6.2.0
59
+ GATEWAY_TIMEOUT = 504
60
+
14
61
  class << self
15
62
  include Loggable
16
63
  end
@@ -33,24 +80,28 @@ module Airbrake
33
80
  parsed_body = JSON.parse(body)
34
81
  logger.debug("#{LOG_LABEL} #{name} (#{code}): #{parsed_body}")
35
82
  parsed_body
36
- when 400, 401, 403, 420
83
+ when BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, ENHANCE_YOUR_CALM
37
84
  parsed_body = JSON.parse(body)
38
85
  logger.error("#{LOG_LABEL} #{parsed_body['message']}")
39
- parsed_body
86
+ parsed_body.merge('code' => code, 'error' => parsed_body['message'])
40
87
  when TOO_MANY_REQUESTS
41
88
  parsed_body = JSON.parse(body)
42
89
  msg = "#{LOG_LABEL} #{parsed_body['message']}"
43
90
  logger.error(msg)
44
- { 'error' => msg, 'rate_limit_reset' => rate_limit_reset(response) }
91
+ {
92
+ 'code' => code,
93
+ 'error' => msg,
94
+ 'rate_limit_reset' => rate_limit_reset(response),
95
+ }
45
96
  else
46
97
  body_msg = truncated_body(body)
47
98
  logger.error("#{LOG_LABEL} unexpected code (#{code}). Body: #{body_msg}")
48
- { 'error' => body_msg }
99
+ { 'code' => code, 'error' => body_msg }
49
100
  end
50
101
  rescue StandardError => ex
51
102
  body_msg = truncated_body(body)
52
103
  logger.error("#{LOG_LABEL} error while parsing body (#{ex}). Body: #{body_msg}")
53
- { 'error' => ex.inspect }
104
+ { 'code' => code, 'error' => ex.inspect }
54
105
  end
55
106
  end
56
107
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -9,6 +9,20 @@ module Airbrake
9
9
  # @return [String] body for HTTP requests
10
10
  CONTENT_TYPE = 'application/json'.freeze
11
11
 
12
+ # @return [Array<Integer>] response codes that are good to be backlogged
13
+ # @since v6.2.0
14
+ BACKLOGGABLE_STATUS_CODES = [
15
+ Response::BAD_REQUEST,
16
+ Response::FORBIDDEN,
17
+ Response::ENHANCE_YOUR_CALM,
18
+ Response::REQUEST_TIMEOUT,
19
+ Response::CONFLICT,
20
+ Response::TOO_MANY_REQUESTS,
21
+ Response::INTERNAL_SERVER_ERROR,
22
+ Response::BAD_GATEWAY,
23
+ Response::GATEWAY_TIMEOUT,
24
+ ].freeze
25
+
12
26
  include Loggable
13
27
 
14
28
  # @param [Symbol] method HTTP method to use to send payload
@@ -16,6 +30,7 @@ module Airbrake
16
30
  @config = Airbrake::Config.instance
17
31
  @method = method
18
32
  @rate_limit_reset = Time.now
33
+ @backlog = Backlog.new(self) if @config.backlog
19
34
  end
20
35
 
21
36
  # Sends a POST or PUT request to the given +endpoint+ with the +data+ payload.
@@ -26,15 +41,11 @@ module Airbrake
26
41
  def send(data, promise, endpoint = @config.error_endpoint)
27
42
  return promise if rate_limited_ip?(promise)
28
43
 
29
- response = nil
30
44
  req = build_request(endpoint, data)
31
-
32
45
  return promise if missing_body?(req, promise)
33
46
 
34
- https = build_https(endpoint)
35
-
36
47
  begin
37
- response = https.request(req)
48
+ response = build_https(endpoint).request(req)
38
49
  rescue StandardError => ex
39
50
  reason = "#{LOG_LABEL} HTTP error: #{ex}"
40
51
  logger.error(reason)
@@ -42,15 +53,22 @@ module Airbrake
42
53
  end
43
54
 
44
55
  parsed_resp = Response.parse(response)
45
- if parsed_resp.key?('rate_limit_reset')
46
- @rate_limit_reset = parsed_resp['rate_limit_reset']
47
- end
56
+ handle_rate_limit(parsed_resp)
57
+ @backlog << [data, endpoint] if add_to_backlog?(parsed_resp)
48
58
 
49
59
  return promise.reject(parsed_resp['error']) if parsed_resp.key?('error')
50
60
 
51
61
  promise.resolve(parsed_resp)
52
62
  end
53
63
 
64
+ # Closes all the resources that this sender has allocated.
65
+ #
66
+ # @return [void]
67
+ # @since v6.2.0
68
+ def close
69
+ @backlog.close
70
+ end
71
+
54
72
  private
55
73
 
56
74
  def build_https(uri)
@@ -86,6 +104,19 @@ module Airbrake
86
104
  req
87
105
  end
88
106
 
107
+ def handle_rate_limit(parsed_resp)
108
+ return unless parsed_resp.key?('rate_limit_reset')
109
+
110
+ @rate_limit_reset = parsed_resp['rate_limit_reset']
111
+ end
112
+
113
+ def add_to_backlog?(parsed_resp)
114
+ return unless @backlog
115
+ return unless parsed_resp.key?('code')
116
+
117
+ BACKLOGGABLE_STATUS_CODES.include?(parsed_resp['code'])
118
+ end
119
+
89
120
  def proxy_params
90
121
  return unless @config.proxy.key?(:host)
91
122
 
@@ -3,7 +3,7 @@
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
5
  # @api public
6
- AIRBRAKE_RUBY_VERSION = '6.1.2'.freeze
6
+ AIRBRAKE_RUBY_VERSION = '6.2.0'.freeze
7
7
 
8
8
  # @return [Hash{Symbol=>String}] the information about the notifier library
9
9
  # @since v5.0.0
data/lib/airbrake-ruby.rb CHANGED
@@ -18,9 +18,9 @@ require 'airbrake-ruby/remote_settings/settings_data'
18
18
  require 'airbrake-ruby/remote_settings'
19
19
  require 'airbrake-ruby/promise'
20
20
  require 'airbrake-ruby/thread_pool'
21
+ require 'airbrake-ruby/response'
21
22
  require 'airbrake-ruby/sync_sender'
22
23
  require 'airbrake-ruby/async_sender'
23
- require 'airbrake-ruby/response'
24
24
  require 'airbrake-ruby/nested_exception'
25
25
  require 'airbrake-ruby/ignorable'
26
26
  require 'airbrake-ruby/inspectable'
@@ -59,6 +59,7 @@ require 'airbrake-ruby/monotonic_time'
59
59
  require 'airbrake-ruby/timed_trace'
60
60
  require 'airbrake-ruby/queue'
61
61
  require 'airbrake-ruby/context'
62
+ require 'airbrake-ruby/backlog'
62
63
 
63
64
  # Airbrake is a thin wrapper around instances of the notifier classes (such as
64
65
  # notice, performance & deploy notifiers). It creates a way to access them via a
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.2
4
+ version: 6.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-01 00:00:00.000000000 Z
11
+ date: 2022-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree3
@@ -40,6 +40,7 @@ extra_rdoc_files: []
40
40
  files:
41
41
  - lib/airbrake-ruby.rb
42
42
  - lib/airbrake-ruby/async_sender.rb
43
+ - lib/airbrake-ruby/backlog.rb
43
44
  - lib/airbrake-ruby/backtrace.rb
44
45
  - lib/airbrake-ruby/benchmark.rb
45
46
  - lib/airbrake-ruby/code_hunk.rb