airbrake-ruby 4.11.1 → 4.13.3

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
  SHA256:
3
- metadata.gz: 6b9d78ba8789ccfa27f139a5ad0fef6b544da9bde4239dfda22cc543493bdb45
4
- data.tar.gz: c32c7e64425037221491c64de8e22d548815929ec6e3a0289aad3c74f8ffedc7
3
+ metadata.gz: 63e544ab7354b30c5a8f4e3e73ba22daf9f27a0e3d4804e3bf015f38b34a6314
4
+ data.tar.gz: f2d238127b26e3fdfdc1b7848f847760993a87d15182ec7266e42d7db753609c
5
5
  SHA512:
6
- metadata.gz: 711d058e48af32390e7b92243171014122b5e454cea35afd19ca72e5fe39d04b919f0cbc95dfdf23685a3114cac14430310eb5a0c785c263d85a52ca26bab861
7
- data.tar.gz: 300176c0bab2a6ddcb4b3453069be19ef5a477021ca10f68256edabc97c9d0d15b3b4a5659f2676e53071f89229b59c90ac6d0f1016d953342af8d9fc8b27581
6
+ metadata.gz: 759ab3e188a8c20f2363e6aabdda6634f278b87cf24a67ecaad3998dde0dfe9de856ad8ca44f6a068b22afc2f8dcd7408ab66be6010ec3090cd2b52eef4434d8
7
+ data.tar.gz: 845c5c487af401362663273c85d56fb924578332a227ba534ced253d954af17ebdade9c6d5346a8e9640fcb4c8583ad068c8c43d0bc85fa7ec41298aff07c978
@@ -1,7 +1,6 @@
1
1
  require 'net/https'
2
2
  require 'logger'
3
3
  require 'json'
4
- require 'thread'
5
4
  require 'set'
6
5
  require 'socket'
7
6
  require 'time'
@@ -84,6 +83,13 @@ module Airbrake
84
83
  # special cases where we need to work around older implementations
85
84
  JRUBY = (RUBY_ENGINE == 'jruby')
86
85
 
86
+ # @return [Boolean] true if this Ruby supports safe levels and tainting,
87
+ # to guard against using deprecated or unsupported features.
88
+ HAS_SAFE_LEVEL = (
89
+ RUBY_ENGINE == 'ruby' &&
90
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7')
91
+ )
92
+
87
93
  class << self
88
94
  # @since v4.2.3
89
95
  # @api private
@@ -364,7 +370,7 @@ module Airbrake
364
370
  # @since v3.0.0
365
371
  # @see Airbrake::PerformanceNotifier#notify
366
372
  def notify_request(request_info, stash = {})
367
- request = Request.new(request_info)
373
+ request = Request.new(**request_info)
368
374
  request.stash.merge!(stash)
369
375
  performance_notifier.notify(request)
370
376
  end
@@ -374,7 +380,7 @@ module Airbrake
374
380
  # @since v4.10.0
375
381
  # @see .notify_request
376
382
  def notify_request_sync(request_info, stash = {})
377
- request = Request.new(request_info)
383
+ request = Request.new(**request_info)
378
384
  request.stash.merge!(stash)
379
385
  performance_notifier.notify_sync(request)
380
386
  end
@@ -407,7 +413,7 @@ module Airbrake
407
413
  # @since v3.2.0
408
414
  # @see Airbrake::PerformanceNotifier#notify
409
415
  def notify_query(query_info, stash = {})
410
- query = Query.new(query_info)
416
+ query = Query.new(**query_info)
411
417
  query.stash.merge!(stash)
412
418
  performance_notifier.notify(query)
413
419
  end
@@ -418,7 +424,7 @@ module Airbrake
418
424
  # @since v4.10.0
419
425
  # @see .notify_query
420
426
  def notify_query_sync(query_info, stash = {})
421
- query = Query.new(query_info)
427
+ query = Query.new(**query_info)
422
428
  query.stash.merge!(stash)
423
429
  performance_notifier.notify_sync(query)
424
430
  end
@@ -446,7 +452,7 @@ module Airbrake
446
452
  # @return [void]
447
453
  # @since v4.2.0
448
454
  def notify_performance_breakdown(breakdown_info, stash = {})
449
- performance_breakdown = PerformanceBreakdown.new(breakdown_info)
455
+ performance_breakdown = PerformanceBreakdown.new(**breakdown_info)
450
456
  performance_breakdown.stash.merge!(stash)
451
457
  performance_notifier.notify(performance_breakdown)
452
458
  end
@@ -456,7 +462,7 @@ module Airbrake
456
462
  # @since v4.10.0
457
463
  # @see .notify_performance_breakdown
458
464
  def notify_performance_breakdown_sync(breakdown_info, stash = {})
459
- performance_breakdown = PerformanceBreakdown.new(breakdown_info)
465
+ performance_breakdown = PerformanceBreakdown.new(**breakdown_info)
460
466
  performance_breakdown.stash.merge!(stash)
461
467
  performance_notifier.notify_sync(performance_breakdown)
462
468
  end
@@ -484,7 +490,7 @@ module Airbrake
484
490
  # @since v4.9.0
485
491
  # @see .notify_queue_sync
486
492
  def notify_queue(queue_info, stash = {})
487
- queue = Queue.new(queue_info)
493
+ queue = Queue.new(**queue_info)
488
494
  queue.stash.merge!(stash)
489
495
  performance_notifier.notify(queue)
490
496
  end
@@ -493,7 +499,7 @@ module Airbrake
493
499
  # @since v4.10.0
