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