celerbrake-ruby 0.1.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 +7 -0
- data/lib/celerbrake-ruby/async_sender.rb +57 -0
- data/lib/celerbrake-ruby/backlog.rb +123 -0
- data/lib/celerbrake-ruby/backtrace.rb +197 -0
- data/lib/celerbrake-ruby/benchmark.rb +39 -0
- data/lib/celerbrake-ruby/code_hunk.rb +51 -0
- data/lib/celerbrake-ruby/config/processor.rb +77 -0
- data/lib/celerbrake-ruby/config/validator.rb +97 -0
- data/lib/celerbrake-ruby/config.rb +291 -0
- data/lib/celerbrake-ruby/context.rb +51 -0
- data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
- data/lib/celerbrake-ruby/file_cache.rb +54 -0
- data/lib/celerbrake-ruby/filter_chain.rb +112 -0
- data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
- data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
- data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
- data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
- data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
- data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
- data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
- data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
- data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
- data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
- data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
- data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
- data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
- data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
- data/lib/celerbrake-ruby/grouppable.rb +12 -0
- data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
- data/lib/celerbrake-ruby/ignorable.rb +43 -0
- data/lib/celerbrake-ruby/inspectable.rb +39 -0
- data/lib/celerbrake-ruby/loggable.rb +34 -0
- data/lib/celerbrake-ruby/mergeable.rb +12 -0
- data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
- data/lib/celerbrake-ruby/nested_exception.rb +59 -0
- data/lib/celerbrake-ruby/notice.rb +157 -0
- data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
- data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
- data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
- data/lib/celerbrake-ruby/promise.rb +110 -0
- data/lib/celerbrake-ruby/query.rb +59 -0
- data/lib/celerbrake-ruby/queue.rb +65 -0
- data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
- data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
- data/lib/celerbrake-ruby/remote_settings.rb +128 -0
- data/lib/celerbrake-ruby/request.rb +48 -0
- data/lib/celerbrake-ruby/response.rb +125 -0
- data/lib/celerbrake-ruby/stashable.rb +15 -0
- data/lib/celerbrake-ruby/stat.rb +66 -0
- data/lib/celerbrake-ruby/sync_sender.rb +145 -0
- data/lib/celerbrake-ruby/tdigest.rb +379 -0
- data/lib/celerbrake-ruby/thread_pool.rb +139 -0
- data/lib/celerbrake-ruby/time_truncate.rb +17 -0
- data/lib/celerbrake-ruby/timed_trace.rb +56 -0
- data/lib/celerbrake-ruby/truncator.rb +121 -0
- data/lib/celerbrake-ruby/version.rb +16 -0
- data/lib/celerbrake-ruby.rb +592 -0
- metadata +251 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module Celerbrake
|
|
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. Supports proxies.
|
|
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
|
+
# @since v5.0.0
|
|
12
|
+
# @api private
|
|
13
|
+
class RemoteSettings
|
|
14
|
+
include Celerbrake::Loggable
|
|
15
|
+
|
|
16
|
+
# @return [Hash{Symbol=>String}] metadata to be attached to every GET
|
|
17
|
+
# request
|
|
18
|
+
QUERY_PARAMS = URI.encode_www_form(
|
|
19
|
+
notifier_name: Celerbrake::NOTIFIER_INFO[:name],
|
|
20
|
+
notifier_version: Celerbrake::NOTIFIER_INFO[:version],
|
|
21
|
+
os: RUBY_PLATFORM,
|
|
22
|
+
language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
|
|
23
|
+
).freeze
|
|
24
|
+
|
|
25
|
+
# @return [String]
|
|
26
|
+
HTTP_OK = '200'.freeze
|
|
27
|
+
|
|
28
|
+
# Polls remote config of the given project.
|
|
29
|
+
#
|
|
30
|
+
# @param [Integer] project_id
|
|
31
|
+
# @param [String] host
|
|
32
|
+
# @yield [data]
|
|
33
|
+
# @yieldparam data [Celerbrake::RemoteSettings::SettingsData]
|
|
34
|
+
# @return [Celerbrake::RemoteSettings]
|
|
35
|
+
def self.poll(project_id, host, &block)
|
|
36
|
+
new(project_id, host, &block).poll
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param [Integer] project_id
|
|
40
|
+
# @yield [data]
|
|
41
|
+
# @yieldparam data [Celerbrake::RemoteSettings::SettingsData]
|
|
42
|
+
def initialize(project_id, host, &block)
|
|
43
|
+
@data = SettingsData.new(project_id, {})
|
|
44
|
+
@host = host
|
|
45
|
+
@block = block
|
|
46
|
+
@config = Celerbrake::Config.instance
|
|
47
|
+
@poll = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Polls remote config of the given project in background.
|
|
51
|
+
#
|
|
52
|
+
# @return [self]
|
|
53
|
+
def poll
|
|
54
|
+
@poll ||= Thread.new do
|
|
55
|
+
@block.call(@data)
|
|
56
|
+
|
|
57
|
+
loop do
|
|
58
|
+
@block.call(@data.merge!(fetch_config))
|
|
59
|
+
sleep(@data.interval)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Stops the background poller thread.
|
|
67
|
+
#
|
|
68
|
+
# @return [void]
|
|
69
|
+
def stop_polling
|
|
70
|
+
@poll.kill if @poll
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def fetch_config
|
|
76
|
+
uri = build_config_uri
|
|
77
|
+
https = build_https(uri)
|
|
78
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
79
|
+
response = nil
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
response = https.request(req)
|
|
83
|
+
rescue StandardError => ex
|
|
84
|
+
reason = "#{LOG_LABEL} HTTP error: #{ex}"
|
|
85
|
+
logger.error(reason)
|
|
86
|
+
return {}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless response.code == HTTP_OK
|
|
90
|
+
logger.error(response.body)
|
|
91
|
+
return {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
json = nil
|
|
95
|
+
begin
|
|
96
|
+
json = JSON.parse(response.body)
|
|
97
|
+
rescue JSON::ParserError => ex
|
|
98
|
+
logger.error(ex)
|
|
99
|
+
return {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
json
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_config_uri
|
|
106
|
+
uri = URI(@data.config_route(@host))
|
|
107
|
+
uri.query = QUERY_PARAMS
|
|
108
|
+
uri
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_https(uri)
|
|
112
|
+
Net::HTTP.new(uri.host, uri.port, *proxy_params).tap do |https|
|
|
113
|
+
https.use_ssl = uri.is_a?(URI::HTTPS)
|
|
114
|
+
if @config.timeout
|
|
115
|
+
https.open_timeout = @config.timeout
|
|
116
|
+
https.read_timeout = @config.timeout
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def proxy_params
|
|
122
|
+
return unless @config.proxy.key?(:host)
|
|
123
|
+
|
|
124
|
+
[@config.proxy[:host], @config.proxy[:port], @config.proxy[:user],
|
|
125
|
+
@config.proxy[:password]]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Request holds request data that powers route stats.
|
|
3
|
+
#
|
|
4
|
+
# @see Celerbrake.notify_request
|
|
5
|
+
# @api public
|
|
6
|
+
# @since v3.2.0
|
|
7
|
+
class Request
|
|
8
|
+
include HashKeyable
|
|
9
|
+
include Ignorable
|
|
10
|
+
include Stashable
|
|
11
|
+
include Mergeable
|
|
12
|
+
include Grouppable
|
|
13
|
+
|
|
14
|
+
attr_accessor :method, :route, :status_code, :timing, :time
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
method:,
|
|
18
|
+
route:,
|
|
19
|
+
status_code:,
|
|
20
|
+
timing: nil,
|
|
21
|
+
time: Time.now
|
|
22
|
+
)
|
|
23
|
+
@time_utc = TimeTruncate.utc_truncate_minutes(time)
|
|
24
|
+
@method = method
|
|
25
|
+
@route = route
|
|
26
|
+
@status_code = status_code
|
|
27
|
+
@timing = timing
|
|
28
|
+
@time = time
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def destination
|
|
32
|
+
'routes-stats'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cargo
|
|
36
|
+
'routes'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
'method' => method,
|
|
42
|
+
'route' => route,
|
|
43
|
+
'statusCode' => status_code,
|
|
44
|
+
'time' => @time_utc,
|
|
45
|
+
}.delete_if { |_key, val| val.nil? }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Parses responses coming from the Celerbrake API. Handles HTTP errors by
|
|
3
|
+
# logging them.
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
6
|
+
# @since v1.0.0
|
|
7
|
+
module Response
|
|
8
|
+
# @return [Integer] the limit of the response body
|
|
9
|
+
TRUNCATE_LIMIT = 100
|
|
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
|
+
|
|
42
|
+
# @return [Integer] HTTP code returned when an IP sends over 10k/min notices
|
|
43
|
+
TOO_MANY_REQUESTS = 429
|
|
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
|
+
|
|
61
|
+
class << self
|
|
62
|
+
include Loggable
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parses HTTP responses from the Celerbrake API.
|
|
66
|
+
#
|
|
67
|
+
# @param [Net::HTTPResponse] response
|
|
68
|
+
# @return [Hash{String=>String}] parsed response
|
|
69
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
70
|
+
def self.parse(response)
|
|
71
|
+
code = response.code.to_i
|
|
72
|
+
body = response.body
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
case code
|
|
76
|
+
when 200, 204
|
|
77
|
+
logger.debug("#{LOG_LABEL} #{name} (#{code}): #{body}")
|
|
78
|
+
{ response.msg => response.body }
|
|
79
|
+
when 201
|
|
80
|
+
parsed_body = JSON.parse(body)
|
|
81
|
+
logger.debug("#{LOG_LABEL} #{name} (#{code}): #{parsed_body}")
|
|
82
|
+
parsed_body
|
|
83
|
+
when BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, ENHANCE_YOUR_CALM
|
|
84
|
+
parsed_body = JSON.parse(body)
|
|
85
|
+
logger.error("#{LOG_LABEL} #{parsed_body['message']}")
|
|
86
|
+
parsed_body.merge('code' => code, 'error' => parsed_body['message'])
|
|
87
|
+
when TOO_MANY_REQUESTS
|
|
88
|
+
parsed_body = JSON.parse(body)
|
|
89
|
+
msg = "#{LOG_LABEL} #{parsed_body['message']}"
|
|
90
|
+
logger.error(msg)
|
|
91
|
+
{
|
|
92
|
+
'code' => code,
|
|
93
|
+
'error' => msg,
|
|
94
|
+
'rate_limit_reset' => rate_limit_reset(response),
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
body_msg = truncated_body(body)
|
|
98
|
+
logger.error("#{LOG_LABEL} unexpected code (#{code}). Body: #{body_msg}")
|
|
99
|
+
{ 'code' => code, 'error' => body_msg }
|
|
100
|
+
end
|
|
101
|
+
rescue StandardError => ex
|
|
102
|
+
body_msg = truncated_body(body)
|
|
103
|
+
logger.error("#{LOG_LABEL} error while parsing body (#{ex}). Body: #{body_msg}")
|
|
104
|
+
{ 'code' => code, 'error' => ex.inspect }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
108
|
+
|
|
109
|
+
def self.truncated_body(body)
|
|
110
|
+
if body.nil?
|
|
111
|
+
'[EMPTY_BODY]'.freeze
|
|
112
|
+
elsif body.length > TRUNCATE_LIMIT
|
|
113
|
+
body[0..TRUNCATE_LIMIT] << '...'
|
|
114
|
+
else
|
|
115
|
+
body
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
private_class_method :truncated_body
|
|
119
|
+
|
|
120
|
+
def self.rate_limit_reset(response)
|
|
121
|
+
Time.now + response['X-RateLimit-Delay'].to_i
|
|
122
|
+
end
|
|
123
|
+
private_class_method :rate_limit_reset
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Stashable should be included in any class that wants the ability to stash
|
|
3
|
+
# arbitrary objects. It is mainly used by data objects that users can access
|
|
4
|
+
# through filters.
|
|
5
|
+
#
|
|
6
|
+
# @since v4.4.0
|
|
7
|
+
# @api private
|
|
8
|
+
module Stashable
|
|
9
|
+
# @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
|
|
10
|
+
# in filters
|
|
11
|
+
def stash
|
|
12
|
+
@stash ||= {}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
|
|
3
|
+
module Celerbrake
|
|
4
|
+
# Stat is a data structure that allows accumulating performance data (route
|
|
5
|
+
# performance, SQL query performance and such). It's powered by TDigests.
|
|
6
|
+
#
|
|
7
|
+
# Usually, one Stat corresponds to one metric (route or query,
|
|
8
|
+
# etc.). Incrementing a stat means pushing new performance statistics.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# stat = Celerbrake::Stat.new
|
|
12
|
+
# stat.increment_ms(2000)
|
|
13
|
+
# stat.to_h # Pack and serialize data so it can be transmitted.
|
|
14
|
+
#
|
|
15
|
+
# @since v3.2.0
|
|
16
|
+
class Stat
|
|
17
|
+
attr_accessor :sum, :sumsq, :tdigest
|
|
18
|
+
|
|
19
|
+
# @param [Float] sum The sum of duration in milliseconds
|
|
20
|
+
# @param [Float] sumsq The squared sum of duration in milliseconds
|
|
21
|
+
# @param [TDigest::TDigest] tdigest Packed durations. By default,
|
|
22
|
+
# compression is 20
|
|
23
|
+
def initialize(sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
|
|
24
|
+
@sum = sum
|
|
25
|
+
@sumsq = sumsq
|
|
26
|
+
@tdigest = tdigest
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Hash{String=>Object}] stats as a hash with compressed TDigest
|
|
31
|
+
# (serialized as base64)
|
|
32
|
+
def to_h
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
tdigest.compress!
|
|
35
|
+
{
|
|
36
|
+
'count' => tdigest.size,
|
|
37
|
+
'sum' => sum,
|
|
38
|
+
'sumsq' => sumsq,
|
|
39
|
+
'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes),
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Increments tdigest timings and updates tdigest with given +ms+ value.
|
|
45
|
+
#
|
|
46
|
+
# @param [Float] ms
|
|
47
|
+
# @return [void]
|
|
48
|
+
def increment_ms(ms)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
self.sum += ms
|
|
51
|
+
self.sumsq += ms * ms
|
|
52
|
+
|
|
53
|
+
tdigest.push(ms)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# We define custom inspect so that we weed out uninformative TDigest, which
|
|
58
|
+
# is also very slow to dump when we log Celerbrake::Stat.
|
|
59
|
+
#
|
|
60
|
+
# @return [String]
|
|
61
|
+
def inspect
|
|
62
|
+
"#<struct Celerbrake::Stat count=#{tdigest.size}, sum=#{sum}, sumsq=#{sumsq}>"
|
|
63
|
+
end
|
|
64
|
+
alias pretty_print inspect
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Responsible for sending data to Celerbrake synchronously via PUT or POST
|
|
3
|
+
# methods. Supports proxies.
|
|
4
|
+
#
|
|
5
|
+
# @see AsyncSender
|
|
6
|
+
# @api private
|
|
7
|
+
# @since v1.0.0
|
|
8
|
+
class SyncSender
|
|
9
|
+
# @return [String] body for HTTP requests
|
|
10
|
+
CONTENT_TYPE = 'application/json'.freeze
|
|
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
|
+
|
|
26
|
+
include Loggable
|
|
27
|
+
|
|
28
|
+
# @param [Symbol] method HTTP method to use to send payload
|
|
29
|
+
def initialize(method = :post)
|
|
30
|
+
@config = Celerbrake::Config.instance
|
|
31
|
+
@method = method
|
|
32
|
+
@rate_limit_reset = Time.now
|
|
33
|
+
@backlog = Backlog.new(self) if @config.backlog
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Sends a POST or PUT request to the given +endpoint+ with the +data+ payload.
|
|
37
|
+
#
|
|
38
|
+
# @param [#to_json] data
|
|
39
|
+
# @param [URI::HTTPS] endpoint
|
|
40
|
+
# @return [Hash{String=>String}] the parsed HTTP response
|
|
41
|
+
def send(data, promise, endpoint = @config.error_endpoint)
|
|
42
|
+
return promise if rate_limited_ip?(promise)
|
|
43
|
+
|
|
44
|
+
req = build_request(endpoint, data)
|
|
45
|
+
return promise if missing_body?(req, promise)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
response = build_https(endpoint).request(req)
|
|
49
|
+
rescue StandardError => ex
|
|
50
|
+
reason = "#{LOG_LABEL} HTTP error: #{ex}"
|
|
51
|
+
logger.error(reason)
|
|
52
|
+
return promise.reject(reason)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
parsed_resp = Response.parse(response)
|
|
56
|
+
handle_rate_limit(parsed_resp)
|
|
57
|
+
@backlog << [data, endpoint] if add_to_backlog?(parsed_resp)
|
|
58
|
+
|
|
59
|
+
return promise.reject(parsed_resp['error']) if parsed_resp.key?('error')
|
|
60
|
+
|
|
61
|
+
promise.resolve(parsed_resp)
|
|
62
|
+
end
|
|
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
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_https(uri)
|
|
75
|
+
Net::HTTP.new(uri.host, uri.port, *proxy_params).tap do |https|
|
|
76
|
+
https.use_ssl = uri.is_a?(URI::HTTPS)
|
|
77
|
+
if @config.timeout
|
|
78
|
+
https.open_timeout = @config.timeout
|
|
79
|
+
https.read_timeout = @config.timeout
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_request(uri, data)
|
|
85
|
+
req =
|
|
86
|
+
if @method == :put
|
|
87
|
+
Net::HTTP::Put.new(uri.request_uri)
|
|
88
|
+
else
|
|
89
|
+
Net::HTTP::Post.new(uri.request_uri)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
build_request_body(req, data)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_request_body(req, data)
|
|
96
|
+
req.body = data.to_json
|
|
97
|
+
|
|
98
|
+
req['Authorization'] = "Bearer #{@config.project_key}"
|
|
99
|
+
req['Content-Type'] = CONTENT_TYPE
|
|
100
|
+
req['User-Agent'] =
|
|
101
|
+
"#{Celerbrake::NOTIFIER_INFO[:name]}/#{Celerbrake::CELERBRAKE_RUBY_VERSION} " \
|
|
102
|
+
"Ruby/#{RUBY_VERSION}"
|
|
103
|
+
|
|
104
|
+
req
|
|
105
|
+
end
|
|
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
|
+
|
|
120
|
+
def proxy_params
|
|
121
|
+
return unless @config.proxy.key?(:host)
|
|
122
|
+
|
|
123
|
+
[@config.proxy[:host], @config.proxy[:port], @config.proxy[:user],
|
|
124
|
+
@config.proxy[:password]]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def rate_limited_ip?(promise)
|
|
128
|
+
rate_limited = Time.now < @rate_limit_reset
|
|
129
|
+
promise.reject("#{LOG_LABEL} IP is rate limited") if rate_limited
|
|
130
|
+
rate_limited
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def missing_body?(req, promise)
|
|
134
|
+
missing = req.body.nil?
|
|
135
|
+
|
|
136
|
+
if missing
|
|
137
|
+
reason = "#{LOG_LABEL} data was not sent because of missing body"
|
|
138
|
+
logger.error(reason)
|
|
139
|
+
promise.reject(reason)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
missing
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|