celerbrake-ruby 0.1.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 +7 -0
- data/lib/celerbrake-ruby/async_sender.rb +57 -0
- data/lib/celerbrake-ruby/backlog.rb +123 -0
- data/lib/celerbrake-ruby/backtrace.rb +197 -0
- data/lib/celerbrake-ruby/benchmark.rb +39 -0
- data/lib/celerbrake-ruby/code_hunk.rb +51 -0
- data/lib/celerbrake-ruby/config/processor.rb +77 -0
- data/lib/celerbrake-ruby/config/validator.rb +97 -0
- data/lib/celerbrake-ruby/config.rb +291 -0
- data/lib/celerbrake-ruby/context.rb +51 -0
- data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
- data/lib/celerbrake-ruby/file_cache.rb +54 -0
- data/lib/celerbrake-ruby/filter_chain.rb +112 -0
- data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
- data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
- data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
- data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
- data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
- data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
- data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
- data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
- data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
- data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
- data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
- data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
- data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
- data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
- data/lib/celerbrake-ruby/grouppable.rb +12 -0
- data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
- data/lib/celerbrake-ruby/ignorable.rb +43 -0
- data/lib/celerbrake-ruby/inspectable.rb +39 -0
- data/lib/celerbrake-ruby/loggable.rb +34 -0
- data/lib/celerbrake-ruby/mergeable.rb +12 -0
- data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
- data/lib/celerbrake-ruby/nested_exception.rb +59 -0
- data/lib/celerbrake-ruby/notice.rb +157 -0
- data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
- data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
- data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
- data/lib/celerbrake-ruby/promise.rb +110 -0
- data/lib/celerbrake-ruby/query.rb +59 -0
- data/lib/celerbrake-ruby/queue.rb +65 -0
- data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
- data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
- data/lib/celerbrake-ruby/remote_settings.rb +128 -0
- data/lib/celerbrake-ruby/request.rb +48 -0
- data/lib/celerbrake-ruby/response.rb +125 -0
- data/lib/celerbrake-ruby/stashable.rb +15 -0
- data/lib/celerbrake-ruby/stat.rb +66 -0
- data/lib/celerbrake-ruby/sync_sender.rb +145 -0
- data/lib/celerbrake-ruby/tdigest.rb +379 -0
- data/lib/celerbrake-ruby/thread_pool.rb +139 -0
- data/lib/celerbrake-ruby/time_truncate.rb +17 -0
- data/lib/celerbrake-ruby/timed_trace.rb +56 -0
- data/lib/celerbrake-ruby/truncator.rb +121 -0
- data/lib/celerbrake-ruby/version.rb +16 -0
- data/lib/celerbrake-ruby.rb +592 -0
- metadata +251 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 05635c625ddcfffc4ee0d02816587d8ae16eb9114fbb856c32c27c3eb3f934b6
|
|
4
|
+
data.tar.gz: 592b95064454a6c385583aa5ba60baceaf27731912f77c6e9d7310c7e3afc8c5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d8f415e4ae3c11ed67984395dfe11371f3660d17e160e472e36cbad94718f6a7f76579d976bd7e63eb87924bedf9333daa4d60b5ac10f515f22e666ff2f26cb5
|
|
7
|
+
data.tar.gz: '05581f19888a99ebbd515213165caeb25787f5b373c6ea4537a284b8ba09802d32c177a971e39a7ce92a4238d32ac958cce00a3527ffe87a998d4fa3511ed32c'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Responsible for sending notices to Celerbrake asynchronously.
|
|
3
|
+
#
|
|
4
|
+
# @see SyncSender
|
|
5
|
+
# @api private
|
|
6
|
+
# @since v1.0.0
|
|
7
|
+
class AsyncSender
|
|
8
|
+
def initialize(method = :post, name = 'async-sender')
|
|
9
|
+
@config = Celerbrake::Config.instance
|
|
10
|
+
@sync_sender = SyncSender.new(method)
|
|
11
|
+
@name = name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Asynchronously sends a notice to Celerbrake.
|
|
15
|
+
#
|
|
16
|
+
# @param [Celerbrake::Notice] data Whatever needs to be sent
|
|
17
|
+
# @param [Celerbrake::Promise] promise
|
|
18
|
+
# @param [URI] endpoint Where to send +data+
|
|
19
|
+
# @return [Celerbrake::Promise]
|
|
20
|
+
def send(data, promise, endpoint = @config.error_endpoint)
|
|
21
|
+
unless thread_pool << [data, promise, endpoint]
|
|
22
|
+
return promise.reject(
|
|
23
|
+
"AsyncSender has reached its capacity of #{@config.queue_size}",
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
promise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [void]
|
|
31
|
+
def close
|
|
32
|
+
@sync_sender.close
|
|
33
|
+
thread_pool.close
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def closed?
|
|
38
|
+
thread_pool.closed?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def has_workers?
|
|
43
|
+
thread_pool.has_workers?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def thread_pool
|
|
49
|
+
@thread_pool ||= ThreadPool.new(
|
|
50
|
+
name: @name,
|
|
51
|
+
worker_size: @config.workers,
|
|
52
|
+
queue_size: @config.queue_size,
|
|
53
|
+
block: proc { |args| @sync_sender.send(*args) },
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Backlog accepts notices and APM events and synchronously sends them in the
|
|
3
|
+
# background at regular intervals. The backlog is a queue of data that failed
|
|
4
|
+
# to be sent due to some error. In a nutshell, it's a retry mechanism.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
# @since v6.2.0
|
|
8
|
+
class Backlog
|
|
9
|
+
include Loggable
|
|
10
|
+
|
|
11
|
+
# @return [Integer] how many records to keep in the backlog
|
|
12
|
+
BACKLOG_SIZE = 100
|
|
13
|
+
|
|
14
|
+
# @return [Integer] flush period in seconds
|
|
15
|
+
TWO_MINUTES = 60 * 2
|
|
16
|
+
|
|
17
|
+
def initialize(sync_sender, flush_period = TWO_MINUTES)
|
|
18
|
+
@sync_sender = sync_sender
|
|
19
|
+
@flush_period = flush_period
|
|
20
|
+
@queue = SizedQueue.new(BACKLOG_SIZE).extend(MonitorMixin)
|
|
21
|
+
@has_backlog_data = @queue.new_cond
|
|
22
|
+
@schedule_flush = nil
|
|
23
|
+
|
|
24
|
+
@seen = Set.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Appends data to the backlog. Once appended, the flush schedule will
|
|
28
|
+
# start. Chainable.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# backlog << [{ 'data' => 1 }, 'https://celerbrake.com/api']
|
|
32
|
+
#
|
|
33
|
+
# @param [Array<#to_json, String>] data An array of two elements, where the
|
|
34
|
+
# first element is the data we are sending and the second element is the
|
|
35
|
+
# URL that we are sending to
|
|
36
|
+
# @return [self]
|
|
37
|
+
def <<(data)
|
|
38
|
+
@queue.synchronize do
|
|
39
|
+
return self if @seen.include?(data)
|
|
40
|
+
|
|
41
|
+
@seen << data
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
@queue.push(data, true)
|
|
45
|
+
rescue ThreadError
|
|
46
|
+
logger.error("#{LOG_LABEL} Celerbrake::Backlog full")
|
|
47
|
+
return self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@has_backlog_data.signal
|
|
51
|
+
schedule_flush
|
|
52
|
+
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Closes all the resources that this sender has allocated.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @since v6.2.0
|
|
61
|
+
def close
|
|
62
|
+
@queue.synchronize do
|
|
63
|
+
if @schedule_flush
|
|
64
|
+
@schedule_flush.kill
|
|
65
|
+
logger.debug("#{LOG_LABEL} Celerbrake::Backlog closed")
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def schedule_flush
|
|
73
|
+
@schedule_flush ||= Thread.new do
|
|
74
|
+
loop do
|
|
75
|
+
@queue.synchronize do
|
|
76
|
+
wait
|
|
77
|
+
next if @queue.empty?
|
|
78
|
+
|
|
79
|
+
flush
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def wait
|
|
86
|
+
@has_backlog_data.wait(@flush_period) while time_elapsed < @flush_period
|
|
87
|
+
@last_flush = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def time_elapsed
|
|
91
|
+
MonotonicTime.time_in_s - last_flush
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def last_flush
|
|
95
|
+
@last_flush ||= MonotonicTime.time_in_s
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def flush
|
|
99
|
+
unless @queue.empty?
|
|
100
|
+
logger.debug(
|
|
101
|
+
"#{LOG_LABEL} Celerbrake::Backlog flushing #{@queue.size} messages",
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
failed = 0
|
|
106
|
+
|
|
107
|
+
until @queue.empty?
|
|
108
|
+
data, endpoint = @queue.pop
|
|
109
|
+
promise = Celerbrake::Promise.new
|
|
110
|
+
@sync_sender.send(data, promise, endpoint)
|
|
111
|
+
failed += 1 if promise.rejected?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if failed > 0
|
|
115
|
+
logger.debug(
|
|
116
|
+
"#{LOG_LABEL} Celerbrake::Backlog #{failed} messages were not flushed",
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@seen.clear
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Represents a cross-Ruby backtrace from exceptions (including JRuby Java
|
|
3
|
+
# exceptions). Provides information about stack frames (such as line number,
|
|
4
|
+
# file and method) in convenient for Celerbrake format.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# begin
|
|
8
|
+
# raise 'Oops!'
|
|
9
|
+
# rescue
|
|
10
|
+
# Backtrace.parse($!)
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since v1.0.0
|
|
15
|
+
module Backtrace
|
|
16
|
+
module Patterns
|
|
17
|
+
# @return [Regexp] the pattern that matches standard Ruby stack frames,
|
|
18
|
+
# such as ./spec/notice_spec.rb:43:in `block (3 levels) in <top (required)>'
|
|
19
|
+
RUBY = %r{\A
|
|
20
|
+
(?<file>.+) # Matches './spec/notice_spec.rb'
|
|
21
|
+
:
|
|
22
|
+
(?<line>\d+) # Matches '43'
|
|
23
|
+
:in\s
|
|
24
|
+
`(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
|
|
25
|
+
\z}x.freeze
|
|
26
|
+
|
|
27
|
+
# @return [Regexp] the pattern that matches JRuby Java stack frames, such
|
|
28
|
+
# as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
|
|
29
|
+
JAVA = %r{\A
|
|
30
|
+
(?<function>.+) # Matches 'org.jruby.ast.NewlineNode.interpret'
|
|
31
|
+
\(
|
|
32
|
+
(?<file>
|
|
33
|
+
(?:uri:classloader:/.+(?=:)) # Matches '/META-INF/jruby.home/protocol.rb'
|
|
34
|
+
|
|
|
35
|
+
(?:uri_3a_classloader_3a_.+(?=:)) # Matches 'uri_3a_classloader_3a_/gems/...'
|
|
36
|
+
|
|
|
37
|
+
[^:]+ # Matches 'NewlineNode.java'
|
|
38
|
+
)
|
|
39
|
+
:?
|
|
40
|
+
(?<line>\d+)? # Matches '105'
|
|
41
|
+
\)
|
|
42
|
+
\z}x.freeze
|
|
43
|
+
|
|
44
|
+
# @return [Regexp] the pattern that tries to assume what a generic stack
|
|
45
|
+
# frame might look like, when exception's backtrace is set manually.
|
|
46
|
+
GENERIC = %r{\A
|
|
47
|
+
(?:from\s)?
|
|
48
|
+
(?<file>.+) # Matches '/foo/bar/baz.ext'
|
|
49
|
+
:
|
|
50
|
+
(?<line>\d+)? # Matches '43' or nothing
|
|
51
|
+
(?:
|
|
52
|
+
in\s`(?<function>.+)' # Matches "in `func'"
|
|
53
|
+
|
|
|
54
|
+
:in\s(?<function>.+) # Matches ":in func"
|
|
55
|
+
)? # ... or nothing
|
|
56
|
+
\z}x.freeze
|
|
57
|
+
|
|
58
|
+
# @return [Regexp] the pattern that matches exceptions from PL/SQL such as
|
|
59
|
+
# ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
|
|
60
|
+
# @note This is raised by https://github.com/kubo/ruby-oci8
|
|
61
|
+
OCI = /\A
|
|
62
|
+
(?:
|
|
63
|
+
ORA-\d{5}
|
|
64
|
+
:\sat\s
|
|
65
|
+
(?:"(?<function>.+)",\s)?
|
|
66
|
+
line\s(?<line>\d+)
|
|
67
|
+
|
|
|
68
|
+
#{GENERIC}
|
|
69
|
+
)
|
|
70
|
+
\z/x.freeze
|
|
71
|
+
|
|
72
|
+
# @return [Regexp] the pattern that matches CoffeeScript backtraces
|
|
73
|
+
# usually coming from Rails & ExecJS
|
|
74
|
+
EXECJS = /\A
|
|
75
|
+
(?:
|
|
76
|
+
# Matches 'compile ((execjs):6692:19)'
|
|
77
|
+
(?<function>.+)\s\((?<file>.+):(?<line>\d+):\d+\)
|
|
78
|
+
|
|
|
79
|
+
# Matches 'bootstrap_node.js:467:3'
|
|
80
|
+
(?<file>.+):(?<line>\d+):\d+(?<function>)
|
|
81
|
+
|
|
|
82
|
+
# Matches the Ruby part of the backtrace
|
|
83
|
+
#{RUBY}
|
|
84
|
+
)
|
|
85
|
+
\z/x.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Integer] how many first frames should include code hunks
|
|
89
|
+
CODE_FRAME_LIMIT = 10
|
|
90
|
+
|
|
91
|
+
# Parses an exception's backtrace.
|
|
92
|
+
#
|
|
93
|
+
# @param [Exception] exception The exception, which contains a backtrace to
|
|
94
|
+
# parse
|
|
95
|
+
# @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
|
|
96
|
+
def self.parse(exception)
|
|
97
|
+
return [] if exception.backtrace.nil? || exception.backtrace.none?
|
|
98
|
+
|
|
99
|
+
parse_backtrace(exception)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Checks whether the given exception was generated by JRuby's VM.
|
|
103
|
+
#
|
|
104
|
+
# @param [Exception] exception
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def self.java_exception?(exception)
|
|
107
|
+
if defined?(Java::JavaLang::Throwable) &&
|
|
108
|
+
exception.is_a?(Java::JavaLang::Throwable)
|
|
109
|
+
return true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return false unless exception.respond_to?(:backtrace)
|
|
113
|
+
|
|
114
|
+
(Patterns::JAVA =~ exception.backtrace.first) != nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class << self
|
|
118
|
+
include Loggable
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def best_regexp_for(exception)
|
|
123
|
+
if java_exception?(exception)
|
|
124
|
+
Patterns::JAVA
|
|
125
|
+
elsif oci_exception?(exception)
|
|
126
|
+
Patterns::OCI
|
|
127
|
+
elsif execjs_exception?(exception)
|
|
128
|
+
Patterns::EXECJS
|
|
129
|
+
else
|
|
130
|
+
Patterns::RUBY
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def oci_exception?(exception)
|
|
135
|
+
defined?(OCIError) && exception.is_a?(OCIError)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def execjs_exception?(exception)
|
|
139
|
+
return false unless defined?(ExecJS::RuntimeError)
|
|
140
|
+
return true if exception.is_a?(ExecJS::RuntimeError)
|
|
141
|
+
return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
|
|
142
|
+
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def stack_frame(regexp, stackframe)
|
|
147
|
+
if (match = match_frame(regexp, stackframe))
|
|
148
|
+
return {
|
|
149
|
+
file: match[:file],
|
|
150
|
+
line: (Integer(match[:line]) if match[:line]),
|
|
151
|
+
function: match[:function],
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
logger.error(
|
|
156
|
+
"can't parse '#{stackframe}' (please file an issue so we can fix " \
|
|
157
|
+
"it: https://github.com/celerbrake/celerbrake-ruby/issues/new)",
|
|
158
|
+
)
|
|
159
|
+
{ file: nil, line: nil, function: stackframe }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def match_frame(regexp, stackframe)
|
|
163
|
+
match = regexp.match(stackframe)
|
|
164
|
+
return match if match
|
|
165
|
+
|
|
166
|
+
Patterns::GENERIC.match(stackframe)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_backtrace(exception)
|
|
170
|
+
regexp = best_regexp_for(exception)
|
|
171
|
+
root_directory = Celerbrake::Config.instance.root_directory.to_s
|
|
172
|
+
|
|
173
|
+
exception.backtrace.map.with_index do |stackframe, i|
|
|
174
|
+
frame = stack_frame(regexp, stackframe)
|
|
175
|
+
next(frame) if !Celerbrake::Config.instance.code_hunks || frame[:file].nil?
|
|
176
|
+
|
|
177
|
+
if !root_directory.empty?
|
|
178
|
+
populate_code(frame) if frame_in_root?(frame, root_directory)
|
|
179
|
+
elsif i < CODE_FRAME_LIMIT
|
|
180
|
+
populate_code(frame)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
frame
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def populate_code(frame)
|
|
188
|
+
code = Celerbrake::CodeHunk.new.get(frame[:file], frame[:line])
|
|
189
|
+
frame[:code] = code if code
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def frame_in_root?(frame, root_directory)
|
|
193
|
+
frame[:file].start_with?(root_directory) && frame[:file] !~ %r{vendor/bundle}
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Benchmark benchmarks Ruby code.
|
|
3
|
+
#
|
|
4
|
+
# @since v4.2.4
|
|
5
|
+
# @api public
|
|
6
|
+
class Benchmark
|
|
7
|
+
# Measures monotonic time for the given operation.
|
|
8
|
+
#
|
|
9
|
+
# @yieldreturn [void]
|
|
10
|
+
def self.measure
|
|
11
|
+
benchmark = new
|
|
12
|
+
|
|
13
|
+
yield
|
|
14
|
+
|
|
15
|
+
benchmark.stop
|
|
16
|
+
benchmark.duration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Float]
|
|
20
|
+
attr_reader :duration
|
|
21
|
+
|
|
22
|
+
# @since v4.3.0
|
|
23
|
+
def initialize
|
|
24
|
+
@start = MonotonicTime.time_in_ms
|
|
25
|
+
@duration = 0.0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Stops the benchmark and stores `duration`.
|
|
29
|
+
#
|
|
30
|
+
# @since v4.3.0
|
|
31
|
+
# @return [Boolean] true for the first invocation, false in all other cases
|
|
32
|
+
def stop
|
|
33
|
+
return false if @duration > 0.0
|
|
34
|
+
|
|
35
|
+
@duration = MonotonicTime.time_in_ms - @start
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Represents a small hunk of code consisting of a base line and a couple lines
|
|
3
|
+
# around it
|
|
4
|
+
# @api private
|
|
5
|
+
class CodeHunk
|
|
6
|
+
# @return [Integer] the maximum length of a line
|
|
7
|
+
MAX_LINE_LEN = 200
|
|
8
|
+
|
|
9
|
+
# @return [Integer] how many lines should be read around the base line
|
|
10
|
+
NLINES = 2
|
|
11
|
+
|
|
12
|
+
include Loggable
|
|
13
|
+
|
|
14
|
+
# @param [String] file The file to read
|
|
15
|
+
# @param [Integer] line The base line in the file
|
|
16
|
+
# @return [Hash{Integer=>String}, nil] lines of code around the base line
|
|
17
|
+
def get(file, line)
|
|
18
|
+
return unless File.exist?(file)
|
|
19
|
+
return unless line
|
|
20
|
+
|
|
21
|
+
lines = get_lines(file, [line - NLINES, 1].max, line + NLINES) || {}
|
|
22
|
+
return { 1 => '' } if lines.empty?
|
|
23
|
+
|
|
24
|
+
lines
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def get_from_cache(file)
|
|
30
|
+
Celerbrake::FileCache[file] ||= File.foreach(file)
|
|
31
|
+
rescue StandardError => ex
|
|
32
|
+
logger.error(
|
|
33
|
+
"#{self.class.name}: can't read code hunk for #{file}: #{ex}",
|
|
34
|
+
)
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_lines(file, start_line, end_line)
|
|
39
|
+
return unless (cached_file = get_from_cache(file))
|
|
40
|
+
|
|
41
|
+
lines = {}
|
|
42
|
+
cached_file.with_index(1) do |l, i|
|
|
43
|
+
next if i < start_line
|
|
44
|
+
break if i > end_line
|
|
45
|
+
|
|
46
|
+
lines[i] = l[0...MAX_LINE_LEN].rstrip
|
|
47
|
+
end
|
|
48
|
+
lines
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
class Config
|
|
3
|
+
# Processor is a helper class, which is responsible for setting default
|
|
4
|
+
# config values, default notifier filters and remote configuration changes.
|
|
5
|
+
#
|
|
6
|
+
# @since v5.0.0
|
|
7
|
+
# @api private
|
|
8
|
+
class Processor
|
|
9
|
+
# @param [Celerbrake::Config] config
|
|
10
|
+
# @return [Celerbrake::Config::Processor]
|
|
11
|
+
def self.process(config)
|
|
12
|
+
new(config).process
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [Celerbrake::Config] config
|
|
16
|
+
def initialize(config)
|
|
17
|
+
@config = config
|
|
18
|
+
@blocklist_keys = @config.blocklist_keys
|
|
19
|
+
@allowlist_keys = @config.allowlist_keys
|
|
20
|
+
@project_id = @config.project_id
|
|
21
|
+
@poll_callback = Celerbrake::RemoteSettings::Callback.new(config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [Celerbrake::NoticeNotifier] notifier
|
|
25
|
+
# @return [void]
|
|
26
|
+
def process_blocklist(notifier)
|
|
27
|
+
return if @blocklist_keys.none?
|
|
28
|
+
|
|
29
|
+
blocklist = Celerbrake::Filters::KeysBlocklist.new(@blocklist_keys)
|
|
30
|
+
notifier.add_filter(blocklist)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [Celerbrake::NoticeNotifier] notifier
|
|
34
|
+
# @return [void]
|
|
35
|
+
def process_allowlist(notifier)
|
|
36
|
+
return if @allowlist_keys.none?
|
|
37
|
+
|
|
38
|
+
allowlist = Celerbrake::Filters::KeysAllowlist.new(@allowlist_keys)
|
|
39
|
+
notifier.add_filter(allowlist)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Celerbrake::RemoteSettings]
|
|
43
|
+
def process_remote_configuration
|
|
44
|
+
return unless @config.remote_config
|
|
45
|
+
return unless @project_id
|
|
46
|
+
|
|
47
|
+
# Never poll remote configuration in the test environment.
|
|
48
|
+
return if @config.environment == 'test'
|
|
49
|
+
|
|
50
|
+
# If the current environment is ignored, don't try to poll remote
|
|
51
|
+
# configuration.
|
|
52
|
+
return if @config.ignore_environments.include?(@config.environment)
|
|
53
|
+
|
|
54
|
+
RemoteSettings.poll(@project_id, @config.remote_config_host) do |data|
|
|
55
|
+
@poll_callback.call(data)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param [Celerbrake::NoticeNotifier] notifier
|
|
60
|
+
# @return [void]
|
|
61
|
+
def add_filters(notifier)
|
|
62
|
+
return unless @config.root_directory
|
|
63
|
+
|
|
64
|
+
[
|
|
65
|
+
Celerbrake::Filters::RootDirectoryFilter,
|
|
66
|
+
Celerbrake::Filters::GitRevisionFilter,
|
|
67
|
+
Celerbrake::Filters::GitRepositoryFilter,
|
|
68
|
+
Celerbrake::Filters::GitLastCheckoutFilter,
|
|
69
|
+
].each do |filter|
|
|
70
|
+
next if notifier.has_filter?(filter)
|
|
71
|
+
|
|
72
|
+
notifier.add_filter(filter.new(@config.root_directory))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
class Config
|
|
3
|
+
# Validator validates values of {Celerbrake::Config} options. A valid config
|
|
4
|
+
# is a config that guarantees that data can be sent to Celerbrake given its
|
|
5
|
+
# configuration.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
# @since v1.5.0
|
|
9
|
+
class Validator
|
|
10
|
+
# @return [Array<Class>] the list of allowed types to configure the
|
|
11
|
+
# environment option
|
|
12
|
+
VALID_ENV_TYPES = [NilClass, String, Symbol].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# @param [Celerbrake::Config] config
|
|
16
|
+
# @since v4.1.0
|
|
17
|
+
def validate(config)
|
|
18
|
+
promise = Celerbrake::Promise.new
|
|
19
|
+
|
|
20
|
+
unless valid_project_id?(config)
|
|
21
|
+
return promise.reject(':project_id is required')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless valid_project_key?(config)
|
|
25
|
+
return promise.reject(':project_key is required')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless valid_environment?(config)
|
|
29
|
+
return promise.reject(
|
|
30
|
+
"the 'environment' option must be configured " \
|
|
31
|
+
"with a Symbol (or String), but '#{config.environment.class}' was " \
|
|
32
|
+
"provided: #{config.environment}",
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
promise.resolve(:ok)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether the given +config+ allows sending data to Celerbrake. It doesn't
|
|
40
|
+
# matter if it's valid or invalid.
|
|
41
|
+
#
|
|
42
|
+
# @param [Celerbrake::Config] config
|
|
43
|
+
# @since v4.1.0
|
|
44
|
+
def check_notify_ability(config)
|
|
45
|
+
promise = Celerbrake::Promise.new
|
|
46
|
+
|
|
47
|
+
unless config.error_notifications
|
|
48
|
+
return promise.reject('error notifications are disabled')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if ignored_environment?(config)
|
|
52
|
+
return promise.reject(
|
|
53
|
+
"current environment '#{config.environment}' is ignored",
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
promise.resolve(:ok)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def valid_project_id?(config)
|
|
63
|
+
return true if config.project_id.to_i > 0
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def valid_project_key?(config)
|
|
69
|
+
return false unless config.project_key.is_a?(String)
|
|
70
|
+
return false if config.project_key.empty?
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def valid_environment?(config)
|
|
76
|
+
VALID_ENV_TYPES.any? { |type| config.environment.is_a?(type) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ignored_environment?(config)
|
|
80
|
+
if config.ignore_environments.any? && config.environment.nil?
|
|
81
|
+
config.logger.warn(
|
|
82
|
+
"#{LOG_LABEL} the 'environment' option is not set, " \
|
|
83
|
+
"'ignore_environments' has no effect",
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return false if config.ignore_environments.none? || !config.environment
|
|
88
|
+
|
|
89
|
+
env = config.environment.to_s
|
|
90
|
+
config.ignore_environments.any? do |pattern|
|
|
91
|
+
pattern.is_a?(Regexp) ? env.match(pattern) : env == pattern.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|