airbrake-ruby 6.1.2 → 6.2.0

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