airbrake-ruby 4.5.1 → 4.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ffbab1be170a9aef318ba4f64637e80a8d272c4a
4
- data.tar.gz: c79758680cc22faea98b5d93877a7c46a26949e4
3
+ metadata.gz: a05b705bc951aed966742d0403f8c29f9e96949b
4
+ data.tar.gz: e629610d7eace9ddca72ab165636b7e91bd7b255
5
5
  SHA512:
6
- metadata.gz: 057622e0593896c5acbe522e1e248f37fcbafdf64f7d9cfed980a19bbfa0da07e07e46091d1feaa5a4a42890078f5e4acc99b85cfaea00d7919b3469046f171a
7
- data.tar.gz: 859ab7e7f71ce69259481901f289746ac1cb2148a2690c8134db1cab087935d2e2944d2f821c875b7aeed41cfb0226fcb73399ffe247da373d23a7e5ec3993bd
6
+ metadata.gz: 37c876fee2ce05781a18f76cddd9f9c16c70a0d49e9d473736859ecfa6af5de03f6b3d75efde455096a79f80be73afbdd0ea0db85d49aa351c6875df3e16eecf
7
+ data.tar.gz: 2bd3600c3cf8263c4cc8f8606ea7a8ee55a2347275d2122b47a6703b0c0864061dfbc5ffa29524f966c6457261c77b991d19e0df1a07bb263154a6d42b45e987
@@ -12,6 +12,7 @@ require 'airbrake-ruby/stashable'
12
12
  require 'airbrake-ruby/config'
13
13
  require 'airbrake-ruby/config/validator'
14
14
  require 'airbrake-ruby/promise'
15
+ require 'airbrake-ruby/thread_pool'
15
16
  require 'airbrake-ruby/sync_sender'
16
17
  require 'airbrake-ruby/async_sender'
17
18
  require 'airbrake-ruby/response'
@@ -252,6 +253,7 @@ module Airbrake
252
253
  # @return [void]
253
254
  def close
254
255
  notice_notifier.close
256
+ performance_notifier.close
255
257
  end
256
258
 
257
259
  # Pings the Airbrake Deploy API endpoint about the occurred deploy.
@@ -1,7 +1,5 @@
1
1
  module Airbrake
2
- # Responsible for sending notices to Airbrake asynchronously. The class
3
- # supports an unlimited number of worker threads and an unlimited queue size
4
- # (both values are configurable).
2
+ # Responsible for sending notices to Airbrake asynchronously.
5
3
  #
6
4
  # @see SyncSender
7
5
  # @api private
@@ -15,25 +13,9 @@ module Airbrake
15
13
  "and the following notice will not be delivered " \
16
14
  "Error: %<type>s - %<message>s\nBacktrace: %<backtrace>s\n".freeze
17
15
 
18
- # @return [ThreadGroup] the list of workers
19
- # @note This is exposed for eaiser unit testing
20
- # @since v4.0.0
21
- attr_reader :workers
22
-
23
- # @return [Array<[Airbrake::Notice,Airbrake::Promise]>] the list of unsent
24
- # payload
25
- # @note This is exposed for eaiser unit testing
26
- # @since v4.0.0
27
- attr_reader :unsent
28
-
29
- def initialize
16
+ def initialize(method = :post)
30
17
  @config = Airbrake::Config.instance
31
- @unsent = SizedQueue.new(Airbrake::Config.instance.queue_size)
32
- @sender = SyncSender.new
33
- @closed = false
34
- @workers = ThreadGroup.new
35
- @mutex = Mutex.new
36
- @pid = nil
18
+ @method = method
37
19
  end
38
20
 
39
21
  # Asynchronously sends a notice to Airbrake.
@@ -41,83 +23,39 @@ module Airbrake
41
23
  # @param [Airbrake::Notice] notice A notice that was generated by the
42
24
  # library
43
25
  # @return [Airbrake::Promise]
44
- def send(notice, promise)
45
- return will_not_deliver(notice, promise) if @unsent.size >= @unsent.max
26
+ def send(notice, promise, endpoint = @config.endpoint)
27
+ unless thread_pool << [notice, promise, endpoint]
28
+ return will_not_deliver(notice, promise)
29
+ end
46
30
 
47
- @unsent << [notice, promise]
48
31
  promise
