airbrake-ruby 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 25d739899ae0250aa01c5e81ab01d76bbcfac28d
4
- data.tar.gz: fca6e05b839805cb5d4b4171145c1dfeb4b4890b
3
+ metadata.gz: da152828a1f9be5664faef3a2bce8cb09755ea4f
4
+ data.tar.gz: 3db1f8a4dc6f496c1067d7d0e295054ee6c8fce2
5
5
  SHA512:
6
- metadata.gz: 13fd52e48ce2f8405640785323f2cc330d9364496b58ce0b8fb416b28baff5584a95b97ded5441ee47aca872af43cf7251ebb7cfd6d3b1660c523ee774e6a870
7
- data.tar.gz: d96f9bb73da48029a993bda925ec727a945177300721eb521f5966bac962f6d9ae2941cd191b4bc2d5497c07da7764c5d7c740bf5bb7fc0a30b61c50b263c08f
6
+ metadata.gz: cbf759c4bfb802574e23176ae0629b46bc4c7ddf73221477aa3e5b4af6dae02b4ca7d4b7e76984486e3b5b8f4d0a1d997bbf32c46d8076054555c83851160cfa
7
+ data.tar.gz: 3055d714f7d8263efd11e9de8748c04bb84031382fb1e377eef2333c2712636b48d774186a5f0b6d092de9d1fa73d43446dbf06f5b41e4587acf94a7cb05a92a
@@ -46,6 +46,7 @@ require 'airbrake-ruby/time_truncate'
46
46
  require 'airbrake-ruby/tdigest'
47
47
  require 'airbrake-ruby/query'
48
48
  require 'airbrake-ruby/request'
49
+ require 'airbrake-ruby/performance_breakdown'
49
50
 
50
51
  # Airbrake is a thin wrapper around instances of the notifier classes (such as
51
52
  # notice, performance & deploy notifiers). It creates a way to access them via a
@@ -352,7 +353,6 @@ module Airbrake
352
353
  # )
353
354
  #
354
355
  # @param [Hash{Symbol=>Object}] query_info
355
- # @option request_info [String] :environment (optional)
356
356
  # @option request_info [String] :method The HTTP method that triggered this
357
357
  # SQL query (optional)
358
358
  # @option request_info [String] :route The route that triggered this SQL
@@ -367,6 +367,30 @@ module Airbrake
367
367
  @performance_notifier.notify(Query.new(query_info))
368
368
  end
369
369
 
370
+ # Increments performance breakdown statistics of a certain route.
371
+ #
372
+ # @example
373
+ # Airbrake.notify_request(
374
+ # method: 'POST',
375
+ # route: '/thing/:id/create',
376
+ # response_type: 'json',
377
+ # groups: { db: 24.0, view: 0.4 }, # ms
378
+ # start_time: timestamp,
379
+ # end_time: Time.now
380
+ # )
381
+ #
382
+ # @param [Hash{Symbol=>Object}] breakdown_info
383
+ # @option breakdown_info [String] :method HTTP method
384
+ # @option breakdown_info [String] :route
385
+ # @option breakdown_info [String] :response_type
386
+ # @option breakdown_info [Array<Hash{Symbol=>Float}>] :groups
387
+ # @option breakdown_info [Date] :start_time
388
+ # @return [void]
389
+ # @since v4.2.0
390
+ def notify_performance_breakdown(breakdown_info)
391
+ @performance_notifier.notify(PerformanceBreakdown.new(breakdown_info))
392
+ end
393
+
370
394
  # Runs a callback before {.notify_request} or {.notify_query} kicks in. This
371
395
  # is useful if you want to ignore specific resources or filter the data the
372
396
  # resource contains.
@@ -0,0 +1,44 @@
1
+ module Airbrake
2
+ # PerformanceBreakdown holds data that shows how much time a request spent
3
+ # doing certaing subtasks such as (DB querying, view rendering, etc). request
4
+ #
5
+ # @see Airbrake.notify_breakdown
6
+ # @api public
7
+ # @since v4.3.0
8
+ # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
9
+ PerformanceBreakdown = Struct.new(
10
+ :method, :route, :response_type, :groups, :start_time, :end_time
11
+ ) do
12
+ include HashKeyable
13
+ include Ignorable
14
+
15
+ def initialize(
16
+ method:,
17
+ route:,
18
+ response_type:,
19
+ groups:,
20
+ start_time:,
21
+ end_time: Time.now
22
+ )
23
+ super(method, route, response_type, groups, start_time, end_time)
24
+ end
25
+
26
+ def destination
27
+ 'routes-breakdowns'
28
+ end
29
+
30
+ def cargo
31
+ 'routes'
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ 'method' => method,
37
+ 'route' => route,
38
+ 'responseType' => response_type,
39
+ 'time' => TimeTruncate.utc_truncate_minutes(start_time)
40
+ }.delete_if { |_key, val| val.nil? }
41
+ end
42
+ end
43
+ # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
44
+ end
@@ -34,14 +34,8 @@ module Airbrake
34
34
  return if resource.ignored?