494
500
  # @see .notify_queue
495
501
  def notify_queue_sync(queue_info, stash = {})
496
- queue = Queue.new(queue_info)
502
+ queue = Queue.new(**queue_info)
497
503
  queue.stash.merge!(stash)
498
504
  performance_notifier.notify_sync(queue)
499
505
  end
@@ -575,6 +581,7 @@ module Airbrake
575
581
  notice_notifier.add_filter(whitelist)
576
582
  end
577
583
 
584
+ return if configured?
578
585
  return unless config.root_directory
579
586
 
580
587
  [
@@ -7,12 +7,6 @@ module Airbrake
7
7
  class AsyncSender
8
8
  include Loggable
9
9
 
10
- # @return [String]
11
- WILL_NOT_DELIVER_MSG =
12
- "%<log_label>s AsyncSender has reached its capacity of %<capacity>s " \
13
- "and the following notice will not be delivered " \
14
- "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
15
-
16
10
  def initialize(method = :post)
17
11
  @config = Airbrake::Config.instance
18
12
  @method = method
@@ -20,12 +14,13 @@ module Airbrake
20
14
 
21
15
  # Asynchronously sends a notice to Airbrake.
22
16
  #
23
- # @param [Airbrake::Notice] notice A notice that was generated by the
24
- # library
17
+ # @param [Hash] payload Whatever needs to be sent
25
18
  # @return [Airbrake::Promise]
26
- def send(notice, promise, endpoint = @config.endpoint)
27
- unless thread_pool << [notice, promise, endpoint]
28
- return will_not_deliver(notice, promise)
19
+ def send(payload, promise, endpoint = @config.endpoint)
20
+ unless thread_pool << [payload, promise, endpoint]
21
+ return promise.reject(
22
+ "AsyncSender has reached its capacity of #{@config.queue_size}",
23
+ )
29
24
  end
30
25
 
31
26
  promise
@@ -58,23 +53,5 @@ module Airbrake
58
53
  )
59
54
  end
60
55
  end
61
-
62
- def will_not_deliver(notice, promise)
63
- error = notice[:errors].first
64
-
65
- logger.error(
66
- format(
67
- WILL_NOT_DELIVER_MSG,
68
- log_label: LOG_LABEL,
69
- capacity: @config.queue_size,
70
- type: error[:type],
71
- message: error[:message],
72
- backtrace: error[:backtrace].map do |line|
73
- "#{line[:file]}:#{line[:line]} in `#{line[:function]}'"
74
- end.join("\n"),
75
- ),
76
- )
77
- promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
78
- end
79
56
  end
80
57
  end
@@ -101,6 +101,12 @@ module Airbrake
101
101
  # @since v4.6.0
102
102
  attr_accessor :query_stats
103
103
 
104
+ # @return [Boolean] true if the library should send job/queue/worker stats
105
+ # to Airbrake, false otherwise
106
+ # @api public
107
+ # @since v4.12.0
108
+ attr_accessor :job_stats
109
+
104
110
  class << self
105
111
  # @return [Config]
106
112
  attr_writer :instance
@@ -113,12 +119,13 @@ module Airbrake
113
119
 
114
120
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
115
121
  # config
122
+ # rubocop:disable Metrics/AbcSize
116
123
  def initialize(user_config = {})
117
124
  self.proxy = {}
118
125
  self.queue_size = 100
119
126
  self.workers = 1
120
127
  self.code_hunks = true
121
- self.logger = ::Logger.new(File::NULL)
128
+ self.logger = ::Logger.new(File::NULL).tap { |l| l.level = Logger::WARN }
122
129
  self.project_id = user_config[:project_id]
123
130
  self.project_key = user_config[:project_key]
124
131
  self.host = 'https://api.airbrake.io'
@@ -139,9 +146,11 @@ module Airbrake
139
146
  self.performance_stats = true
140
147
  self.performance_stats_flush_period = 15
141
148
  self.query_stats = true
149
+ self.job_stats = true
142
150
 
143
151
  merge(user_config)
144
152
  end
153
+ # rubocop:enable Metrics/AbcSize
145
154
 
146
155
  # The full URL to the Airbrake Notice API. Based on the +:host+ option.
147
156
  # @return [URI] the endpoint address
@@ -213,6 +222,8 @@ module Airbrake
213
222
  promise.reject("The Performance Stats feature is disabled")
214
223
  elsif resource.is_a?(Airbrake::Query) && !query_stats
215
224
  promise.reject("The Query Stats feature is disabled")
225
+ elsif resource.is_a?(Airbrake::Queue) && !job_stats
226
+ promise.reject("The Job Stats feature is disabled")
216
227
  else
217
228
  promise
218
229
  end
@@ -24,7 +24,18 @@ module Airbrake
24
24
 
25
25
  # @return [Array<Symbol>] parts of a Notice's *context* payload that can
26
26
  # be modified by blacklist/whitelist filters
27
- FILTERABLE_CONTEXT_KEYS = %i[user headers].freeze
27
+ FILTERABLE_CONTEXT_KEYS = %i[
28
+ user
29
+
30
+ # Provided by Airbrake::Rack::HttpHeadersFilter
31
+ headers
32
+ referer
33
+ httpMethod
34
+
35
+ # Provided by Airbrake::Rack::ContextFilter
36
+ userAddr
37
+ userAgent
38
+ ].freeze
28
39
 
29
40
  include Loggable
30
41
 
@@ -72,7 +72,7 @@ module Airbrake
72
72
  thread_info[:group] = th.group.list.map(&:inspect)