49
32
  end
50
33
 
51
- # Closes the instance making it a no-op (it shut downs all worker
52
- # threads). Before closing, waits on all unsent notices to be sent.
53
- #
54
34
  # @return [void]
55
- # @raise [Airbrake::Error] when invoked more than one time
56
35
  def close
57
- threads = @mutex.synchronize do
58
- raise Airbrake::Error, 'attempted to close already closed sender' if closed?
59
-
60
- unless @unsent.empty?
61
- msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
62
- logger.debug(msg + ' (Ctrl-C to abort)')
63
- end
64
-
65
- @config.workers.times { @unsent << [:stop, Airbrake::Promise.new] }
66
- @closed = true
67
- @workers.list.dup
68
- end
69
-
70
- threads.each(&:join)
71
- logger.debug("#{LOG_LABEL} closed")
36
+ thread_pool.close
72
37
  end
73
38
 
74
- # Checks whether the sender is closed and thus usable.
75
39
  # @return [Boolean]
76
40
  def closed?
77
- @closed
41
+ thread_pool.closed?
78
42
  end
79
43
 
80
- # Checks if an active sender has any workers. A sender doesn't have any
81
- # workers only in two cases: when it was closed or when all workers
82
- # crashed. An *active* sender doesn't have any workers only when something
83
- # went wrong.
84
- #
85
- # Workers are expected to crash when you +fork+ the process the workers are
86
- # living in. In this case we detect a +fork+ and try to revive them here.
87
- #
88
- # Another possible scenario that crashes workers is when you close the
89
- # instance on +at_exit+, but some other +at_exit+ hook prevents the process
90
- # from exiting.
91
- #
92
- # @return [Boolean] true if an instance wasn't closed, but has no workers
93
- # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
44
+ # @return [Boolean]
94
45
  def has_workers?
95
- @mutex.synchronize do
96
- return false if @closed
97
-
98
- if @pid != Process.pid && @workers.list.empty?
99
- @pid = Process.pid
100
- spawn_workers
101
- end
102
-
103
- !@closed && @workers.list.any?
104
- end
46
+ thread_pool.has_workers?
105
47
  end
106
48
 
107
49
  private
108
50
 
109
- def spawn_workers
110
- @workers = ThreadGroup.new
111
- @config.workers.times { @workers.add(spawn_worker) }
112
- @workers.enclose
113
- end
114
-
115
- def spawn_worker
116
- Thread.new do
117
- while (message = @unsent.pop)
118
- break if message.first == :stop
119
- @sender.send(*message)
120
- end
51
+ def thread_pool
52
+ @thread_pool ||= begin
53
+ sender = SyncSender.new(@method)
54
+ ThreadPool.new(
55
+ worker_size: @config.workers,
56
+ queue_size: @config.queue_size,
57
+ block: proc { |args| sender.send(*args) }
58
+ )
121
59
  end
122
60
  end
123
61
 
@@ -128,7 +66,7 @@ module Airbrake
128
66
  format(
129
67
  WILL_NOT_DELIVER_MSG,
130
68
  log_label: LOG_LABEL,
131
- capacity: @unsent.max,
69
+ capacity: @config.queue_size,
132
70
  type: error[:type],
133
71
  message: error[:message],
134
72
  backtrace: error[:backtrace].map do |line|
@@ -136,7 +74,7 @@ module Airbrake
136
74
  end.join("\n")
137
75
  )
138
76
  )
139
- promise.reject("AsyncSender has reached its capacity of #{@unsent.max}")
77
+ promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
140
78
  end
141
79
  end
142
80
  end
@@ -83,18 +83,24 @@ module Airbrake
83
83
  # @since v2.5.0
84
84
  attr_accessor :code_hunks
85
85
 
86
- # @return [Boolean] true if the library should send performance stats
87
- # information to Airbrake (routes, SQL queries), false otherwise
86
+ # @return [Boolean] true if the library should send route performance stats
87
+ # to Airbrake, false otherwise
88
88
  # @api public
89
89
  # @since v3.2.0
90
90
  attr_accessor :performance_stats
91
91
 
92
92
  # @return [Integer] how many seconds to wait before sending collected route
93
93
  # stats
94
- # @api public
94
+ # @api private
95
95
  # @since v3.2.0
