airbrake-ruby 3.1.0 → 3.2.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +197 -43
  3. data/lib/airbrake-ruby/config.rb +43 -11
  4. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  5. data/lib/airbrake-ruby/filter_chain.rb +32 -50
  6. data/lib/airbrake-ruby/filters/git_repository_filter.rb +9 -1
  7. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  8. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  9. data/lib/airbrake-ruby/ignorable.rb +44 -0
  10. data/lib/airbrake-ruby/notice.rb +2 -22
  11. data/lib/airbrake-ruby/{notifier.rb → notice_notifier.rb} +66 -46
  12. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  13. data/lib/airbrake-ruby/stat.rb +56 -0
  14. data/lib/airbrake-ruby/tdigest.rb +393 -0
  15. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  16. data/lib/airbrake-ruby/version.rb +1 -1
  17. data/spec/airbrake_spec.rb +57 -13
  18. data/spec/async_sender_spec.rb +0 -2
  19. data/spec/backtrace_spec.rb +0 -2
  20. data/spec/code_hunk_spec.rb +0 -2
  21. data/spec/config/validator_spec.rb +0 -2
  22. data/spec/config_spec.rb +16 -4
  23. data/spec/deploy_notifier_spec.rb +41 -0
  24. data/spec/file_cache.rb +0 -2
  25. data/spec/filter_chain_spec.rb +1 -7
  26. data/spec/filters/context_filter_spec.rb +0 -2
  27. data/spec/filters/dependency_filter_spec.rb +0 -2
  28. data/spec/filters/exception_attributes_filter_spec.rb +0 -2
  29. data/spec/filters/gem_root_filter_spec.rb +0 -2
  30. data/spec/filters/git_last_checkout_filter_spec.rb +0 -2
  31. data/spec/filters/git_repository_filter.rb +0 -2
  32. data/spec/filters/git_revision_filter_spec.rb +0 -2
  33. data/spec/filters/keys_blacklist_spec.rb +0 -2
  34. data/spec/filters/keys_whitelist_spec.rb +0 -2
  35. data/spec/filters/root_directory_filter_spec.rb +0 -2
  36. data/spec/filters/sql_filter_spec.rb +219 -0
  37. data/spec/filters/system_exit_filter_spec.rb +0 -2
  38. data/spec/filters/thread_filter_spec.rb +0 -2
  39. data/spec/ignorable_spec.rb +14 -0
  40. data/spec/nested_exception_spec.rb +0 -2
  41. data/spec/{notifier_spec.rb → notice_notifier_spec.rb} +24 -114
  42. data/spec/{notifier_spec → notice_notifier_spec}/options_spec.rb +40 -39
  43. data/spec/notice_spec.rb +2 -4
  44. data/spec/performance_notifier_spec.rb +287 -0
  45. data/spec/promise_spec.rb +0 -2
  46. data/spec/response_spec.rb +0 -2
  47. data/spec/stat_spec.rb +35 -0
  48. data/spec/sync_sender_spec.rb +0 -2
  49. data/spec/tdigest_spec.rb +230 -0
  50. data/spec/time_truncate_spec.rb +13 -0
  51. data/spec/truncator_spec.rb +0 -2
  52. metadata +34 -15
  53. data/lib/airbrake-ruby/route_sender.rb +0 -175
  54. data/spec/route_sender_spec.rb +0 -130
@@ -0,0 +1,13 @@
1
+ RSpec.describe Airbrake::TimeTruncate do
2
+ describe "#utc_truncate_minutes" do
3
+ it "truncates time to the floor minute and returns an RFC3339 timestamp" do
4
+ time = Time.new(2018, 1, 1, 0, 0, 20, 0)
5
+ expect(subject.utc_truncate_minutes(time)).to eq('2018-01-01T00:00:00+00:00')
6
+ end
7
+
8
+ it "converts time with zone to UTC" do
9
+ time = Time.new(2018, 1, 1, 0, 0, 20, '-05:00')
10
+ expect(subject.utc_truncate_minutes(time)).to eq('2018-01-01T05:00:00+00:00')
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  RSpec.describe Airbrake::Truncator do
4
2
  def multiply_by_2_max_len(chr)