73
73
  thread_info[:priority] = th.priority
74
74
 
75
- thread_info[:safe_level] = th.safe_level unless Airbrake::JRUBY
75
+ thread_info[:safe_level] = th.safe_level if Airbrake::HAS_SAFE_LEVEL
76
76
  end
77
77
 
78
78
  def sanitize_value(value)
@@ -22,7 +22,7 @@ module Airbrake
22
22
 
23
23
  # @return [Logger]
24
24
  def instance
25
- @instance ||= ::Logger.new(File::NULL)
25
+ @instance ||= ::Logger.new(File::NULL).tap { |l| l.level = ::Logger::WARN }
26
26
  end
27
27
  end
28
28
 
@@ -16,6 +16,11 @@ module Airbrake
16
16
  time_in_nanoseconds / (10.0**6)
17
17
  end
18
18
 
19
+ # @return [Integer] current monotonic time in seconds
20
+ def time_in_s
21
+ time_in_nanoseconds / (10.0**9)
22
+ end
23
+
19
24
  private
20
25
 
21
26
  if defined?(Process::CLOCK_MONOTONIC)
@@ -76,7 +76,7 @@ module Airbrake
76
76
  #
77
77
  # @return [Hash{String=>String}, nil]
78
78
  # @api private
79
- def to_json
79
+ def to_json(*_args)
80
80
  loop do
81
81
  begin
82
82
  json = @payload.to_json
@@ -9,7 +9,7 @@ module Airbrake
9
9
  # @return [Array<Class>] filters to be executed first
10
10
  DEFAULT_FILTERS = [
11
11
  Airbrake::Filters::SystemExitFilter,
12
- Airbrake::Filters::GemRootFilter
12
+ Airbrake::Filters::GemRootFilter,
13
13
 
14
14
  # Optional filters (must be included by users):
15
15
  # Airbrake::Filters::ThreadFilter
@@ -5,16 +5,16 @@ module Airbrake
5
5
  # @see Airbrake.notify_breakdown
6
6
  # @api public
7
7
  # @since v4.2.0
8
- # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
9
- PerformanceBreakdown = Struct.new(
10
- :method, :route, :response_type, :groups, :start_time, :end_time, :timing,
11
- :time
12
- ) do
8
+ # rubocop:disable Metrics/ParameterLists
9
+ class PerformanceBreakdown
13
10
  include HashKeyable
14
11
  include Ignorable
15
12
  include Stashable
16
13
  include Mergeable
17
14
 
15
+ attr_accessor :method, :route, :response_type, :groups, :start_time,
16
+ :end_time, :timing, :time
17
+
18
18
  def initialize(
19
19
  method:,
20
20
  route:,
@@ -26,9 +26,14 @@ module Airbrake
26
26
  time: Time.now
27
27
  )
28
28
  @time_utc = TimeTruncate.utc_truncate_minutes(time)
29
- super(
30
- method, route, response_type, groups, start_time, end_time, timing, time
31
- )
29
+ @method = method
30
+ @route = route
31
+ @response_type = response_type
32
+ @groups = groups
33
+ @start_time = start_time
34
+ @end_time = end_time
35
+ @timing = timing
36
+ @time = time
32
37
  end
33
38
 
34
39
  def destination
@@ -48,5 +53,5 @@ module Airbrake
48
53
  }.delete_if { |_key, val| val.nil? }
49
54
  end
50
55
  end
51
- # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
56
+ # rubocop:enable Metrics/ParameterLists
52
57
  end
@@ -14,18 +14,20 @@ module Airbrake
14
14
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
15
15
  @async_sender = AsyncSender.new(:put)
16
16
  @sync_sender = SyncSender.new(:put)
17
- @payload = {}
18
17
  @schedule_flush = nil
19
- @mutex = Mutex.new
20
18
  @filter_chain = FilterChain.new
21
- @waiting = false
19
+
20
+ @payload = {}.extend(MonitorMixin)
21
+ @has_payload = @payload.new_cond
22
22
  end
23
23
 
24
24
  # @param [Hash] resource
25
25
  # @see Airbrake.notify_query
26
26
  # @see Airbrake.notify_request
27
27
  def notify(resource)
28
- send_resource(resource, sync: false)
28
+ @payload.synchronize do
29
+ send_resource(resource, sync: false)
30
+ end
29
31
  end
30
32
 
31
33
  # @param [Hash] resource
@@ -46,7 +48,7 @@ module Airbrake
46
48
  end
47
49
 
48
50
  def close
49
- @mutex.synchronize do
51
+ @payload.synchronize do
50
52
  @schedule_flush.kill if @schedule_flush
51
53
  @async_sender.close
52
54
  logger.debug("#{LOG_LABEL} performance notifier closed")
@@ -55,6 +57,46 @@ module Airbrake
55
57
 
56
58
  private
57
59
 
60
+ def schedule_flush
61
+ @schedule_flush ||= Thread.new do
62
+ loop do
63
+ @payload.synchronize do
64
+ @last_flush_time ||= MonotonicTime.time_in_s
65
+
66
+ while (MonotonicTime.time_in_s - @last_flush_time) < @flush_period
67
+ @has_payload.wait(@flush_period)
68
+ end
69
+
70
+ if @payload.none?
71
+ @last_flush_time = nil
72
+ next
73
+ end
74
+
75
+ send(@async_sender, @payload, Airbrake::Promise.new)
76
+ @payload.clear
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def send_resource(resource, sync:)
83
+ promise = check_configuration(resource)
84
+ return promise if promise.rejected?
85
+
86
+ @filter_chain.refine(resource)
87
+ if resource.ignored?
88
+ return Promise.new.reject("#{resource.class} was ignored by a filter")
89
+ end
90
+
91
+ update_payload(resource)
92
+ if sync || @flush_period == 0
93
+ send(@sync_sender, @payload, promise)
94
+ else
95
+ @has_payload.signal
96
+ schedule_flush
97
+ end
98
+ end
99
+
58
100
  def update_payload(resource)
