airbrake-ruby 4.5.1 → 4.7.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: 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