35
35
 
36
36
  @mutex.synchronize do
37
- @payload[resource] ||= Airbrake::Stat.new
38
- @payload[resource].increment(resource.start_time, resource.end_time)
39
-
40
- if @flush_period > 0
41
- schedule_flush(promise)
42
- else
43
- send(@payload, promise)
44
- end
37
+ update_payload(resource)
38
+ @flush_period > 0 ? schedule_flush(promise) : send(@payload, promise)
45
39
  end
46
40
 
47
41
  promise
@@ -59,6 +53,16 @@ module Airbrake
59
53
 
60
54
  private
61
55
 
56
+ def update_payload(resource)
57
+ @payload[resource] ||= { total: Airbrake::Stat.new }
58
+ @payload[resource][:total].increment(resource.start_time, resource.end_time)
59
+
60
+ resource.groups.each do |name, ms|
61
+ @payload[resource][name] ||= Airbrake::Stat.new
62
+ @payload[resource][name].increment_ms(ms)
63
+ end
64
+ end
65
+
62
66
  def schedule_flush(promise)
63
67
  @schedule_flush ||= Thread.new do
64
68
  sleep(@flush_period)
@@ -74,27 +78,50 @@ module Airbrake
74
78
  end
75
79
  end
76
80
 
77
- # rubocop:disable Metrics/AbcSize
78
81
  def send(payload, promise)
79
82
  signature = "#{self.class.name}##{__method__}"
80
83
  raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
81
84
 
82
85
  logger.debug("#{LOG_LABEL} #{signature}: #{payload}")
83
86
 