59
101
  if (total_stat = @payload[resource])
60
102
  @payload.key(total_stat).merge(resource)
@@ -83,61 +125,6 @@ module Airbrake
83
125
  end
84
126
  end
85
127
 
86
- def schedule_flush
87
- return if @payload.empty?
88
-
89
- if @schedule_flush && @schedule_flush.status == 'sleep' && @waiting
90
- begin
91
- @schedule_flush.run
92
- rescue ThreadError => exception
93
- logger.error("#{LOG_LABEL}: error occurred while flushing: #{exception}")
94
- end
95
- end
96
-
97
- @schedule_flush ||= spawn_timer
98
- end
99
-
100
- def spawn_timer
101
- Thread.new do
102
- loop do
103
- if @payload.none?
104
- @waiting = true
105
- Thread.stop
106
- @waiting = false
107
- end
108
-
109
- sleep(@flush_period)
110
-
111
- payload = nil
112
- @mutex.synchronize do
113
- payload = @payload
114
- @payload = {}
115
- end
116
-
117
- send(@async_sender, payload, Airbrake::Promise.new)
118
- end
119
- end
120
- end
121
-
122
- def send_resource(resource, sync:)
123
- promise = check_configuration(resource)
124
- return promise if promise.rejected?
125
-
126
- @filter_chain.refine(resource)
127
- if resource.ignored?
128
- return Promise.new.reject("#{resource.class} was ignored by a filter")
129
- end
130
-
131
- @mutex.synchronize do
132
- update_payload(resource)
133
- if sync || @flush_period == 0
134
- send(@sync_sender, @payload, promise)
135
- else
136
- schedule_flush
137
- end
138
- end
139
- end
140
-
141
128
  def check_configuration(resource)
142
129
  promise = @config.check_configuration
143
130
  return promise if promise.rejected?
@@ -153,16 +140,17 @@ module Airbrake
153
140
  end
154
141
 
155
142
  def send(sender, payload, promise)
156
- signature = "#{self.class.name}##{__method__}"
157
- raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
158
-
159
- logger.debug { "#{LOG_LABEL} #{signature}: #{payload}" }
143
+ raise "payload cannot be empty. Race?" if payload.none?
160
144
 
161
145
  with_grouped_payload(payload) do |resource_hash, destination|
162
146
  url = URI.join(
163
147
  @config.host,
164
148
  "api/v5/projects/#{@config.project_id}/#{destination}",
165
149
  )
150
+
151
+ logger.debug do
152
+ "#{LOG_LABEL} #{self.class.name}##{__method__}: #{resource_hash}"
153
+ end
166
154
  sender.send(resource_hash, promise, url)
167
155
  end
168
156
 
@@ -4,17 +4,17 @@ module Airbrake
4
4
  # @see Airbrake.notify_query
5
5
  # @api public
6
6
  # @since v3.2.0
7
- # rubocop:disable Metrics/ParameterLists, Metrics/BlockLength
8
- Query = Struct.new(
9
- :method, :route, :query, :func, :file, :line, :start_time, :end_time,
10
- :timing, :time
11
- ) do
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Query
12
9
  include HashKeyable
13
10
  include Ignorable
14
11
  include Stashable
15
12
  include Mergeable
16
13
  include Grouppable
17
14
 
15
+ attr_accessor :method, :route, :query, :func, :file, :line, :start_time,
16
+ :end_time, :timing, :time
17
+
18
18
  def initialize(
19
19
  method:,
20
20
  route:,
@@ -28,10 +28,16 @@ module Airbrake
28
28
  time: Time.now
29
29
  )
30
30
  @time_utc = TimeTruncate.utc_truncate_minutes(time)
31
- super(
32
- method, route, query, func, file, line, start_time, end_time, timing,
33
- time
34
- )
31
+ @method = method
32
+ @route = route
33
+ @query = query
34
+ @func = func
35
+ @file = file
36
+ @line = line
37
+ @start_time = start_time
38
+ @end_time = end_time
39
+ @timing = timing
40
+ @time = time
35
41
  end
36
42
 
37
43
  def destination
@@ -53,6 +59,6 @@ module Airbrake
53
59
  'line' => line,
54
60
  }.delete_if { |_key, val| val.nil? }
55
61
  end
56
- # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
62
+ # rubocop:enable Metrics/ParameterLists
57
63
  end
58
64
  end
@@ -4,14 +4,15 @@ module Airbrake
4
4
  # @see Airbrake.notify_queue
5
5
  # @api public
6
6
  # @since v4.9.0
7
- # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
8
- Queue = Struct.new(
9
- :queue, :error_count, :groups, :start_time, :end_time, :timing, :time
10
- ) do
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Queue
11
9
  include HashKeyable
12
10
  include Ignorable
13
11
  include Stashable
14
12
 
13
+ attr_accessor :queue, :error_count, :groups, :start_time, :end_time,
14
+ :timing, :time
15
+
15
16
  def initialize(
16
17
  queue:,
17
18
  error_count:,
@@ -22,7 +23,13 @@ module Airbrake
22
23
  time: Time.now
23
24
  )
