airbrake-ruby 3.2.2-java

Sign up to get free protection for your applications and to get access to all the features.
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