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 +4 -4
- data/lib/airbrake-ruby.rb +2 -0
- data/lib/airbrake-ruby/async_sender.rb +21 -83
- data/lib/airbrake-ruby/config.rb +24 -3
- data/lib/airbrake-ruby/file_cache.rb +6 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +22 -1
- data/lib/airbrake-ruby/performance_breakdown.rb +2 -1
- data/lib/airbrake-ruby/performance_notifier.rb +44 -16
- data/lib/airbrake-ruby/query.rb +2 -1
- data/lib/airbrake-ruby/request.rb +2 -1
- data/lib/airbrake-ruby/thread_pool.rb +128 -0
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/async_sender_spec.rb +32 -115
- data/spec/config_spec.rb +45 -0
- data/spec/{file_cache.rb → file_cache_spec.rb} +2 -4
- data/spec/filters/sql_filter_spec.rb +47 -4
- data/spec/filters/thread_filter_spec.rb +2 -2
- data/spec/{notice_notifier_spec → notice_notifier}/options_spec.rb +0 -0
- data/spec/performance_notifier_spec.rb +64 -28
- data/spec/thread_pool_spec.rb +158 -0
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a05b705bc951aed966742d0403f8c29f9e96949b
|
4
|
+
data.tar.gz: e629610d7eace9ddca72ab165636b7e91bd7b255
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37c876fee2ce05781a18f76cddd9f9c16c70a0d49e9d473736859ecfa6af5de03f6b3d75efde455096a79f80be73afbdd0ea0db85d49aa351c6875df3e16eecf
|
7
|
+
data.tar.gz: 2bd3600c3cf8263c4cc8f8606ea7a8ee55a2347275d2122b47a6703b0c0864061dfbc5ffa29524f966c6457261c77b991d19e0df1a07bb263154a6d42b45e987
|
data/lib/airbrake-ruby.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
41
|
+
thread_pool.closed?
|
78
42
|
end
|
79
43
|
|
80
|
-
#
|
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
|
-
|
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
|
110
|
-
@
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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: @
|
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 #{@
|
77
|
+
promise.reject("AsyncSender has reached its capacity of #{@config.queue_size}")
|
140
78
|
end
|
141
79
|
end
|
142
80
|
end
|
data/lib/airbrake-ruby/config.rb
CHANGED
@@ -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
|
-
#
|
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
|
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)
|
@@ -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
|
-
|
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' =>
|
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 =
|
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 =
|
29
|
-
|
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
|
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
|
67
|
-
|
68
|
-
sleep(@flush_period)
|
73
|
+
def schedule_flush
|
74
|
+
return if @payload.empty?
|
69
75
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
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
|
|
data/lib/airbrake-ruby/query.rb
CHANGED
@@ -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' =>
|
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' =>
|
41
|
+
'time' => @start_time_utc
|
41
42
|
}.delete_if { |_key, val| val.nil? }
|
42
43
|
end
|
43
44
|
end
|