24
25
  @time_utc = TimeTruncate.utc_truncate_minutes(time)
25
- super(queue, error_count, groups, start_time, end_time, timing, time)
26
+ @queue = queue
27
+ @error_count = error_count
28
+ @groups = groups
29
+ @start_time = start_time
30
+ @end_time = end_time
31
+ @timing = timing
32
+ @time = time
26
33
  end
27
34
 
28
35
  def destination
@@ -51,6 +58,15 @@ module Airbrake
51
58
  def merge(other)
52
59
  self.error_count += other.error_count
53
60
  end
61
+
62
+ # Queues don't have routes, but we want to define this to make sure our
63
+ # filter API is consistent (other models define this property)
64
+ #
65
+ # @return [String] empty route
66
+ # @see https://github.com/airbrake/airbrake-ruby/pull/537
67
+ def route
68
+ ''
69
+ end
54
70
  end
55
- # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
71
+ # rubocop:enable Metrics/ParameterLists
56
72
  end
@@ -4,16 +4,17 @@ module Airbrake
4
4
  # @see Airbrake.notify_request
5
5
  # @api public
6
6
  # @since v3.2.0
7
- # rubocop:disable Metrics/BlockLength, Metrics/ParameterLists
8
- Request = Struct.new(
9
- :method, :route, :status_code, :start_time, :end_time, :timing, :time
10
- ) do
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Request
11
9
  include HashKeyable
12
10
  include Ignorable
13
11
  include Stashable
14
12
  include Mergeable
15
13
  include Grouppable
16
14
 
15
+ attr_accessor :method, :route, :status_code, :start_time, :end_time,
16
+ :timing, :time
17
+
17
18
  def initialize(
18
19
  method:,
19
20
  route:,
@@ -24,7 +25,13 @@ module Airbrake
24
25
  time: Time.now
25
26
  )
26
27
  @time_utc = TimeTruncate.utc_truncate_minutes(time)
27
- super(method, route, status_code, start_time, end_time, timing, time)
28
+ @method = method
29
+ @route = route
30
+ @status_code = status_code
31
+ @start_time = start_time
32
+ @end_time = end_time
33
+ @timing = timing
34
+ @time = time
28
35
  end
29
36
 
30
37
  def destination
@@ -44,5 +51,5 @@ module Airbrake
44
51
  }.delete_if { |_key, val| val.nil? }
45
52
  end
46
53
  end
47
- # rubocop:enable Metrics/BlockLength, Metrics/ParameterLists
54
+ # rubocop:enable Metrics/ParameterLists
48
55
  end
@@ -1,6 +1,5 @@
1
1
  require 'base64'
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  module Airbrake
5
4
  # Stat is a data structure that allows accumulating performance data (route
6
5
  # performance, SQL query performance and such). It's powered by TDigests.
@@ -14,30 +13,36 @@ module Airbrake
14
13
  # stat.to_h # Pack and serialize data so it can be transmitted.
15
14
  #
16
15
  # @since v3.2.0
17
- Stat = Struct.new(:count, :sum, :sumsq, :tdigest) do
18
- # @param [Integer] count How many times this stat was incremented
16
+ class Stat
17
+ attr_accessor :sum, :sumsq, :tdigest
18
+
19
19
  # @param [Float] sum The sum of duration in milliseconds
20
20
  # @param [Float] sumsq The squared sum of duration in milliseconds
21
21
  # @param [TDigest::TDigest] tdigest Packed durations. By default,
22
22
  # compression is 20
23
- def initialize(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
24
- super(count, sum, sumsq, tdigest)
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
25
28
  end
26
29
 
27
30
  # @return [Hash{String=>Object}] stats as a hash with compressed TDigest
28
31
  # (serialized as base64)
29
32
  def to_h
30
- tdigest.compress!
31
- {
32
- 'count' => count,
33
- 'sum' => sum,
34
- 'sumsq' => sumsq,
35
- 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes),
36
- }
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
37
42
  end
38
43
 
39
- # Increments count and updates performance with the difference of +end_time+
40
- # and +start_time+.
44
+ # Increments tdigest timings and updates tdigest with the difference between
45
+ # +end_time+ and +start_time+.
41
46
  #
42
47
  # @param [Date] start_time
43
48
  # @param [Date] end_time
@@ -47,17 +52,17 @@ module Airbrake
47
52
  increment_ms((end_time - start_time) * 1000)
48
53
  end
49
54
 
50
- # Increments count and updates performance with given +ms+ value.
55
+ # Increments tdigest timings and updates tdigest with given +ms+ value.
51
56
  #
52
57
  # @param [Float] ms
53
58
  # @return [void]
54
59
  def increment_ms(ms)
55
- self.count += 1
56
-
57
- self.sum += ms
58
- self.sumsq += ms * ms
60
+ @mutex.synchronize do
61
+ self.sum += ms
62
+ self.sumsq += ms * ms
59
63
 
60
- tdigest.push(ms)
64
+ tdigest.push(ms)
65
+ end
61
66
  end
62
67
 
63
68
  # We define custom inspect so that we weed out uninformative TDigest, which
@@ -65,9 +70,8 @@ module Airbrake
65
70
  #
66
71
  # @return [String]
67
72
  def inspect
68
- "#<struct Airbrake::Stat count=#{count}, sum=#{sum}, sumsq=#{sumsq}>"
73
+ "#<struct Airbrake::Stat count=#{tdigest.size}, sum=#{sum}, sumsq=#{sumsq}>"
69
74
  end
