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.
Files changed (107) hide show
  1. checksums.yaml +5 -5
  2. data/lib/airbrake-ruby/async_sender.rb +24 -109
  3. data/lib/airbrake-ruby/backlog.rb +123 -0
  4. data/lib/airbrake-ruby/backtrace.rb +8 -7
  5. data/lib/airbrake-ruby/benchmark.rb +1 -1
  6. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  7. data/lib/airbrake-ruby/config/processor.rb +77 -0
  8. data/lib/airbrake-ruby/config/validator.rb +9 -3
  9. data/lib/airbrake-ruby/config.rb +78 -19
  10. data/lib/airbrake-ruby/context.rb +51 -0
  11. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  12. data/lib/airbrake-ruby/file_cache.rb +6 -0
  13. data/lib/airbrake-ruby/filter_chain.rb +18 -1
  14. data/lib/airbrake-ruby/filters/context_filter.rb +4 -5
  15. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  16. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +3 -3
  17. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  18. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +7 -7
  19. data/lib/airbrake-ruby/filters/git_repository_filter.rb +11 -2
  20. data/lib/airbrake-ruby/filters/git_revision_filter.rb +3 -1
  21. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  22. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  23. data/lib/airbrake-ruby/filters/keys_filter.rb +41 -22
  24. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  25. data/lib/airbrake-ruby/filters/sql_filter.rb +35 -11
  26. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  27. data/lib/airbrake-ruby/filters/thread_filter.rb +6 -5
  28. data/lib/airbrake-ruby/grouppable.rb +12 -0
  29. data/lib/airbrake-ruby/ignorable.rb +1 -2
  30. data/lib/airbrake-ruby/inspectable.rb +2 -2
  31. data/lib/airbrake-ruby/loggable.rb +1 -1
  32. data/lib/airbrake-ruby/mergeable.rb +12 -0
  33. data/lib/airbrake-ruby/monotonic_time.rb +6 -1
  34. data/lib/airbrake-ruby/nested_exception.rb +22 -1
  35. data/lib/airbrake-ruby/notice.rb +12 -17
  36. data/lib/airbrake-ruby/notice_notifier.rb +15 -7
  37. data/lib/airbrake-ruby/performance_breakdown.rb +16 -9
  38. data/lib/airbrake-ruby/performance_notifier.rb +106 -54
  39. data/lib/airbrake-ruby/promise.rb +1 -0
  40. data/lib/airbrake-ruby/query.rb +20 -14
  41. data/lib/airbrake-ruby/queue.rb +65 -0
  42. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  43. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  44. data/lib/airbrake-ruby/remote_settings.rb +128 -0
  45. data/lib/airbrake-ruby/request.rb +14 -11
  46. data/lib/airbrake-ruby/response.rb +56 -5
  47. data/lib/airbrake-ruby/stat.rb +27 -34
  48. data/lib/airbrake-ruby/sync_sender.rb +43 -11
  49. data/lib/airbrake-ruby/tdigest.rb +52 -66
  50. data/lib/airbrake-ruby/thread_pool.rb +139 -0
  51. data/lib/airbrake-ruby/time_truncate.rb +2 -2
  52. data/lib/airbrake-ruby/timed_trace.rb +1 -3
  53. data/lib/airbrake-ruby/truncator.rb +10 -4
  54. data/lib/airbrake-ruby/version.rb +11 -1
  55. data/lib/airbrake-ruby.rb +157 -78
  56. metadata +122 -116
  57. data/spec/airbrake_spec.rb +0 -324
  58. data/spec/async_sender_spec.rb +0 -155
  59. data/spec/backtrace_spec.rb +0 -427
  60. data/spec/benchmark_spec.rb +0 -33
  61. data/spec/code_hunk_spec.rb +0 -115
  62. data/spec/config/validator_spec.rb +0 -184
  63. data/spec/config_spec.rb +0 -154
  64. data/spec/deploy_notifier_spec.rb +0 -48
  65. data/spec/file_cache.rb +0 -36
  66. data/spec/filter_chain_spec.rb +0 -92
  67. data/spec/filters/context_filter_spec.rb +0 -23
  68. data/spec/filters/dependency_filter_spec.rb +0 -12
  69. data/spec/filters/exception_attributes_filter_spec.rb +0 -50
  70. data/spec/filters/gem_root_filter_spec.rb +0 -41
  71. data/spec/filters/git_last_checkout_filter_spec.rb +0 -46
  72. data/spec/filters/git_repository_filter.rb +0 -61
  73. data/spec/filters/git_revision_filter_spec.rb +0 -126
  74. data/spec/filters/keys_blacklist_spec.rb +0 -225
  75. data/spec/filters/keys_whitelist_spec.rb +0 -194
  76. data/spec/filters/root_directory_filter_spec.rb +0 -39
  77. data/spec/filters/sql_filter_spec.rb +0 -219
  78. data/spec/filters/system_exit_filter_spec.rb +0 -14
  79. data/spec/filters/thread_filter_spec.rb +0 -277
  80. data/spec/fixtures/notroot.txt +0 -7
  81. data/spec/fixtures/project_root/code.rb +0 -221
  82. data/spec/fixtures/project_root/empty_file.rb +0 -0
  83. data/spec/fixtures/project_root/long_line.txt +0 -1
  84. data/spec/fixtures/project_root/short_file.rb +0 -3
  85. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +0 -5
  86. data/spec/helpers.rb +0 -9
  87. data/spec/ignorable_spec.rb +0 -14
  88. data/spec/inspectable_spec.rb +0 -45
  89. data/spec/monotonic_time_spec.rb +0 -12
  90. data/spec/nested_exception_spec.rb +0 -73
  91. data/spec/notice_notifier_spec/options_spec.rb +0 -259
  92. data/spec/notice_notifier_spec.rb +0 -356
  93. data/spec/notice_spec.rb +0 -296
  94. data/spec/performance_breakdown_spec.rb +0 -12
  95. data/spec/performance_notifier_spec.rb +0 -435
  96. data/spec/promise_spec.rb +0 -197
  97. data/spec/query_spec.rb +0 -11
  98. data/spec/request_spec.rb +0 -11
  99. data/spec/response_spec.rb +0 -88
  100. data/spec/spec_helper.rb +0 -100
  101. data/spec/stashable_spec.rb +0 -23
  102. data/spec/stat_spec.rb +0 -47
  103. data/spec/sync_sender_spec.rb +0 -133
  104. data/spec/tdigest_spec.rb +0 -230
  105. data/spec/time_truncate_spec.rb +0 -13
  106. data/spec/timed_trace_spec.rb +0 -125
  107. 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