96
96
  attr_accessor :performance_stats_flush_period
97
97
 
98
+ # @return [Boolean] true if the library should send SQL stats to Airbrake,
99
+ # false otherwise
100
+ # @api public
101
+ # @since v4.6.0
102
+ attr_accessor :query_stats
103
+
98
104
  class << self
99
105
  # @return [Config]
100
106
  attr_writer :instance
@@ -132,6 +138,7 @@ module Airbrake
132
138
  self.versions = {}
133
139
  self.performance_stats = true
134
140
  self.performance_stats_flush_period = 15
141
+ self.query_stats = false
135
142
 
136
143
  merge(user_config)
137
144
  end
@@ -197,6 +204,20 @@ module Airbrake
197
204
  check_notify_ability
198
205
  end
199
206
 
207
+ # @return [Promise] resolved promise if neither of the performance options
208
+ # reject it, false otherwise
209
+ def check_performance_options(resource)
210
+ promise = Airbrake::Promise.new
211
+
212
+ if !performance_stats
213
+ promise.reject("The Performance Stats feature is disabled")
214
+ elsif resource.is_a?(Airbrake::Query) && !query_stats
215
+ promise.reject("The Query Stats feature is disabled")
216
+ else
217
+ promise
218
+ end
219
+ end
220
+
200
221
  private
201
222
 
202
223
  def set_option(option, value)
@@ -40,6 +40,12 @@ module Airbrake
40
40
  data.empty?
41
41
  end
42
42
 
43
+ # @since ?.?.?
44
+ # @return [void]
45
+ def self.reset
46
+ @data = {}
47
+ end
48
+
43
49
  def self.data
44
50
  @data ||= {}
45
51
  end
@@ -37,6 +37,10 @@ module Airbrake
37
37
  # rubocop:enable Metrics/LineLength
38
38
  }.freeze
39
39
 
40
+ # @return [Regexp] the regexp that is applied after the feature regexps
41
+ # were used
42
+ POST_FILTER = /(?<=[values|in ]\().+(?=\))/i
43
+
40
44
  # @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
41
45
  # to a certain dialect
42
46
  DIALECT_FEATURES = {
@@ -75,6 +79,16 @@ module Airbrake
75
79
  oracle_enhanced: %r{'|/\*|\*/}
76
80
  }.freeze
77
81
 
82
+ # @return [Array<Regexp>] the list of queries to be ignored
83
+ IGNORED_QUERIES = [
84
+ /\ACOMMIT/i,
85
+ /\ABEGIN/i,
86
+ /\ASET/i,
87
+ /\ASHOW/i,
88
+ /\AWITH/i,
89
+ /FROM pg_attribute/i
90
+ ].freeze
91
+
78
92
  def initialize(dialect)
79
93
  @dialect =
80
94
  case dialect
@@ -95,7 +109,14 @@ module Airbrake
95
109
  def call(resource)
96
110
  return unless resource.respond_to?(:query)
97
111
 
98
- q = resource.query.gsub(@regexp, FILTERED)
112
+ query = resource.query
113
+ if IGNORED_QUERIES.any? { |q| q =~ query }
114
+ resource.ignore!
115
+ return
116
+ end
117
+
118
+ q = query.gsub(@regexp, FILTERED)
119
+ q.gsub!(POST_FILTER, FILTERED) if q =~ POST_FILTER
99
120
  q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
100
121
  resource.query = q
101
122
  end
@@ -21,6 +21,7 @@ module Airbrake
21
21
  start_time:,
22
22
  end_time: Time.now
23
23
  )
24
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
24
25
  super(method, route, response_type, groups, start_time, end_time)
25
26
  end
26
27
 
@@ -37,7 +38,7 @@ module Airbrake
37
38
  'method' => method,
38
39
  'route' => route,
39
40
  'responseType' => response_type,
40
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
41
+ 'time' => @start_time_utc
41
42
  }.delete_if { |_key, val| val.nil? }
42
43
  end
43
44
  end
@@ -11,11 +11,12 @@ module Airbrake
11
11
  def initialize
12
12
  @config = Airbrake::Config.instance
13
13
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
- @sender = SyncSender.new(:put)
14
+ @sender = AsyncSender.new(:put)
15
15
  @payload = {}
16
16
  @schedule_flush = nil