70
- alias_method :pretty_print, :inspect
75
+ alias pretty_print inspect
71
76
  end
72
77
  end
73
- # rubocop:enable Metrics/BlockLength
@@ -37,14 +37,15 @@ module Airbrake
37
37
  end
38
38
 
39
39
  attr_accessor :centroids
40
+ attr_reader :size
41
+
40
42
  def initialize(delta = 0.01, k = 25, cx = 1.1)
41
43
  @delta = delta
42
44
  @k = k
43
45
  @cx = cx
44
46
  @centroids = RBTree.new
45
- @nreset = 0
46
- @n = 0
47
- reset!
47
+ @size = 0
48
+ @last_cumulate = 0
48
49
  end
49
50
 
50
51
  def +(other)
@@ -59,8 +60,8 @@ module Airbrake
59
60
  # compression as defined by Java implementation
60
61
  size = @centroids.size
61
62
  output = [VERBOSE_ENCODING, compression, size]
62
- output += @centroids.map { |_, c| c.mean }
63
- output += @centroids.map { |_, c| c.n }
63
+ output += @centroids.each_value.map(&:mean)
64
+ output += @centroids.each_value.map(&:n)
64
65
  output.pack("NGNG#{size}N#{size}")
65
66
  end
66
67
 
@@ -70,14 +71,14 @@ module Airbrake
70
71
  output = [self.class::SMALL_ENCODING, compression, size]
71
72
  x = 0
72
73
  # delta encoding allows saving 4-bytes floats
73
- mean_arr = @centroids.map do |_, c|
74
+ mean_arr = @centroids.each_value.map do |c|
74
75
  val = c.mean - x
75
76
  x = c.mean
76
77
  val
77
78
  end
78
79
  output += mean_arr
79
80
  # Variable length encoding of numbers
80
- c_arr = @centroids.each_with_object([]) do |(_, c), arr|
81
+ c_arr = @centroids.each_value.each_with_object([]) do |c, arr|
81
82
  k = 0
82
83
  n = c.n
83
84
  while n < 0 || n > 0x7f
@@ -95,7 +96,7 @@ module Airbrake
95
96
  # rubocop:enable Metrics/AbcSize
96
97
 
97
98
  def as_json(_ = nil)
98
- @centroids.map { |_, c| c.as_json }
99
+ @centroids.each_value.map(&:as_json)
99
100
  end
100
101
 
101
102
  def bound_mean(x)
@@ -138,21 +139,17 @@ module Airbrake
138
139
  end
139
140
 
140
141
  def find_nearest(x)
141
- return nil if size == 0
142
-
143
- ceil = @centroids.upper_bound(x)
144
- floor = @centroids.lower_bound(x)
142
+ return if size == 0
145
143
 
146
- return floor[1] if ceil.nil?
147
- return ceil[1] if floor.nil?
144
+ upper_key, upper = @centroids.upper_bound(x)
145
+ lower_key, lower = @centroids.lower_bound(x)
146
+ return lower unless upper_key
147
+ return upper unless lower_key
148
148
 
149
- ceil_key = ceil[0]
150
- floor_key = floor[0]
151
-
152
- if (floor_key - x).abs < (ceil_key - x).abs
153
- floor[1]
149
+ if (lower_key - x).abs < (upper_key - x).abs
150
+ lower
154
151
  else
155
- ceil[1]
152
+ upper
156
153
  end
157
154
  end
158
155
 
@@ -186,7 +183,7 @@ module Airbrake
186
183
  mean_cumn += (item - lower.mean) * (upper.mean_cumn - lower.mean_cumn) \
187
184
  / (upper.mean - lower.mean)
188
185
  end
189
- mean_cumn / @n
186
+ mean_cumn / @size
190
187
  end
191
188
  end
192
189
  is_array ? x : x.first
@@ -207,7 +204,7 @@ module Airbrake
207
204
  nil
208
205
  else
209
206
  _cumulate(true)
210
- h = @n * item
207
+ h = @size * item
211
208
  lower, upper = bound_mean_cumn(h)
212
209
  if lower.nil? && upper.nil?
213
210
  nil
@@ -237,17 +234,12 @@ module Airbrake
237
234
 
238
235
  def reset!
239
236
  @centroids.clear
240
- @n = 0
241
- @nreset += 1
237
+ @size = 0
242
238
  @last_cumulate = 0
243
239
  end
244
240
 
245
- def size
246
- @n || 0
247
- end
248
-
249
241
  def to_a
250
- @centroids.map { |_, c| c }
242
+ @centroids.each_value.to_a
251
243
  end
252
244
 
253
245
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -307,16 +299,16 @@ module Airbrake
307
299
 
308
300
  private
309
301
 
310
- def _add_weight(nearest, x, n)
311
- nearest.mean += n * (x - nearest.mean) / (nearest.n + n) unless x == nearest.mean
312
-
313
- _cumulate(false, true) if nearest.mean_cumn.nil?
302
+ def _add_weight(centroid, x, n)
303
+ unless x == centroid.mean
304
+ centroid.mean += n * (x - centroid.mean) / (centroid.n + n)
305
+ end
314
306
 
315
- nearest.cumn += n
316
- nearest.mean_cumn += n / 2.0
317
- nearest.n += n
307
+ _cumulate(false, true) if centroid.mean_cumn.nil?
318
308
 
319
- nil
309
+ centroid.cumn += n
310
+ centroid.mean_cumn += n / 2.0
311
+ centroid.n += n
320
312
  end
321
313
 