84
- payload.group_by { |k, _v| k.name }.each do |resource_name, data|
85
- data = { resource_name => data.map { |k, v| k.to_h.merge!(v.to_h) } }
86
- data['environment'] = @config.environment if @config.environment
87
-
88
- @sender.send(
89
- data,
90
- promise,
91
- URI.join(
92
- @config.host,
93
- "api/v5/projects/#{@config.project_id}/#{resource_name}-stats"
94
- )
87
+ with_grouped_payload(payload) do |resource_hash, destination|
88
+ url = URI.join(
89
+ @config.host,
90
+ "api/v5/projects/#{@config.project_id}/#{destination}"
95
91
  )
92
+ @sender.send(resource_hash, promise, url)
93
+ end
94
+
95
+ promise
96
+ end
97
+
98
+ def with_grouped_payload(raw_payload)
99
+ grouped_payload = raw_payload.group_by do |resource, _stats|
100
+ [resource.cargo, resource.destination]
101
+ end
102
+
103
+ grouped_payload.each do |(cargo, destination), resources|
104
+ payload = {}
105
+ payload[cargo] = serialize_resources(resources)
106
+ payload['environment'] = @config.environment if @config.environment
107
+
108
+ yield(payload, destination)
109
+ end
110
+ end
111
+
112
+ def serialize_resources(resources)
113
+ resources.map do |resource, stats|
114
+ resource_hash = resource.to_h.merge!(stats[:total].to_h)
115
+
116
+ if resource.groups.any?
117
+ group_stats = stats.reject { |name, _stat| name == :total }
118
+ resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
119
+ stat.to_h
120
+ end
121
+ end
122
+
123
+ resource_hash
96
124
  end
97
125
  end
98
- # rubocop:enable Metrics/AbcSize
99
126
  end
100
127
  end
@@ -24,10 +24,18 @@ module Airbrake
24
24
  super(method, route, query, func, file, line, start_time, end_time)
25
25
  end
26
26
 
27
- def name
27
+ def destination
28
+ 'queries-stats'
29
+ end
30
+
31
+ def cargo
28
32
  'queries'
29
33
  end
30
34
 
35
+ def groups
36
+ {}
37
+ end
38
+
31
39
  def to_h
32
40
  {
33
41
  'method' => method,
@@ -4,6 +4,7 @@ module Airbrake
4
4
  # @see Airbrake.notify_request
5
5
  # @api public
6
6
  # @since v3.2.0
7
+ # rubocop:disable Metrics/BlockLength
7
8
  Request = Struct.new(:method, :route, :status_code, :start_time, :end_time) do
8
9
  include HashKeyable
9
10
  include Ignorable
@@ -18,10 +19,18 @@ module Airbrake
18
19
  super(method, route, status_code, start_time, end_time)
19
20
  end
20
21
 
21
- def name
22
+ def destination
23
+ 'routes-stats'
24
+ end
25
+
26
+ def cargo
22
27
  'routes'
23
28
  end
24
29
 
30
+ def groups
31
+ {}
32
+ end
33
+
25
34
  def to_h
26
35
  {
27
36
  'method' => method,
@@ -31,4 +40,5 @@ module Airbrake
31
40
  }.delete_if { |_key, val| val.nil? }
32
41
  end
33
42
  end
43
+ # rubocop:enable Metrics/BlockLength
34
44
  end
@@ -43,10 +43,16 @@ module Airbrake
43
43
  # @return [void]
44
44
  def increment(start_time, end_time = nil)
45
45
  end_time ||= Time.new
46
+ increment_ms((end_time - start_time) * 1000)
47
+ end
46
48
 
49
+ # Increments count and updates performance with given +ms+ value.
50
+ #
51
+ # @param [Float] ms
52
+ # @return [void]
53
+ def increment_ms(ms)
47
54
  self.count += 1
48
55
 
49
- ms = (end_time - start_time) * 1000
50
56
  self.sum += ms
51
57
  self.sumsq += ms * ms
52
58
 
@@ -2,5 +2,5 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '4.1.0'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '4.2.0'.freeze
6
6
  end
@@ -185,10 +185,6 @@ RSpec.describe Airbrake::NoticeNotifier do
185
185
  describe "#notify_sync" do
186
186
  let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
187
187
 
188
- let(:user_params) do
189
- { project_id: 1, project_key: 'abc', logger: Logger.new('/dev/null') }
190
- end
191
-
192
188
  let(:body) do
193
189
  {
194
190
  'id' => '00054414-b147-6ffa-85d6-1524d83362a6',
@@ -1,8 +1,4 @@
1
1
  RSpec.describe Airbrake::NoticeNotifier do
2
- def expect_a_request_with_body(body)
3
- expect(a_request(:post, endpoint).with(body: body)).to have_been_made.once
4
- end
5
-
6
2
  let(:project_id) { 105138 }
7
3
  let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
8
4
  let(:localhost) { 'http://localhost:8080' }
@@ -1,10 +1,12 @@
1
1
  RSpec.describe Airbrake::PerformanceNotifier do
2
2
  let(:routes) { 'https://api.airbrake.io/api/v5/projects/1/routes-stats' }
3
3
  let(:queries) { 'https://api.airbrake.io/api/v5/projects/1/queries-stats' }
4
+ let(:breakdowns) { 'https://api.airbrake.io/api/v5/projects/1/routes-breakdowns' }
4
5
 
5
6
  before do
6
7
  stub_request(:put, routes).to_return(status: 200, body: '')
7
8
  stub_request(:put, queries).to_return(status: 200, body: '')
9
+ stub_request(:put, breakdowns).to_return(status: 200, body: '')
8
10
 
9
11
  Airbrake::Config.instance = Airbrake::Config.new(
10
12
  project_id: 1,
@@ -73,6 +75,47 @@ RSpec.describe Airbrake::PerformanceNotifier do
73
75
  ).to have_been_made
74
76
  end
75
77
 
78
+ it "sends full performance breakdown" do
79
+ subject.notify(
80
+ Airbrake::PerformanceBreakdown.new(
81
+ method: 'DELETE',
82
+ route: '/routes-breakdowns',
83
+ response_type: 'json',
84
+ start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
85
+ end_time: Time.new(2018, 1, 1, 0, 50, 0, 0),
86
+ groups: { db: 131, view: 421 }
87
+ )
88
+ )
89
+
90
+ expect(
91
+ a_request(:put, breakdowns).with(body: %r|
92
+ \A{"routes":\[{
93
+ "method":"DELETE",
94
+ "route":"/routes-breakdowns",
95
+ "responseType":"json",
96
+ "time":"2018-01-01T00:49:00\+00:00",
97
+ "count":1,
98
+ "sum":60000.0,
99
+ "sumsq":3600000000.0,
100
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB",
101
+ "groups":{
102
+ "db":{
103
+ "count":1,
104
+ "sum":131.0,
105
+ "sumsq":17161.0,
106
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUMDAAAB"
107
+ },
108
+ "view":{
109
+ "count":1,
110
+ "sum":421.0,
111
+ "sumsq":177241.0,
112
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUPSgAAB"
113
+ }
114
+ }
115
+ }\]}\z|x)
116
+ ).to have_been_made
117
+ end
118
+
76
119
  it "rounds time to the floor minute" do
77
120
  subject.notify(
78
121
  Airbrake::Request.new(
@@ -177,6 +220,57 @@ RSpec.describe Airbrake::PerformanceNotifier do
177
220
  ).to have_been_made
178
221
  end
179
222
 
223
+ it "groups performance breakdowns by route key" do
224
+ subject.notify(
225
+ Airbrake::PerformanceBreakdown.new(
226
+ method: 'DELETE',
227
+ route: '/routes-breakdowns',
228
+ response_type: 'json',
229
+ start_time: Time.new(2018, 1, 1, 0, 0, 20, 0),
230
+ end_time: Time.new(2018, 1, 1, 0, 0, 22, 0),
231
+ groups: { db: 131, view: 421 }
232
+ )
233
+ )
234
+ subject.notify(
235
+ Airbrake::PerformanceBreakdown.new(
236
+ method: 'DELETE',
237
+ route: '/routes-breakdowns',
238
+ response_type: 'json',
239
+ start_time: Time.new(2018, 1, 1, 0, 0, 30, 0),
240
+ end_time: Time.new(2018, 1, 1, 0, 0, 32, 0),
241
+ groups: { db: 55, view: 11 }
242
+ )
243
+ )
244
+
245
+ expect(
246
+ a_request(:put, breakdowns).with(body: %r|
247
+ \A{"routes":\[{
248
+ "method":"DELETE",
249
+ "route":"/routes-breakdowns",
250
+ "responseType":"json",
251
+ "time":"2018-01-01T00:00:00\+00:00",
252
+ "count":2,
253
+ "sum":4000.0,
254
+ "sumsq":8000000.0,
255
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAUT6AAAC",
256
+ "groups":{
257
+ "db":{
258
+ "count":2,
259
+ "sum":186.0,
260
+ "sumsq":20186.0,
261
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAkJcAABCmAAAAQE="
262
+ },
263
+ "view":{
264
+ "count":2,
265
+ "sum":432.0,
266
+ "sumsq":177362.0,
267
+ "tdigest":"AAAAAkA0AAAAAAAAAAAAAkEwAABDzQAAAQE="
268
+ }
269
+ }
270
+ }\]}\z|x)
271
+ ).to have_been_made
272
+ end
273
+
180
274
  it "returns a promise" do
181
275
  promise = subject.notify(
182
276
  Airbrake::Request.new(
@@ -339,9 +433,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
339
433
  describe "#delete_filter" do
340
434
  let(:filter) do
341
435
  Class.new do
342
- def call(resource)
343
- resource.ignore!
344
- end
436
+ def call(resource); end
345
437
  end
346
438
  end
347
439
 
@@ -1,3 +1,6 @@
1
+ require 'simplecov'
2
+ SimpleCov.start if ENV['COVERAGE']
3
+
1
4
  require 'airbrake-ruby'
2
5
 
3
6
  require 'rspec/its'
@@ -12,21 +12,19 @@ RSpec.describe Airbrake::Stat do
12
12
 
13
13
  describe "#increment" do
14
14
  let(:start_time) { Time.new(2018, 1, 1, 0, 0, 20, 0) }
15
- let(:end_time) { Time.new(2018, 1, 1, 0, 0, 21, 0) }
15
+ let(:end_time) { Time.new(2018, 1, 1, 0, 0, 22, 0) }
16
16
 
17
17
  before { subject.increment(start_time, end_time) }
18
18
 
19
- it "increments count" do
20
- expect(subject.count).to eq(1)
21
- end
19
+ its(:sum) { is_expected.to eq(2000) }
20
+ end
22
21
 
23
- it "updates sum" do
24
- expect(subject.sum).to eq(1000)
25
- end
22
+ describe "#increment_ms" do
23
+ before { subject.increment_ms(1000) }
26
24
 
27
- it "updates sumsq" do
28
- expect(subject.sumsq).to eq(1000000)
29
- end
25
+ its(:count) { is_expected.to eq(1) }
26
+ its(:sum) { is_expected.to eq(1000) }
27
+ its(:sumsq) { is_expected.to eq(1000000) }
30
28
 
31
29
  it "updates tdigest" do
32
30
  expect(subject.tdigest.size).to eq(1)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.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-02-28 00:00:00.000000000 Z
11
+ date: 2019-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree3
@@ -68,6 +68,7 @@ files:
68
68
  - lib/airbrake-ruby/nested_exception.rb
69
69
  - lib/airbrake-ruby/notice.rb
70
70
  - lib/airbrake-ruby/notice_notifier.rb
71
+ - lib/airbrake-ruby/performance_breakdown.rb
71
72
  - lib/airbrake-ruby/performance_notifier.rb
72
73
  - lib/airbrake-ruby/promise.rb
73
74
  - lib/airbrake-ruby/query.rb