airbrake-ruby 3.2.2-java

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +554 -0
  3. data/lib/airbrake-ruby/async_sender.rb +119 -0
  4. data/lib/airbrake-ruby/backtrace.rb +194 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +53 -0
  6. data/lib/airbrake-ruby/config.rb +238 -0
  7. data/lib/airbrake-ruby/config/validator.rb +63 -0
  8. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  9. data/lib/airbrake-ruby/file_cache.rb +48 -0
  10. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  11. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  18. data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
  19. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  20. data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  25. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  26. data/lib/airbrake-ruby/ignorable.rb +44 -0
  27. data/lib/airbrake-ruby/nested_exception.rb +39 -0
  28. data/lib/airbrake-ruby/notice.rb +165 -0
  29. data/lib/airbrake-ruby/notice_notifier.rb +228 -0
  30. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  31. data/lib/airbrake-ruby/promise.rb +99 -0
  32. data/lib/airbrake-ruby/response.rb +71 -0
  33. data/lib/airbrake-ruby/stat.rb +56 -0
  34. data/lib/airbrake-ruby/sync_sender.rb +111 -0
  35. data/lib/airbrake-ruby/tdigest.rb +393 -0
  36. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  37. data/lib/airbrake-ruby/truncator.rb +115 -0
  38. data/lib/airbrake-ruby/version.rb +6 -0
  39. data/spec/airbrake_spec.rb +171 -0
  40. data/spec/async_sender_spec.rb +154 -0
  41. data/spec/backtrace_spec.rb +438 -0
  42. data/spec/code_hunk_spec.rb +118 -0
  43. data/spec/config/validator_spec.rb +189 -0
  44. data/spec/config_spec.rb +281 -0
  45. data/spec/deploy_notifier_spec.rb +41 -0
  46. data/spec/file_cache.rb +36 -0
  47. data/spec/filter_chain_spec.rb +83 -0
  48. data/spec/filters/context_filter_spec.rb +25 -0
  49. data/spec/filters/dependency_filter_spec.rb +14 -0
  50. data/spec/filters/exception_attributes_filter_spec.rb +63 -0
  51. data/spec/filters/gem_root_filter_spec.rb +44 -0
  52. data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
  53. data/spec/filters/git_repository_filter.rb +53 -0
  54. data/spec/filters/git_revision_filter_spec.rb +126 -0
  55. data/spec/filters/keys_blacklist_spec.rb +236 -0
  56. data/spec/filters/keys_whitelist_spec.rb +205 -0
  57. data/spec/filters/root_directory_filter_spec.rb +42 -0
  58. data/spec/filters/sql_filter_spec.rb +219 -0
  59. data/spec/filters/system_exit_filter_spec.rb +14 -0
  60. data/spec/filters/thread_filter_spec.rb +279 -0
  61. data/spec/fixtures/notroot.txt +7 -0
  62. data/spec/fixtures/project_root/code.rb +221 -0
  63. data/spec/fixtures/project_root/empty_file.rb +0 -0
  64. data/spec/fixtures/project_root/long_line.txt +1 -0
  65. data/spec/fixtures/project_root/short_file.rb +3 -0
  66. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  67. data/spec/helpers.rb +9 -0
  68. data/spec/ignorable_spec.rb +14 -0
  69. data/spec/nested_exception_spec.rb +75 -0
  70. data/spec/notice_notifier_spec.rb +436 -0
  71. data/spec/notice_notifier_spec/options_spec.rb +266 -0
  72. data/spec/notice_spec.rb +297 -0
  73. data/spec/performance_notifier_spec.rb +287 -0
  74. data/spec/promise_spec.rb +165 -0
  75. data/spec/response_spec.rb +82 -0
  76. data/spec/spec_helper.rb +102 -0
  77. data/spec/stat_spec.rb +35 -0
  78. data/spec/sync_sender_spec.rb +140 -0
  79. data/spec/tdigest_spec.rb +230 -0
  80. data/spec/time_truncate_spec.rb +13 -0
  81. data/spec/truncator_spec.rb +238 -0
  82. metadata +278 -0