322
314
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
@@ -325,17 +317,17 @@ module Airbrake
325
317
  factor = if @last_cumulate == 0
326
318
  Float::INFINITY
327
319
  else
328
- (@n.to_f / @last_cumulate)
320
+ (@size.to_f / @last_cumulate)
329
321
  end
330
- return if @n == @last_cumulate || (!exact && @cx && @cx > factor)
322
+ return if @size == @last_cumulate || (!exact && @cx && @cx > factor)
331
323
  end
332
324
 
333
325
  cumn = 0
334
- @centroids.each do |_, c|
326
+ @centroids.each_value do |c|
335
327
  c.mean_cumn = cumn + c.n / 2.0
336
328
  cumn = c.cumn = cumn + c.n
337
329
  end
338
- @n = @last_cumulate = cumn
330
+ @size = @last_cumulate = cumn
339
331
  nil
340
332
  end
341
333
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
@@ -345,28 +337,25 @@ module Airbrake
345
337
  def _digest(x, n)
346
338
  # Use 'first' and 'last' instead of min/max because of performance reasons
347
339
  # This works because RBTree is sorted
348
- min = @centroids.first
349
- max = @centroids.last
350
-
351
- min = min.nil? ? nil : min[1]
352
- max = max.nil? ? nil : max[1]
340
+ min = min.last if (min = @centroids.first)
341
+ max = max.last if (max = @centroids.last)
353
342
  nearest = find_nearest(x)
354
343
 
355
- @n += n
344
+ @size += n
356
345
 
357
346
  if nearest && nearest.mean == x
358
347
  _add_weight(nearest, x, n)
359
348
  elsif nearest == min
360
- _new_centroid(x, n, 0)
349
+ @centroids[x] = Centroid.new(x, n, 0)
361
350
  elsif nearest == max
362
- _new_centroid(x, n, @n)
351
+ @centroids[x] = Centroid.new(x, n, @size)
363
352
  else
364
- p = nearest.mean_cumn.to_f / @n
365
- max_n = (4 * @n * @delta * p * (1 - p)).floor
353
+ p = nearest.mean_cumn.to_f / @size
354
+ max_n = (4 * @size * @delta * p * (1 - p)).floor
366
355
  if max_n - nearest.n >= n
367
356
  _add_weight(nearest, x, n)
368
357
  else
369
- _new_centroid(x, n, nearest.cumn)
358
+ @centroids[x] = Centroid.new(x, n, nearest.cumn)
370
359
  end
371
360
  end
372
361
 
@@ -382,12 +371,6 @@ module Airbrake
382
371
  end
383
372
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity,
384
373
  # rubocop:enable Metrics/AbcSize
385
-
386
- def _new_centroid(x, n, cumn)
387
- c = Centroid.new(x, n, cumn)
388
- @centroids[x] = c
389
- c
390
- end
391
374
  end
392
375
  # rubocop:enable Metrics/ClassLength
393
376
  end
@@ -45,7 +45,15 @@ module Airbrake
45
45
  # @return [Boolean] true if the message was successfully sent to the pool,
46
46
  # false if the queue is full
47
47
  def <<(message)
48
- return false if backlog >= @queue_size
48
+ if backlog >= @queue_size
49
+ logger.error(
50
+ "#{LOG_LABEL} ThreadPool has reached its capacity of " \
51
+ "#{@queue_size} and the following message will not be " \
52
+ "processed: #{message.inspect}",
53
+ )
54
+ return false
55
+ end
56
+
49
57
  @queue << message
50
58
  true
51
59
  end
@@ -108,8 +108,8 @@ module Airbrake
108
108
  return str if utf8_string && str.valid_encoding?
109
109
 
110
110
  temp_str = str.dup
111
- temp_str.encode!(TEMP_ENCODING, ENCODING_OPTIONS) if utf8_string
112
- temp_str.encode!('utf-8', ENCODING_OPTIONS)
111
+ temp_str.encode!(TEMP_ENCODING, **ENCODING_OPTIONS) if utf8_string
112
+ temp_str.encode!('utf-8', **ENCODING_OPTIONS)
113
113
  end
114
114
  end
115
115
  end
@@ -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.11.1'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '4.13.3'.freeze
6
6
  end
@@ -75,6 +75,16 @@ RSpec.describe Airbrake do
75
75
  described_class.configure {}
76
76
  expect(described_class.deploy_notifier).to eql(deploy_notifier)
77
77
  end
78
+
79
+ it "doesn't append the same notice notifier filters over and over" do
80
+ described_class.configure do |c|
81
+ c.project_id = 1
82
+ c.project_key = '2'
83
+ end
84
+
85
+ expect(described_class.notice_notifier).not_to receive(:add_filter)
86
+ 10.times { described_class.configure {} }
87
+ end
78
88
  end
79
89
 
80
90
  context "when blacklist_keys gets configured" do
@@ -58,15 +58,6 @@ RSpec.describe Airbrake::AsyncSender do
58
58
  'error' => "AsyncSender has reached its capacity of 1",
59
59
  )
60
60
  end
61
-
62
- it "logs discarded notice" do
63
- expect(Airbrake::Loggable.instance).to receive(:error).with(
64
- /reached its capacity/,
65
- ).at_least(:once)
66
-
67
- 15.times { subject.send(notice, Airbrake::Promise.new) }
68
- subject.close
69
- end
70
61
  end
71
62
  end
72
63
  end
@@ -22,6 +22,7 @@ RSpec.describe Airbrake::Config do
22
22
  its(:performance_stats) { is_expected.to eq(true) }