- # rubocop:disable Metrics/BlockLength
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
- start_time:,
18
- end_time: Time.now
20
+ timing: nil,
21
+ time: Time.now
19
22
  )
20
- super(method, route, status_code, start_time, end_time)
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' => TimeTruncate.utc_truncate_minutes(start_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 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
@@ -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 resource (route or query,
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.increment(Time.now - 200)
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 = Struct.new(:count, :sum, :sumsq, :tdigest) do
18
- # @param [Integer] count How many times this stat was incremented
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(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
24
- super(count, sum, sumsq, tdigest)
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
- tdigest.compress!
31
- {
32
- 'count' => count,
33
- 'sum' => sum,
34
- 'sumsq' => sumsq,
35
- 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes)
36
- }
37
- end
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 count and updates performance with given +ms+ value.
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
- self.count += 1
56
-
57
- self.sum += ms
58
- self.sumsq += ms * ms
49
+ @mutex.synchronize do
50
+ self.sum += ms
51
+ self.sumsq += ms * ms
59
52
 
60
- tdigest.push(ms)
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=#{count}, sum=#{sum}, sumsq=#{sumsq}>"
62
+ "#<struct Airbrake::Stat count=#{tdigest.size}, sum=#{sum}, sumsq=#{sumsq}>"
69
63
  end
70
- alias_method :pretty_print, :inspect
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.endpoint)
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,14 +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')
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::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
83
- " Ruby/#{RUBY_VERSION}"
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