5
3
  chr * 2 * max_len
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-23 00:00:00.000000000 Z
11
+ date: 2019-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: tdigest
14
+ name: rbtree3
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.1
19
+ version: 0.5.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '='
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.1.1
26
+ version: 0.5.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -134,6 +134,7 @@ files:
134
134
  - lib/airbrake-ruby/code_hunk.rb
135
135
  - lib/airbrake-ruby/config.rb
136
136
  - lib/airbrake-ruby/config/validator.rb
137
+ - lib/airbrake-ruby/deploy_notifier.rb
137
138
  - lib/airbrake-ruby/file_cache.rb
138
139
  - lib/airbrake-ruby/filter_chain.rb
139
140
  - lib/airbrake-ruby/filters/context_filter.rb
@@ -147,15 +148,21 @@ files:
147
148
  - lib/airbrake-ruby/filters/keys_filter.rb
148
149
  - lib/airbrake-ruby/filters/keys_whitelist.rb
149
150
  - lib/airbrake-ruby/filters/root_directory_filter.rb
151
+ - lib/airbrake-ruby/filters/sql_filter.rb
150
152
  - lib/airbrake-ruby/filters/system_exit_filter.rb
151
153
  - lib/airbrake-ruby/filters/thread_filter.rb
154
+ - lib/airbrake-ruby/hash_keyable.rb
155
+ - lib/airbrake-ruby/ignorable.rb
152
156
  - lib/airbrake-ruby/nested_exception.rb
153
157
  - lib/airbrake-ruby/notice.rb
154
- - lib/airbrake-ruby/notifier.rb
158
+ - lib/airbrake-ruby/notice_notifier.rb
159
+ - lib/airbrake-ruby/performance_notifier.rb
155
160
  - lib/airbrake-ruby/promise.rb
156
161
  - lib/airbrake-ruby/response.rb
157
- - lib/airbrake-ruby/route_sender.rb
162
+ - lib/airbrake-ruby/stat.rb
158
163
  - lib/airbrake-ruby/sync_sender.rb
164
+ - lib/airbrake-ruby/tdigest.rb
165
+ - lib/airbrake-ruby/time_truncate.rb
159
166
  - lib/airbrake-ruby/truncator.rb
160
167
  - lib/airbrake-ruby/version.rb
161
168
  - spec/airbrake_spec.rb
@@ -164,6 +171,7 @@ files:
164
171
  - spec/code_hunk_spec.rb
165
172
  - spec/config/validator_spec.rb
166
173
  - spec/config_spec.rb
174
+ - spec/deploy_notifier_spec.rb
167
175
  - spec/file_cache.rb
168
176
  - spec/filter_chain_spec.rb
169
177
  - spec/filters/context_filter_spec.rb
@@ -176,6 +184,7 @@ files:
176
184
  - spec/filters/keys_blacklist_spec.rb
177
185
  - spec/filters/keys_whitelist_spec.rb
178
186
  - spec/filters/root_directory_filter_spec.rb
187
+ - spec/filters/sql_filter_spec.rb
179
188
  - spec/filters/system_exit_filter_spec.rb
180
189
  - spec/filters/thread_filter_spec.rb
181
190
  - spec/fixtures/notroot.txt
@@ -185,15 +194,19 @@ files:
185
194
  - spec/fixtures/project_root/short_file.rb
186
195
  - spec/fixtures/project_root/vendor/bundle/ignored_file.rb
187
196
  - spec/helpers.rb
197
+ - spec/ignorable_spec.rb
188
198
  - spec/nested_exception_spec.rb
199
+ - spec/notice_notifier_spec.rb
200
+ - spec/notice_notifier_spec/options_spec.rb
189
201
  - spec/notice_spec.rb
190
- - spec/notifier_spec.rb
191
- - spec/notifier_spec/options_spec.rb
202
+ - spec/performance_notifier_spec.rb
192
203
  - spec/promise_spec.rb
193
204
  - spec/response_spec.rb