23
23
  its(:performance_stats_flush_period) { is_expected.to eq(15) }
24
24
  its(:query_stats) { is_expected.to eq(true) }
25
+ its(:job_stats) { is_expected.to eq(true) }
25
26
 
26
27
  describe "#new" do
27
28
  context "when user config is passed" do
@@ -146,5 +147,26 @@ RSpec.describe Airbrake::Config do
146
147
  )
147
148
  end
148
149
  end
150
+
151
+ context "when job stats are disabled" do
152
+ before { subject.job_stats = false }
153
+
154
+ let(:resource) do
155
+ Airbrake::Queue.new(queue: 'foo_queue', error_count: 0, timing: 1)
156
+ end
157
+
158
+ it "returns a rejected promise" do
159
+ promise = subject.check_performance_options(resource)
160
+ expect(promise.value).to eq(
161
+ 'error' => "The Job Stats feature is disabled",
162
+ )
163
+ end
164
+ end
165
+ end
166
+
167
+ describe "#logger" do
168
+ it "sets logger level to Logger::WARN" do
169
+ expect(subject.logger.level).to eq(Logger::WARN)
170
+ end
149
171
  end
150
172
  end
@@ -258,7 +258,9 @@ RSpec.describe Airbrake::Filters::ThreadFilter do
258
258
  expect(notice[:params][:thread][:priority]).to eq(0)
259
259
  end
260
260
 
261
- it "appends safe_level", skip: Airbrake::JRUBY do
261
+ it "appends safe_level", skip: (
262
+ "Not supported on this version of Ruby." unless Airbrake::HAS_SAFE_LEVEL
263
+ ) do
262
264
  subject.call(notice)
263
265
  expect(notice[:params][:thread][:safe_level]).to eq(0)
264
266
  end
@@ -0,0 +1,17 @@
1
+ RSpec.describe Airbrake::Loggable do
2
+ describe ".instance" do
3
+ it "returns a logger" do
4
+ expect(described_class.instance).to be_a(Logger)
5
+ end
6
+ end
7
+
8
+ describe "#logger" do
9
+ let(:subject) do
10
+ Class.new { include Airbrake::Loggable }.new
11
+ end
12
+
13
+ it "returns a logger that has Logger::WARN severity" do
14
+ expect(subject.logger.level).to eq(Logger::WARN)
15
+ end
16
+ end
17
+ end
@@ -9,4 +9,15 @@ RSpec.describe Airbrake::MonotonicTime do
9
9
  expect(subject.time_in_ms).to be > old_time
10
10
  end
11
11
  end
12
+
13
+ describe ".time_in_s" do
14
+ it "returns monotonic time in seconds" do
15
+ expect(subject.time_in_s).to be_a(Float)
16
+ end
17
+
18
+ it "always returns time in the future" do
19
+ old_time = subject.time_in_s
20
+ expect(subject.time_in_s).to be > old_time
21
+ end
22
+ end
12
23
  end
@@ -16,6 +16,7 @@ RSpec.describe Airbrake::PerformanceNotifier do
16
16
  performance_stats: true,
17
17
  performance_stats_flush_period: 0,
18
18
  query_stats: true,
19
+ job_stats: true,
19
20
  )
20
21
  end
21
22
 
@@ -18,4 +18,13 @@ RSpec.describe Airbrake::Queue do
18
18
  expect(queue.end_time).to eq(time + 1)
19
19
  end
20
20
  end
21
+
22
+ describe "#route" do
23
+ it "always returns an empty route" do
24
+ queue = described_class.new(
25
+ queue: 'a', error_count: 0, start_time: Time.now,
26
+ )
27
+ expect(queue.route).to be_empty
28
+ end
29
+ end
21
30
  end
@@ -22,7 +22,6 @@ RSpec.describe Airbrake::Stat do
22
22
  describe "#increment_ms" do
23
23
  before { subject.increment_ms(1000) }
24
24
 
25
- its(:count) { is_expected.to eq(1) }
26
25
  its(:sum) { is_expected.to eq(1000) }
27
26
  its(:sumsq) { is_expected.to eq(1000000) }
28
27
 
@@ -51,6 +51,15 @@ RSpec.describe Airbrake::ThreadPool do
51
51
 
52
52
  expect(tasks.size).to be_zero
53
53
  end
54
+
55
+ it "logs discarded tasks" do
56
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
57
+ /reached its capacity/,
58
+ ).exactly(15).times
59
+
60
+ 15.times { subject << 1 }
61
+ subject.close
62
+ end
54
63
  end
55
64
  end
56
65
 
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.11.1
4
+ version: 4.13.3
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-12-20 00:00:00.000000000 Z
11
+ date: 2020-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rbtree3
@@ -120,6 +120,7 @@ files:
120
120
  - spec/helpers.rb
121
121
  - spec/ignorable_spec.rb
122
122
  - spec/inspectable_spec.rb
123
+ - spec/loggable_spec.rb
123
124
  - spec/monotonic_time_spec.rb
124
125
  - spec/nested_exception_spec.rb
125
126
  - spec/notice_notifier/options_spec.rb
@@ -189,6 +190,7 @@ test_files:
189
190
  - spec/tdigest_spec.rb
190
191
  - spec/async_sender_spec.rb
191
192
  - spec/stat_spec.rb
193
+ - spec/loggable_spec.rb
192
194
  - spec/backtrace_spec.rb
193
195
  - spec/notice_notifier_spec.rb
194
196
  - spec/time_truncate_spec.rb