airbrake-ruby 4.6.0 → 6.2.2
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 +5 -5
- data/lib/airbrake-ruby/async_sender.rb +24 -109
- data/lib/airbrake-ruby/backlog.rb +123 -0
- data/lib/airbrake-ruby/backtrace.rb +8 -7
- data/lib/airbrake-ruby/benchmark.rb +1 -1
- data/lib/airbrake-ruby/code_hunk.rb +1 -1
- data/lib/airbrake-ruby/config/processor.rb +77 -0
- data/lib/airbrake-ruby/config/validator.rb +9 -3
- data/lib/airbrake-ruby/config.rb +78 -19
- data/lib/airbrake-ruby/context.rb +51 -0
- data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
- data/lib/airbrake-ruby/file_cache.rb +6 -0
- data/lib/airbrake-ruby/filter_chain.rb +18 -1
- data/lib/airbrake-ruby/filters/context_filter.rb +4 -5
- data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +3 -3
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +7 -7
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +11 -2
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +3 -1
- data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
- data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
- data/lib/airbrake-ruby/filters/keys_filter.rb +41 -22
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +35 -11
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +6 -5
- data/lib/airbrake-ruby/grouppable.rb +12 -0
- data/lib/airbrake-ruby/ignorable.rb +1 -2
- data/lib/airbrake-ruby/inspectable.rb +2 -2
- data/lib/airbrake-ruby/loggable.rb +1 -1
- data/lib/airbrake-ruby/mergeable.rb +12 -0
- data/lib/airbrake-ruby/monotonic_time.rb +6 -1
- data/lib/airbrake-ruby/nested_exception.rb +22 -1
- data/lib/airbrake-ruby/notice.rb +12 -17
- data/lib/airbrake-ruby/notice_notifier.rb +15 -7
- data/lib/airbrake-ruby/performance_breakdown.rb +16 -9
- data/lib/airbrake-ruby/performance_notifier.rb +106 -54
- data/lib/airbrake-ruby/promise.rb +1 -0
- data/lib/airbrake-ruby/query.rb +20 -14
- data/lib/airbrake-ruby/queue.rb +65 -0
- data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
- data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
- data/lib/airbrake-ruby/remote_settings.rb +128 -0
- data/lib/airbrake-ruby/request.rb +14 -11
- data/lib/airbrake-ruby/response.rb +56 -5
- data/lib/airbrake-ruby/stat.rb +27 -34
- data/lib/airbrake-ruby/sync_sender.rb +43 -11
- data/lib/airbrake-ruby/tdigest.rb +52 -66
- data/lib/airbrake-ruby/thread_pool.rb +139 -0
- data/lib/airbrake-ruby/time_truncate.rb +2 -2
- data/lib/airbrake-ruby/timed_trace.rb +1 -3
- data/lib/airbrake-ruby/truncator.rb +10 -4
- data/lib/airbrake-ruby/version.rb +11 -1
- data/lib/airbrake-ruby.rb +157 -78
- metadata +122 -116
- data/spec/airbrake_spec.rb +0 -324
- data/spec/async_sender_spec.rb +0 -155
- data/spec/backtrace_spec.rb +0 -427
- data/spec/benchmark_spec.rb +0 -33
- data/spec/code_hunk_spec.rb +0 -115
- data/spec/config/validator_spec.rb +0 -184
- data/spec/config_spec.rb +0 -154
- data/spec/deploy_notifier_spec.rb +0 -48
- data/spec/file_cache.rb +0 -36
- data/spec/filter_chain_spec.rb +0 -92
- data/spec/filters/context_filter_spec.rb +0 -23
- data/spec/filters/dependency_filter_spec.rb +0 -12
- data/spec/filters/exception_attributes_filter_spec.rb +0 -50
- data/spec/filters/gem_root_filter_spec.rb +0 -41
- data/spec/filters/git_last_checkout_filter_spec.rb +0 -46
- data/spec/filters/git_repository_filter.rb +0 -61
- data/spec/filters/git_revision_filter_spec.rb +0 -126
- data/spec/filters/keys_blacklist_spec.rb +0 -225
- data/spec/filters/keys_whitelist_spec.rb +0 -194
- data/spec/filters/root_directory_filter_spec.rb +0 -39
- data/spec/filters/sql_filter_spec.rb +0 -219
- data/spec/filters/system_exit_filter_spec.rb +0 -14
- data/spec/filters/thread_filter_spec.rb +0 -277
- data/spec/fixtures/notroot.txt +0 -7
- data/spec/fixtures/project_root/code.rb +0 -221
- data/spec/fixtures/project_root/empty_file.rb +0 -0
- data/spec/fixtures/project_root/long_line.txt +0 -1
- data/spec/fixtures/project_root/short_file.rb +0 -3
- data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +0 -5
- data/spec/helpers.rb +0 -9
- data/spec/ignorable_spec.rb +0 -14
- data/spec/inspectable_spec.rb +0 -45
- data/spec/monotonic_time_spec.rb +0 -12
- data/spec/nested_exception_spec.rb +0 -73
- data/spec/notice_notifier_spec/options_spec.rb +0 -259
- data/spec/notice_notifier_spec.rb +0 -356
- data/spec/notice_spec.rb +0 -296
- data/spec/performance_breakdown_spec.rb +0 -12
- data/spec/performance_notifier_spec.rb +0 -435
- data/spec/promise_spec.rb +0 -197
- data/spec/query_spec.rb +0 -11
- data/spec/request_spec.rb +0 -11
- data/spec/response_spec.rb +0 -88
- data/spec/spec_helper.rb +0 -100
- data/spec/stashable_spec.rb +0 -23
- data/spec/stat_spec.rb +0 -47
- data/spec/sync_sender_spec.rb +0 -133
- data/spec/tdigest_spec.rb +0 -230
- data/spec/time_truncate_spec.rb +0 -13
- data/spec/timed_trace_spec.rb +0 -125
- data/spec/truncator_spec.rb +0 -238
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Airbrake
|
|
2
|
+
class RemoteSettings
|
|
3
|
+
# Callback is a class that provides a callback for the config poller, which
|
|
4
|
+
# updates the local config according to the data.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
# @since v5.0.2
|
|
8
|
+
class Callback
|
|
9
|
+
def initialize(config)
|
|
10
|
+
@config = config
|
|
11
|
+
@orig_error_notifications = config.error_notifications
|
|
12
|
+
@orig_performance_stats = config.performance_stats
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [Airbrake::RemoteSettings::SettingsData] data
|
|
16
|
+
# @return [void]
|
|
17
|
+
def call(data)
|
|
18
|
+
@config.logger.debug do
|
|
19
|
+
"#{LOG_LABEL} applying remote settings: #{data.to_h}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@config.error_host = data.error_host if data.error_host
|
|
23
|
+
@config.apm_host = data.apm_host if data.apm_host
|
|
24
|
+
|
|
25
|
+
process_error_notifications(data)
|
|
26
|
+
process_performance_stats(data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def process_error_notifications(data)
|
|
32
|
+
return unless @orig_error_notifications
|
|
33
|
+
|
|
34
|
+
@config.error_notifications = data.error_notifications?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def process_performance_stats(data)
|
|
38
|
+
return unless @orig_performance_stats
|
|
39
|
+
|
|
40
|
+
@config.performance_stats = data.performance_stats?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Airbrake
|
|
2
|
+
class RemoteSettings
|
|
3
|
+
# SettingsData is a container, which wraps JSON payload returned by the
|
|
4
|
+
# remote settings API. It exposes the payload via convenient methods and
|
|
5
|
+
# also ensures that in case some data from the payload is missing, a default
|
|
6
|
+
# value would be returned instead.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # Create the object and pass initial data (empty hash).
|
|
10
|
+
# settings_data = SettingsData.new({})
|
|
11
|
+
#
|
|
12
|
+
# settings_data.interval #=> 600
|
|
13
|
+
#
|
|
14
|
+
# @since v5.0.0
|
|
15
|
+
# @api private
|
|
16
|
+
class SettingsData
|
|
17
|
+
# @return [Integer] how frequently we should poll the config API
|
|
18
|
+
DEFAULT_INTERVAL = 600
|
|
19
|
+
|
|
20
|
+
# @return [String] API version of the S3 API to poll
|
|
21
|
+
API_VER = '2020-06-18'.freeze
|
|
22
|
+
|
|
23
|
+
# @return [String] what path to poll
|
|
24
|
+
CONFIG_ROUTE_PATTERN =
|
|
25
|
+
"%<host>s/#{API_VER}/config/%<project_id>s/config.json".freeze
|
|
26
|
+
|
|
27
|
+
# @return [Hash{Symbol=>String}] the hash of all supported settings where
|
|
28
|
+
# the value is the name of the setting returned by the API
|
|
29
|
+
SETTINGS = {
|
|
30
|
+
errors: 'errors'.freeze,
|
|
31
|
+
apm: 'apm'.freeze,
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# @param [Integer] project_id
|
|
35
|
+
# @param [Hash{String=>Object}] data
|
|
36
|
+
def initialize(project_id, data)
|
|
37
|
+
@project_id = project_id
|
|
38
|
+
@data = data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Merges the given +hash+ with internal data.
|
|
42
|
+
#
|
|
43
|
+
# @param [Hash{String=>Object}] hash
|
|
44
|
+
# @return [self]
|
|
45
|
+
def merge!(hash)
|
|
46
|
+
@data.merge!(hash)
|
|
47
|
+
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Integer] how frequently we should poll for the config
|
|
52
|
+
def interval
|
|
53
|
+
return DEFAULT_INTERVAL if !@data.key?('poll_sec') || !@data['poll_sec']
|
|
54
|
+
|
|
55
|
+
@data['poll_sec'] > 0 ? @data['poll_sec'] : DEFAULT_INTERVAL
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param [String] remote_config_host
|
|
59
|
+
# @return [String] where the config is stored on S3.
|
|
60
|
+
def config_route(remote_config_host)
|
|
61
|
+
if @data['config_route'] && !@data['config_route'].empty?
|
|
62
|
+
return "#{remote_config_host.chomp('/')}/#{@data['config_route']}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
format(
|
|
66
|
+
CONFIG_ROUTE_PATTERN,
|
|
67
|
+
host: remote_config_host.chomp('/'),
|
|
68
|
+
project_id: @project_id,
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Boolean] whether error notifications are enabled
|
|
73
|
+
def error_notifications?
|
|
74
|
+
return true unless (s = find_setting(SETTINGS[:errors]))
|
|
75
|
+
|
|
76
|
+
s['enabled']
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] whether APM is enabled
|
|
80
|
+
def performance_stats?
|
|
81
|
+
return true unless (s = find_setting(SETTINGS[:apm]))
|
|
82
|
+
|
|
83
|
+
s['enabled']
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [String, nil] the host, which provides the API endpoint to which
|
|
87
|
+
# exceptions should be sent
|
|
88
|
+
def error_host
|
|
89
|
+
return unless (s = find_setting(SETTINGS[:errors]))
|
|
90
|
+
|
|
91
|
+
s['endpoint']
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [String, nil] the host, which provides the API endpoint to which
|
|
95
|
+
# APM data should be sent
|
|
96
|
+
def apm_host
|
|
97
|
+
return unless (s = find_setting(SETTINGS[:apm]))
|
|
98
|
+
|
|
99
|
+
s['endpoint']
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Hash{String=>Object}] raw representation of JSON payload
|
|
103
|
+
def to_h
|
|
104
|
+
@data.dup
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def find_setting(name)
|
|
110
|
+
return unless @data.key?('settings')
|
|
111
|
+
|
|
112
|
+
@data['settings'].find { |s| s['name'] == name }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module Airbrake
|
|
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 Airbrake::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: Airbrake::NOTIFIER_INFO[:name],
|
|
20
|
+
notifier_version: Airbrake::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 [Airbrake::RemoteSettings::SettingsData]
|
|
34
|
+
# @return [Airbrake::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 [Airbrake::RemoteSettings::SettingsData]
|
|
42
|
+
def initialize(project_id, host, &block)
|
|
43
|
+
@data = SettingsData.new(project_id, {})
|
|
44
|
+
@host = host
|
|
45
|
+
@block = block
|
|
46
|
+
@config = Airbrake::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
|
|
@@ -4,20 +4,28 @@ module Airbrake
|
|
|
4
4
|
# @see Airbrake.notify_request
|
|
5
5
|
# @api public
|
|
6
6
|
# @since v3.2.0
|
|
7
|
-
|
|
8
|
-
Request = Struct.new(:method, :route, :status_code, :start_time, :end_time) do
|
|
7
|
+
class Request
|
|
9
8
|
include HashKeyable
|
|
10
9
|
include Ignorable
|
|
11
10
|
include Stashable
|
|
11
|
+
include Mergeable
|
|
12
|
+
include Grouppable
|
|
13
|
+
|
|
14
|
+
attr_accessor :method, :route, :status_code, :timing, :time
|
|
12
15
|
|
|
13
16
|
def initialize(
|
|
14
17
|
method:,
|
|
15
18
|
route:,
|
|
16
19
|
status_code:,
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
timing: nil,
|
|
21
|
+
time: Time.now
|
|
19
22
|
)
|
|
20
|
-
|
|
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
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def destination
|
|
@@ -28,18 +36,13 @@ module Airbrake
|
|
|
28
36
|
'routes'
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
def groups
|
|
32
|
-
{}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
39
|
def to_h
|
|
36
40
|
{
|
|
37
41
|
'method' => method,
|
|
38
42
|
'route' => route,
|
|
39
43
|
'statusCode' => status_code,
|
|
40
|
-
'time' =>
|
|
44
|
+
'time' => @time_utc,
|
|
41
45
|
}.delete_if { |_key, val| val.nil? }
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
|
-
# rubocop:enable Metrics/BlockLength
|
|
45
48
|
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
|
data/lib/airbrake-ruby/stat.rb
CHANGED
|
@@ -1,63 +1,57 @@
|
|
|
1
1
|
require 'base64'
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/BlockLength
|
|
4
3
|
module Airbrake
|
|
5
4
|
# Stat is a data structure that allows accumulating performance data (route
|
|
6
5
|
# performance, SQL query performance and such). It's powered by TDigests.
|
|
7
6
|
#
|
|
8
|
-
# Usually, one Stat corresponds to one
|
|
7
|
+
# Usually, one Stat corresponds to one metric (route or query,
|
|
9
8
|
# etc.). Incrementing a stat means pushing new performance statistics.
|
|
10
9
|
#
|
|
11
10
|
# @example
|
|
12
11
|
# stat = Airbrake::Stat.new
|
|
13
|
-
# stat.
|
|
12
|
+
# stat.increment_ms(2000)
|
|
14
13
|
# stat.to_h # Pack and serialize data so it can be transmitted.
|
|
15
14
|
#
|
|
16
15
|
# @since v3.2.0
|
|
17
|
-
Stat
|
|
18
|
-
|
|
16
|
+
class Stat
|
|
17
|
+
attr_accessor :sum, :sumsq, :tdigest
|
|
18
|
+
|
|
19
19
|
# @param [Float] sum The sum of duration in milliseconds
|
|
20
20
|
# @param [Float] sumsq The squared sum of duration in milliseconds
|
|
21
21
|
# @param [TDigest::TDigest] tdigest Packed durations. By default,
|
|
22
22
|
# compression is 20
|
|
23
|
-
def initialize(
|
|
24
|
-
|
|
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
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
# @return [Hash{String=>Object}] stats as a hash with compressed TDigest
|
|
28
31
|
# (serialized as base64)
|
|
29
32
|
def to_h
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Increments count and updates performance with the difference of +end_time+
|
|
40
|
-
# and +start_time+.
|
|
41
|
-
#
|
|
42
|
-
# @param [Date] start_time
|
|
43
|
-
# @param [Date] end_time
|
|
44
|
-
# @return [void]
|
|
45
|
-
def increment(start_time, end_time = nil)
|
|
46
|
-
end_time ||= Time.new
|
|
47
|
-
increment_ms((end_time - start_time) * 1000)
|
|
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
|
|
48
42
|
end
|
|
49
43
|
|
|
50
|
-
# Increments
|
|
44
|
+
# Increments tdigest timings and updates tdigest with given +ms+ value.
|
|
51
45
|
#
|
|
52
46
|
# @param [Float] ms
|
|
53
47
|
# @return [void]
|
|
54
48
|
def increment_ms(ms)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.sumsq += ms * ms
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
self.sum += ms
|
|
51
|
+
self.sumsq += ms * ms
|
|
59
52
|
|
|
60
|
-
|
|
53
|
+
tdigest.push(ms)
|
|
54
|
+
end
|
|
61
55
|
end
|
|
62
56
|
|
|
63
57
|
# We define custom inspect so that we weed out uninformative TDigest, which
|
|
@@ -65,9 +59,8 @@ module Airbrake
|
|
|
65
59
|
#
|
|
66
60
|
# @return [String]
|
|
67
61
|
def inspect
|
|
68
|
-
"#<struct Airbrake::Stat count=#{
|
|
62
|
+
"#<struct Airbrake::Stat count=#{tdigest.size}, sum=#{sum}, sumsq=#{sumsq}>"
|
|
69
63
|
end
|
|
70
|
-
|
|
64
|
+
alias pretty_print inspect
|
|
71
65
|
end
|
|
72
66
|
end
|
|
73
|
-
# rubocop:enable Metrics/BlockLength
|
|
@@ -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.
|
|
@@ -23,18 +38,14 @@ module Airbrake
|
|
|
23
38
|
# @param [#to_json] data
|
|
24
39
|
# @param [URI::HTTPS] endpoint
|
|
25
40
|
# @return [Hash{String=>String}] the parsed HTTP response
|
|
26
|
-
def send(data, promise, endpoint = @config.
|
|
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,14 +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')
|
|
60
|
+
|
|
50
61
|
promise.resolve(parsed_resp)
|
|
51
62
|
end
|
|
52
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
|
+
|
|
53
72
|
private
|
|
54
73
|
|
|
55
74
|
def build_https(uri)
|
|
@@ -79,12 +98,25 @@ module Airbrake
|
|
|
79
98
|
req['Authorization'] = "Bearer #{@config.project_key}"
|
|
80
99
|
req['Content-Type'] = CONTENT_TYPE
|
|
81
100
|
req['User-Agent'] =
|
|
82
|
-
"#{Airbrake::
|
|
83
|
-
"
|
|
101
|
+
"#{Airbrake::NOTIFIER_INFO[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION} " \
|
|
102
|
+
"Ruby/#{RUBY_VERSION}"
|
|
84
103
|
|
|
85
104
|
req
|
|
86
105
|
end
|
|
87
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
|
+
|
|
88
120
|
def proxy_params
|
|
89
121
|
return unless @config.proxy.key?(:host)
|
|
90
122
|
|