@@ -0,0 +1,161 @@
1
+ module Airbrake
2
+ # QueryNotifier aggregates information about SQL queries and periodically sends
3
+ # collected data to Airbrake.
4
+ #
5
+ # @api public
6
+ # @since v3.2.0
7
+ class PerformanceNotifier
8
+ # @param [Airbrake::Config] config
9
+ def initialize(config)
10
+ @config =
11
+ if config.is_a?(Config)
12
+ config
13
+ else
14
+ loc = caller_locations(1..1).first
15
+ signature = "#{self.class.name}##{__method__}"
16
+ warn(
17
+ "#{loc.path}:#{loc.lineno}: warning: passing a Hash to #{signature} " \
18
+ 'is deprecated. Pass `Airbrake::Config` instead'
19
+ )
20
+ Config.new(config)
21
+ end
22
+
23
+ @flush_period = @config.performance_stats_flush_period
24
+ @sender = SyncSender.new(@config, :put)
25
+ @payload = {}
26
+ @schedule_flush = nil
27
+ @mutex = Mutex.new
28
+ @filter_chain = FilterChain.new
29
+ end
30
+
31
+ # @param [Hash] resource
32
+ # @param [Airbrake::Promise] promise
33
+ # @see Airbrake.notify_query
34
+ # @see Airbrake.notify_request
35
+ def notify(resource, promise = Airbrake::Promise.new)
36
+ if @config.ignored_environment?
37
+ return promise.reject("The '#{@config.environment}' environment is ignored")
38
+ end
39
+
40
+ unless @config.performance_stats
41
+ return promise.reject("The Performance Stats feature is disabled")
42
+ end
43
+
44
+ @filter_chain.refine(resource)
45
+ return if resource.ignored?
46
+
47
+ @mutex.synchronize do
48
+ @payload[resource] ||= Airbrake::Stat.new
49
+ @payload[resource].increment(resource.start_time, resource.end_time)
50
+
51
+ if @flush_period > 0
52
+ schedule_flush(promise)
53
+ else
54
+ send(@payload, promise)
55
+ end
56
+ end
57
+
58
+ promise
59
+ end
60
+
61
+ # @see Airbrake.add_performance_filter
62
+ def add_filter(filter = nil, &block)
63
+ @filter_chain.add_filter(block_given? ? block : filter)
64
+ end
65
+
66
+ # @see Airbrake.delete_performance_filter
67
+ def delete_filter(filter_class)
68
+ @filter_chain.delete_filter(filter_class)
69
+ end
70
+
71
+ private
72
+
73
+ def schedule_flush(promise)
74
+ @schedule_flush ||= Thread.new do
75
+ sleep(@flush_period)
76
+
77
+ payload = nil
78
+ @mutex.synchronize do
79
+ payload = @payload
80
+ @payload = {}
81
+ @schedule_flush = nil
82
+ end
83
+
84
+ send(payload, promise)
85
+ end
86
+ end
87
+
88
+ def send(payload, promise)
89
+ signature = "#{self.class.name}##{__method__}"
90
+ raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
91
+
92
+ @config.logger.debug("#{LOG_LABEL} #{signature}: #{payload}")
93
+
94
+ payload.group_by { |k, _v| k.name }.each do |resource_name, data|
95
+ @sender.send(
96
+ { resource_name => data.map { |k, v| k.to_h.merge!(v.to_h) } },
97
+ promise,
98
+ URI.join(
99
+ @config.host,
100
+ "api/v5/projects/#{@config.project_id}/#{resource_name}-stats"
101
+ )
102
+ )
103
+ end
104
+ end
105
+ end
106
+
107
+ # Request holds request data that powers route stats.
108
+ #
109
+ # @see Airbrake.notify_request
110
+ # @api public
111
+ # @since v3.2.0
112
+ Request = Struct.new(:method, :route, :status_code, :start_time, :end_time) do
113
+ include HashKeyable
114
+ include Ignorable
115
+
116
+ def initialize(method:, route:, status_code:, start_time:, end_time: Time.now)
117
+ @ignored = false
118
+ super(method, route, status_code, start_time, end_time)
119
+ end
120
+
121
+ def name
122
+ 'routes'
123
+ end
124
+
125
+ def to_h
126
+ {
127
+ 'method' => method,
128
+ 'route' => route,
129
+ 'statusCode' => status_code,
130
+ 'time' => TimeTruncate.utc_truncate_minutes(start_time)
131
+ }
132
+ end
133
+ end
134
+
135
+ # Query holds SQL query data that powers SQL query collection.
136
+ #
137
+ # @see Airbrake.notify_query
138
+ # @api public
139
+ # @since v3.2.0
140
+ Query = Struct.new(:method, :route, :query, :start_time, :end_time) do
141
+ include HashKeyable
142
+ include Ignorable
143
+
144
+ def initialize(method:, route:, query:, start_time:, end_time: Time.now)
145
+ super(method, route, query, start_time, end_time)
146
+ end
147
+
148
+ def name
149
+ 'queries'
150
+ end
151
+
152
+ def to_h
153
+ {
154
+ 'method' => method,
155
+ 'route' => route,
156
+ 'query' => query,
157
+ 'time' => TimeTruncate.utc_truncate_minutes(start_time)
158
+ }
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,99 @@
1
+ module Airbrake
2
+ # Represents a simplified promise object (similar to promises found in
3
+ # JavaScript), which allows chaining callbacks that are executed when the
4
+ # promise is either resolved or rejected.
5
+ #
6
+ # @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
7
+ # @see https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/promise.rb
8
+ # @since v1.7.0
9
+ class Promise
10
+ # @api private
11
+ # @return [Hash<String,String>] either successful response containing the
12
+ # +id+ key or unsuccessful response containing the +error+ key
13
+ # @note This is a non-blocking call!
14
+ attr_reader :value
15
+
16
+ def initialize
17
+ @on_resolved = []
18
+ @on_rejected = []
19
+ @value = {}
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ # Attaches a callback to be executed when the promise is resolved.
24
+ #
25
+ # @example
26
+ # Airbrake::Promise.new.then { |response| puts response }
27
+ # #=> {"id"=>"00054415-8201-e9c6-65d6-fc4d231d2871",
28
+ # # "url"=>"http://localhost/locate/00054415-8201-e9c6-65d6-fc4d231d2871"}
29
+ #
30
+ # @yield [response]
31
+ # @yieldparam response [Hash<String,String>] Contains the `id` & `url` keys
32
+ # @return [self]
33
+ def then(&block)
34
+ @mutex.synchronize do
35
+ if @value.key?('id')
36
+ yield(@value)
37
+ return self
38
+ end
39
+
40
+ @on_resolved << block
41
+ end
42
+
43
+ self
44
+ end
45
+
46
+ # Attaches a callback to be executed when the promise is rejected.
47
+ #
48
+ # @example
49
+ # Airbrake::Promise.new.rescue { |error| raise error }
50
+ #
51
+ # @yield [error] The error message from the API
52
+ # @yieldparam error [String]
53
+ # @return [self]
54
+ def rescue(&block)
55
+ @mutex.synchronize do
56
+ if @value.key?('error')
57
+ yield(@value['error'])
58
+ return self
59
+ end
60
+
61
+ @on_rejected << block
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ # Resolves the promise.
68
+ #
69
+ # @example
70
+ # Airbrake::Promise.new.resolve('id' => '123')
71
+ #
72
+ # @param response [Hash<String,String>]
73
+ # @return [self]
74
+ def resolve(response)
75
+ @mutex.synchronize do
76
+ @value = response
77
+ @on_resolved.each { |callback| callback.call(response) }
78
+ end
79
+
80
+ self
81
+ end
82
+
83
+ # Rejects the promise.
84
+ #
85
+ # @example
86
+ # Airbrake::Promise.new.reject('Something went wrong')
87
+ #
88
+ # @param error [String]
89
+ # @return [self]
90
+ def reject(error)
91
+ @mutex.synchronize do
92
+ @value['error'] = error
93
+ @on_rejected.each { |callback| callback.call(error) }
94
+ end
95
+
96
+ self
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,71 @@
1
+ module Airbrake
2
+ # Parses responses coming from the Airbrake 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 an IP sends over 10k/min notices
12
+ TOO_MANY_REQUESTS = 429
13
+
14
+ # Parses HTTP responses from the Airbrake API.
15
+ #
16
+ # @param [Net::HTTPResponse] response
17
+ # @param [Logger] logger
18
+ # @return [Hash{String=>String}] parsed response
19
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
20
+ def self.parse(response, logger)
21
+ code = response.code.to_i
22
+ body = response.body
23
+
24
+ begin
25
+ case code
26
+ when 200, 204
27
+ logger.debug("#{LOG_LABEL} #{name} (#{code}): #{body}")
28
+ { response.msg => response.body }
29
+ when 201
30
+ parsed_body = JSON.parse(body)
31
+ logger.debug("#{LOG_LABEL} #{name} (#{code}): #{parsed_body}")
32
+ parsed_body
33
+ when 400, 401, 403, 420
34
+ parsed_body = JSON.parse(body)
35
+ logger.error("#{LOG_LABEL} #{parsed_body['message']}")
36
+ parsed_body
37
+ when TOO_MANY_REQUESTS
38
+ parsed_body = JSON.parse(body)
39
+ msg = "#{LOG_LABEL} #{parsed_body['message']}"
40
+ logger.error(msg)
41
+ { 'error' => msg, 'rate_limit_reset' => rate_limit_reset(response) }
42
+ else
43
+ body_msg = truncated_body(body)
44
+ logger.error("#{LOG_LABEL} unexpected code (#{code}). Body: #{body_msg}")
45
+ { 'error' => body_msg }
46
+ end
47
+ rescue StandardError => ex
48
+ body_msg = truncated_body(body)
49
+ logger.error("#{LOG_LABEL} error while parsing body (#{ex}). Body: #{body_msg}")
50
+ { 'error' => ex.inspect }
51
+ end
52
+ end
53
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
54
+
55
+ def self.truncated_body(body)
56
+ if body.nil?
57
+ '[EMPTY_BODY]'.freeze
58
+ elsif body.length > TRUNCATE_LIMIT
59
+ body[0..TRUNCATE_LIMIT] << '...'
60
+ else
61
+ body
62
+ end
63
+ end
64
+ private_class_method :truncated_body
65
+
66
+ def self.rate_limit_reset(response)
67
+ Time.now + response['X-RateLimit-Delay'].to_i
68
+ end
69
+ private_class_method :rate_limit_reset
70
+ end
71
+ end
@@ -0,0 +1,56 @@
1
+ require 'base64'
2
+
3
+ module Airbrake
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 resource (route or query,
8
+ # etc.). Incrementing a stat means pushing new performance statistics.
9
+ #
10
+ # @example
11
+ # stat = Airbrake::Stat.new
12
+ # stat.increment(Time.now - 200)
13
+ # stat.to_h # Pack and serialize data so it can be transmitted.
14
+ #
15
+ # @since v3.2.0
16
+ Stat = Struct.new(:count, :sum, :sumsq, :tdigest) do
17
+ # @param [Integer] count How many times this stat was incremented
18
+ # @param [Float] sum The sum of duration in milliseconds
19
+ # @param [Float] sumsq The squared sum of duration in milliseconds
20
+ # @param [TDigest::TDigest] tdigest Packed durations. By default,
21
+ # compression is 20
22
+ def initialize(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
23
+ super(count, sum, sumsq, tdigest)
24
+ end
25
+
26
+ # @return [Hash{String=>Object}] stats as a hash with compressed TDigest
27
+ # (serialized as base64)
28
+ def to_h
29
+ tdigest.compress!
30
+ {
31
+ 'count' => count,
32
+ 'sum' => sum,
33
+ 'sumsq' => sumsq,
34
+ 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes)
35
+ }
36
+ end
37
+
38
+ # Increments count and updates performance with the difference of +end_time+
39
+ # and +start_time+.
40
+ #
41
+ # @param [Date] start_time
42
+ # @param [Date] end_time
43
+ # @return [void]
44
+ def increment(start_time, end_time = nil)
45
+ end_time ||= Time.new
46
+
47
+ self.count += 1
48
+
49
+ ms = (end_time - start_time) * 1000
50
+ self.sum += ms
51
+ self.sumsq += ms * ms
52
+
53
+ tdigest.push(ms)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,111 @@
1
+ module Airbrake
2
+ # Responsible for sending data to Airbrake 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
+ # @param [Airbrake::Config] config
13
+ def initialize(config, method = :post)
14
+ @config = config
15
+ @method = method
16
+ @rate_limit_reset = Time.now
17
+ end
18
+
19
+ # Sends a POST or PUT request to the given +endpoint+ with the +data+ payload.
20
+ #
21
+ # @param [#to_json] data
22
+ # @param [URI::HTTPS] endpoint
23
+ # @return [Hash{String=>String}] the parsed HTTP response
24
+ def send(data, promise, endpoint = @config.endpoint)
25
+ return promise if rate_limited_ip?(promise)
26
+
27
+ response = nil
28
+ req = build_request(endpoint, data)
29
+
30
+ return promise if missing_body?(req, promise)
31
+
32
+ https = build_https(endpoint)
33
+
34
+ begin
35
+ response = https.request(req)
36
+ rescue StandardError => ex
37
+ reason = "#{LOG_LABEL} HTTP error: #{ex}"
38
+ @config.logger.error(reason)
39
+ return promise.reject(reason)
40
+ end
41
+
42
+ parsed_resp = Response.parse(response, @config.logger)
43
+ if parsed_resp.key?('rate_limit_reset')
44
+ @rate_limit_reset = parsed_resp['rate_limit_reset']
45
+ end
46
+
47
+ return promise.reject(parsed_resp['error']) if parsed_resp.key?('error')
48
+ promise.resolve(parsed_resp)
49
+ end
50
+
51
+ private
52
+
53
+ def build_https(uri)
54
+ Net::HTTP.new(uri.host, uri.port, *proxy_params).tap do |https|
55
+ https.use_ssl = uri.is_a?(URI::HTTPS)
56
+ if @config.timeout
57
+ https.open_timeout = @config.timeout
58
+ https.read_timeout = @config.timeout
59
+ end
60
+ end
61
+ end
62
+
63
+ def build_request(uri, data)
64
+ req =
65
+ if @method == :put
66
+ Net::HTTP::Put.new(uri.request_uri)
67
+ else
68
+ Net::HTTP::Post.new(uri.request_uri)
69
+ end
70
+
71
+ build_request_body(req, data)
72
+ end
73
+
74
+ def build_request_body(req, data)
75
+ req.body = data.to_json
76
+
77
+ req['Authorization'] = "Bearer #{@config.project_key}"
78
+ req['Content-Type'] = CONTENT_TYPE
79
+ req['User-Agent'] =
80
+ "#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
81
+ " Ruby/#{RUBY_VERSION}"
82
+
83
+ req
84
+ end
85
+
86
+ def proxy_params
87
+ return unless @config.proxy.key?(:host)
88
+
89
+ [@config.proxy[:host], @config.proxy[:port], @config.proxy[:user],
90
+ @config.proxy[:password]]
91
+ end
92
+
93
+ def rate_limited_ip?(promise)
94
+ rate_limited = Time.now < @rate_limit_reset
95
+ promise.reject("#{LOG_LABEL} IP is rate limited") if rate_limited
96
+ rate_limited
97
+ end
98
+
99
+ def missing_body?(req, promise)
100
+ missing = req.body.nil?
101
+
102
+ if missing
103
+ reason = "#{LOG_LABEL} data was not sent because of missing body"
104
+ @config.logger.error(reason)
105
+ promise.reject(reason)
106
+ end
107
+
108
+ missing
109
+ end
110
+ end
111
+ end