194
- - spec/route_sender_spec.rb
195
205
  - spec/spec_helper.rb
206
+ - spec/stat_spec.rb
196
207
  - spec/sync_sender_spec.rb
208
+ - spec/tdigest_spec.rb
209
+ - spec/time_truncate_spec.rb
197
210
  - spec/truncator_spec.rb
198
211
  homepage: https://airbrake.io
199
212
  licenses:
@@ -220,11 +233,11 @@ signing_key:
220
233
  specification_version: 4
221
234
  summary: Ruby notifier for https://airbrake.io
222
235
  test_files:
223
- - spec/route_sender_spec.rb
224
236
  - spec/truncator_spec.rb
225
237
  - spec/helpers.rb
226
238
  - spec/filters/exception_attributes_filter_spec.rb
227
239
  - spec/filters/root_directory_filter_spec.rb
240
+ - spec/filters/sql_filter_spec.rb
228
241
  - spec/filters/keys_whitelist_spec.rb
229
242
  - spec/filters/system_exit_filter_spec.rb
230
243
  - spec/filters/thread_filter_spec.rb
@@ -238,13 +251,19 @@ test_files:
238
251
  - spec/spec_helper.rb
239
252
  - spec/notice_spec.rb
240
253
  - spec/config_spec.rb
254
+ - spec/tdigest_spec.rb
241
255
  - spec/async_sender_spec.rb
256
+ - spec/stat_spec.rb
242
257
  - spec/backtrace_spec.rb
258
+ - spec/notice_notifier_spec.rb
259
+ - spec/time_truncate_spec.rb
243
260
  - spec/promise_spec.rb
244
261
  - spec/config/validator_spec.rb
245
262
  - spec/sync_sender_spec.rb
263
+ - spec/ignorable_spec.rb
264
+ - spec/deploy_notifier_spec.rb
265
+ - spec/performance_notifier_spec.rb
246
266
  - spec/airbrake_spec.rb
247
- - spec/notifier_spec.rb
248
267
  - spec/nested_exception_spec.rb
249
268
  - spec/filter_chain_spec.rb
250
269
  - spec/response_spec.rb
@@ -256,4 +275,4 @@ test_files:
256
275
  - spec/fixtures/project_root/code.rb
257
276
  - spec/fixtures/project_root/short_file.rb
258
277
  - spec/fixtures/project_root/vendor/bundle/ignored_file.rb