17
17
  @mutex = Mutex.new
18
18
  @filter_chain = FilterChain.new
19
+ @waiting = false
19
20
  end
20
21
 
21
22
  # @param [Hash] resource
@@ -25,20 +26,18 @@ module Airbrake
25
26
  promise = @config.check_configuration
26
27
  return promise if promise.rejected?
27
28
 
28
- promise = Airbrake::Promise.new
29
- unless @config.performance_stats
30
- return promise.reject("The Performance Stats feature is disabled")
31
- end
29
+ promise = @config.check_performance_options(resource)
30
+ return promise if promise.rejected?
32
31
 
33
32
  @filter_chain.refine(resource)
34
33
  return if resource.ignored?
35
34
 
36
35
  @mutex.synchronize do
37
36
  update_payload(resource)
38
- @flush_period > 0 ? schedule_flush(promise) : send(@payload, promise)
37
+ @flush_period > 0 ? schedule_flush : send(@payload, promise)
39
38
  end
40
39
 
41
- promise
40
+ promise.resolve(:success)
42
41
  end
43
42
 
44
43
  # @see Airbrake.add_performance_filter
@@ -51,6 +50,14 @@ module Airbrake
51
50
  @filter_chain.delete_filter(filter_class)
52
51
  end
53
52
 
53
+ def close
54
+ @mutex.synchronize do
55
+ @schedule_flush.kill if @schedule_flush
56
+ @sender.close
57
+ logger.debug("#{LOG_LABEL} performance notifier closed")
58
+ end
59
+ end
60
+
54
61
  private
55
62
 
56
63
  def update_payload(resource)
@@ -63,18 +70,39 @@ module Airbrake
63
70
  end
64
71
  end
65
72
 
66
- def schedule_flush(promise)
67
- @schedule_flush ||= Thread.new do
68
- sleep(@flush_period)
73
+ def schedule_flush
74
+ return if @payload.empty?
69
75
 
70
- payload = nil
71
- @mutex.synchronize do
72
- payload = @payload
73
- @payload = {}
74
- @schedule_flush = nil
76
+ if @schedule_flush && @schedule_flush.status == 'sleep' && @waiting
77
+ begin
78
+ @schedule_flush.run
79
+ rescue ThreadError => exception
80
+ logger.error("#{LOG_LABEL}: error occurred while flushing: #{exception}")
75
81
  end
82
+ end
76
83
 
77
- send(payload, promise)
84
+ @schedule_flush ||= spawn_timer
85
+ end
86
+
87
+ def spawn_timer
88
+ Thread.new do
89
+ loop do
90
+ if @payload.none?
91
+ @waiting = true
92
+ Thread.stop
93
+ @waiting = false
94
+ end
95
+
96
+ sleep(@flush_period)
97
+
98
+ payload = nil
99
+ @mutex.synchronize do
100
+ payload = @payload
101
+ @payload = {}
102
+ end
103
+
104
+ send(payload, Airbrake::Promise.new)
105
+ end
78
106
  end
79
107
  end
80
108
 
@@ -22,6 +22,7 @@ module Airbrake
22
22
  start_time:,
23
23
  end_time: Time.now
24
24
  )
25
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
25
26
  super(method, route, query, func, file, line, start_time, end_time)
26
27
  end
27
28
 
@@ -42,7 +43,7 @@ module Airbrake
42
43
  'method' => method,
43
44
  'route' => route,
44
45
  'query' => query,
45
- 'time' => TimeTruncate.utc_truncate_minutes(start_time),
46
+ 'time' => @start_time_utc,
46
47
  'function' => func,
47
48
  'file' => file,
48
49
  'line' => line
@@ -17,6 +17,7 @@ module Airbrake
17
17
  start_time:,
18
18
  end_time: Time.now
19
19
  )
20
+ @start_time_utc = TimeTruncate.utc_truncate_minutes(start_time)
20
21
  super(method, route, status_code, start_time, end_time)
21
22
  end
22
23
 
@@ -37,7 +38,7 @@ module Airbrake
37
38
  'method' => method,
38
39
  'route' => route,
39
40
  'statusCode' => status_code,
40
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
41
+ 'time' => @start_time_utc
41
42
  }.delete_if { |_key, val| val.nil? }
42
43
  end
43
44
  end