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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/lib/celerbrake-ruby/async_sender.rb +57 -0
  3. data/lib/celerbrake-ruby/backlog.rb +123 -0
  4. data/lib/celerbrake-ruby/backtrace.rb +197 -0
  5. data/lib/celerbrake-ruby/benchmark.rb +39 -0
  6. data/lib/celerbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/celerbrake-ruby/config/processor.rb +77 -0
  8. data/lib/celerbrake-ruby/config/validator.rb +97 -0
  9. data/lib/celerbrake-ruby/config.rb +291 -0
  10. data/lib/celerbrake-ruby/context.rb +51 -0
  11. data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
  12. data/lib/celerbrake-ruby/file_cache.rb +54 -0
  13. data/lib/celerbrake-ruby/filter_chain.rb +112 -0
  14. data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
  15. data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
  16. data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  17. data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
  18. data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  19. data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
  20. data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
  21. data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
  22. data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
  23. data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
  24. data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
  25. data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
  26. data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
  27. data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
  28. data/lib/celerbrake-ruby/grouppable.rb +12 -0
  29. data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
  30. data/lib/celerbrake-ruby/ignorable.rb +43 -0
  31. data/lib/celerbrake-ruby/inspectable.rb +39 -0
  32. data/lib/celerbrake-ruby/loggable.rb +34 -0
  33. data/lib/celerbrake-ruby/mergeable.rb +12 -0
  34. data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
  35. data/lib/celerbrake-ruby/nested_exception.rb +59 -0
  36. data/lib/celerbrake-ruby/notice.rb +157 -0
  37. data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
  38. data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
  39. data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
  40. data/lib/celerbrake-ruby/promise.rb +110 -0
  41. data/lib/celerbrake-ruby/query.rb +59 -0
  42. data/lib/celerbrake-ruby/queue.rb +65 -0
  43. data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
  44. data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
  45. data/lib/celerbrake-ruby/remote_settings.rb +128 -0
  46. data/lib/celerbrake-ruby/request.rb +48 -0
  47. data/lib/celerbrake-ruby/response.rb +125 -0
  48. data/lib/celerbrake-ruby/stashable.rb +15 -0
  49. data/lib/celerbrake-ruby/stat.rb +66 -0
  50. data/lib/celerbrake-ruby/sync_sender.rb +145 -0
  51. data/lib/celerbrake-ruby/tdigest.rb +379 -0
  52. data/lib/celerbrake-ruby/thread_pool.rb +139 -0
  53. data/lib/celerbrake-ruby/time_truncate.rb +17 -0
  54. data/lib/celerbrake-ruby/timed_trace.rb +56 -0
  55. data/lib/celerbrake-ruby/truncator.rb +121 -0
  56. data/lib/celerbrake-ruby/version.rb +16 -0
  57. data/lib/celerbrake-ruby.rb +592 -0
  58. 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