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 +4 -4
- data/lib/airbrake-ruby/async_sender.rb +11 -15
- data/lib/airbrake-ruby/backlog.rb +123 -0
- data/lib/airbrake-ruby/config.rb +7 -0
- data/lib/airbrake-ruby/notice_notifier.rb +1 -0
- data/lib/airbrake-ruby/performance_notifier.rb +1 -0
- data/lib/airbrake-ruby/response.rb +56 -5
- data/lib/airbrake-ruby/sync_sender.rb +39 -8
- data/lib/airbrake-ruby/version.rb +1 -1
- data/lib/airbrake-ruby.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 957248cf16866b1a8c873216c0179a79cc39189758f364254ae6534cde41ebda
|
4
|
+
data.tar.gz: d579021d4b619b7307643559ca1a0d19f22782305b3a91a56c25d6e393402949
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
@
|
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 [
|
16
|
+
# @param [Airbrake::Notice] payload Whatever needs to be sent
|
19
17
|
# @return [Airbrake::Promise]
|
20
|
-
def send(
|
21
|
-
unless thread_pool << [
|
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 ||=
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
data/lib/airbrake-ruby/config.rb
CHANGED
@@ -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
|
@@ -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
|
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
|
-
{
|
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 =
|
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
|
-
|
46
|
-
|
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
|
|
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.
|
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-
|
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
|