259
- - spec/notifier_spec/options_spec.rb
278
+ - spec/notice_notifier_spec/options_spec.rb
@@ -1,175 +0,0 @@
1
- require 'tdigest'
2
- require 'base64'
3
-
4
- module Airbrake
5
- # RouteSender aggregates information about requests and periodically sends
6
- # collected data to Airbrake.
7
- # @since v3.0.0
8
- class RouteSender
9
- # Monkey-patch https://github.com/castle/tdigest to pack with Big Endian
10
- # (instead of Little Endian) since our backend wants it.
11
- #
12
- # @see https://github.com/castle/tdigest/blob/master/lib/tdigest/tdigest.rb
13
- # @since v3.0.0
14
- # @api private
15
- module TDigestBigEndianness
16
- refine TDigest::TDigest do
17
- # rubocop:disable Metrics/AbcSize
18
- def as_small_bytes
19
- size = @centroids.size
20
- output = [self.class::SMALL_ENCODING, compression, size]
21
- x = 0
22
- # delta encoding allows saving 4-bytes floats
23
- mean_arr = @centroids.map do |_, c|
24
- val = c.mean - x
25
- x = c.mean
26
- val
27
- end
28
- output += mean_arr
29
- # Variable length encoding of numbers
30
- c_arr = @centroids.each_with_object([]) do |(_, c), arr|
31
- k = 0
32
- n = c.n
33
- while n < 0 || n > 0x7f
34
- b = 0x80 | (0x7f & n)
35
- arr << b
36
- n = n >> 7
37
- k += 1
38
- raise 'Unreasonable large number' if k > 6
39
- end
40
- arr << n
41
- end
42
- output += c_arr
43
- output.pack("NGNg#{size}C#{size}")
44
- end
45
- # rubocop:enable Metrics/AbcSize
46
- end
47
- end
48
-
49
- using TDigestBigEndianness
50
-
51
- # The key that represents a route.
52
- RouteKey = Struct.new(:method, :route, :statusCode, :time)
53
-
54
- # RouteStat holds data that describes a route's performance.
55
- RouteStat = Struct.new(:count, :sum, :sumsq, :tdigest) do
56
- # @param [Integer] count The number of requests
57
- # @param [Float] sum The sum of request duration in milliseconds
58
- # @param [Float] sumsq The squared sum of request duration in milliseconds
59
- # @param [TDigest::TDigest] tdigest By default, the compression is 20
60
- def initialize(
61
- count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest::TDigest.new(0.05)
62
- )
63
- super(count, sum, sumsq, tdigest)
64
- end
65
-
66
- # @return [Hash{String=>Object}] the route stat as a hash with compressed
67
- # and serialized as binary base64 tdigest
68
- def to_h
69
- tdigest.compress!
70
- {
71
- 'count' => count,
72
- 'sum' => sum,
73
- 'sumsq' => sumsq,
74
- 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes)
75
- }
76
- end
77
- end
78
-
79
- # @param [Airbrake::Config] config
80
- def initialize(config)
81
- @config = config
82
- @flush_period = config.route_stats_flush_period
83
- @sender = SyncSender.new(config, :put)
84
- @routes = {}
85
- @thread = nil
86
- @mutex = Mutex.new
87
- end
88
-
89
- # @macro see_public_api_method
90
- # @param [Airbrake::Promise] promise
91
- def notify_request(request_info, promise = Airbrake::Promise.new)
92
- route = create_route_key(
93
- request_info[:method],
94
- request_info[:route],
95
- request_info[:status_code],
96
- utc_truncate_minutes(request_info[:start_time])
97
- )
98
-
99
- @mutex.synchronize do
100
- @routes[route] ||= RouteStat.new
101
- increment_stats(request_info, @routes[route])
102
-
103
- if @flush_period > 0
104
- schedule_flush(promise)
105
- else
106
- send(@routes, promise)
107
- end
108
- end
109
-
110
- promise
111
- end
112
-
113
- private
114
-
115
- def create_route_key(method, route, status_code, tm)
116
- # rubocop:disable Style/DateTime
117
- time = DateTime.new(
118
- tm.year, tm.month, tm.day, tm.hour, tm.min, 0, tm.zone || 0
119
- )
120
- # rubocop:enable Style/DateTime
121
- RouteKey.new(method, route, status_code, time.rfc3339)
122
- end
123
-
124
- def increment_stats(request_info, stat)
125
- stat.count += 1
126
-
127
- end_time = request_info[:end_time] || Time.new
128
- ms = (end_time - request_info[:start_time]) * 1000
129
-
130
- stat.sum += ms
131
- stat.sumsq += ms * ms
132
-
133
- stat.tdigest.push(ms)
134
- end
135
-
136
- def schedule_flush(promise)
137
- @thread ||= Thread.new do
138
- sleep(@flush_period)
139
-
140
- routes = nil
141
- @mutex.synchronize do
142
- routes = @routes
143
- @routes = {}
144
- @thread = nil
145
- end
146
-
147
- send(routes, promise)
148
- end
149
-
150
- # Setting a name is needed to test the timer.
151
- # Ruby <=2.2 doesn't support Thread#name, so we have this check.
152
- @thread.name = 'route-stat-thread' if @thread.respond_to?(:name)
153
- end
154
-
155
- def send(routes, promise)
156
- if routes.none?
157
- raise "#{self.class.name}##{__method__}: routes cannot be empty. Race?"
158
- end
159
-
160
- @config.logger.debug("#{LOG_LABEL} RouteStats#send: #{routes}")
161
-
162
- @sender.send(
163
- { routes: routes.map { |k, v| k.to_h.merge(v.to_h) } },
164
- promise,
165
- URI.join(@config.host, "api/v5/projects/#{@config.project_id}/routes-stats")
166
- )
167
- end
168
-
169
- def utc_truncate_minutes(time)
170
- time_array = time.to_a
171
- time_array[0] = 0
172
- Time.utc(*time_array)
173
- end
174
- end
175
- end
@@ -1,130 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe Airbrake::RouteSender do
4
- let(:endpoint) { 'https://api.airbrake.io/api/v5/projects/1/routes-stats' }
5
-
6
- let(:config) do
7
- Airbrake::Config.new(
8
- project_id: 1,
9
- project_key: 'banana',
10
- route_stats_flush_period: 0.1
11
- )
12
- end
13
-
14
- subject { described_class.new(config) }
15
-
16
- describe "#notify_request" do
17
- before do
18
- stub_request(:put, endpoint).to_return(status: 200, body: '')
19
- end
20
-
21
- # Let the request finish.
22
- after { sleep 0.2 }
23
-
24
- it "rounds time to the floor minute" do
25
- subject.notify_request(
26
- method: 'GET',
27
- route: '/foo',
28
- status_code: 200,
29
- start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
30
- )
31
- sleep 0.2
32
- expect(
33
- a_request(:put, endpoint).with(body: /"time":"2018-01-01T00:00:00\+00:00"/)
34
- ).to have_been_made
35
- end
36
-
37
- it "increments routes with the same key" do
38
- subject.notify_request(
39
- method: 'GET',
40
- route: '/foo',
41
- status_code: 200,
42
- start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
43
- )
44
- subject.notify_request(
45
- method: 'GET',
46
- route: '/foo',
47
- status_code: 200,
48
- start_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
49
- )
50
- sleep 0.2
51
- expect(
52
- a_request(:put, endpoint).with(body: /"count":2/)
53
- ).to have_been_made
54
- end
55
-
56
- it "groups routes by time" do
57
- subject.notify_request(
58
- method: 'GET',
59
- route: '/foo',
60
- status_code: 200,
61
- start_time: Time.new(2018, 1, 1, 0, 0, 49, 0),
62
- end_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
63
- )
64
- subject.notify_request(
65
- method: 'GET',
66
- route: '/foo',
67
- status_code: 200,
68
- start_time: Time.new(2018, 1, 1, 0, 1, 49, 0),
69
- end_time: Time.new(2018, 1, 1, 0, 1, 55, 0)
70
- )
71
- sleep 0.2
72
- expect(
73
- a_request(:put, endpoint).with(
74
- body: %r|\A
75
- {"routes":\[
76
- {"method":"GET","route":"/foo","statusCode":200,
77
- "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":1000.0,
78
- "sumsq":1000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUR6AAAB"},
79
- {"method":"GET","route":"/foo","statusCode":200,
80
- "time":"2018-01-01T00:01:00\+00:00","count":1,"sum":6000.0,
81
- "sumsq":36000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUW7gAAB"}\]}
82
- \z|x
83
- )
84
- ).to have_been_made
85
- end
86
-
87
- it "groups routes by route key" do
88
- subject.notify_request(
89
- method: 'GET',
90
- route: '/foo',
91
- status_code: 200,
92
- start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
93
- end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
94
- )
95
- subject.notify_request(
96
- method: 'POST',
97
- route: '/foo',
98
- status_code: 200,
99
- start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
100
- end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
101
- )
102
- sleep 0.2
103
- expect(
104
- a_request(:put, endpoint).with(
105
- body: %r|\A
106
- {"routes":\[
107
- {"method":"GET","route":"/foo","statusCode":200,
108
- "time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
109
- "sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"},
110
- {"method":"POST","route":"/foo","statusCode":200,
111
- "time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
112
- "sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"}\]}
113
- \z|x
114
- )
115
- ).to have_been_made
116
- end
117
-
118
- it "returns a promise" do
119
- promise = subject.notify_request(
120
- method: 'GET',
121
- route: '/foo',
122
- status_code: 200,
123
- start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
124
- )
125
- sleep 0.2
126
- expect(promise).to be_an(Airbrake::Promise)
127
- expect(promise.value).to eq('' => nil)
128
- end
129
- end
130
- end