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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# Attaches thread & fiber local variables along with general thread
|
|
4
|
+
# information.
|
|
5
|
+
# @api private
|
|
6
|
+
class ThreadFilter
|
|
7
|
+
# @return [Integer]
|
|
8
|
+
attr_reader :weight
|
|
9
|
+
|
|
10
|
+
# @return [Array<Class>] the list of classes that can be safely converted
|
|
11
|
+
# to JSON
|
|
12
|
+
SAFE_CLASSES = [
|
|
13
|
+
NilClass,
|
|
14
|
+
TrueClass,
|
|
15
|
+
FalseClass,
|
|
16
|
+
String,
|
|
17
|
+
Symbol,
|
|
18
|
+
Regexp,
|
|
19
|
+
Numeric,
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
# Variables starting with this prefix are not attached to a notice.
|
|
23
|
+
# @see https://github.com/celerbrake/celerbrake-ruby/issues/229
|
|
24
|
+
# @return [String]
|
|
25
|
+
IGNORE_PREFIX = '_'.freeze
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@weight = 110
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @macro call_filter
|
|
32
|
+
def call(notice)
|
|
33
|
+
th = Thread.current
|
|
34
|
+
thread_info = {}
|
|
35
|
+
|
|
36
|
+
if (vars = thread_variables(th)).any?
|
|
37
|
+
thread_info[:thread_variables] = vars
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if (vars = fiber_variables(th)).any?
|
|
41
|
+
thread_info[:fiber_variables] = vars
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if (name = th.name)
|
|
45
|
+
thread_info[:name] = name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
add_thread_info(th, thread_info)
|
|
49
|
+
|
|
50
|
+
notice[:params][:thread] = thread_info
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def thread_variables(th)
|
|
56
|
+
th.thread_variables.map.with_object({}) do |var, h|
|
|
57
|
+
next if var.to_s.start_with?(IGNORE_PREFIX)
|
|
58
|
+
|
|
59
|
+
h[var] = sanitize_value(th.thread_variable_get(var))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fiber_variables(th)
|
|
64
|
+
th.keys.map.with_object({}) do |key, h|
|
|
65
|
+
next if key.to_s.start_with?(IGNORE_PREFIX)
|
|
66
|
+
|
|
67
|
+
h[key] = sanitize_value(th[key])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_thread_info(th, thread_info)
|
|
72
|
+
thread_info[:self] = th.inspect
|
|
73
|
+
thread_info[:group] = th.group.list.map(&:inspect)
|
|
74
|
+
thread_info[:priority] = th.priority
|
|
75
|
+
|
|
76
|
+
thread_info[:safe_level] = th.safe_level if Celerbrake::HAS_SAFE_LEVEL
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sanitize_value(value)
|
|
80
|
+
return value if SAFE_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
81
|
+
|
|
82
|
+
case value
|
|
83
|
+
when Array
|
|
84
|
+
value = value.map { |elem| sanitize_value(elem) }
|
|
85
|
+
when Hash
|
|
86
|
+
value.transform_values { |v| sanitize_value(v) }
|
|
87
|
+
else
|
|
88
|
+
value.to_s
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Grouppable adds the `#groups` method, so that we don't need to define it in
|
|
3
|
+
# all of performance models every time we add a model without groups.
|
|
4
|
+
#
|
|
5
|
+
# @since v4.9.0
|
|
6
|
+
# @api private
|
|
7
|
+
module Grouppable
|
|
8
|
+
def groups
|
|
9
|
+
{}
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# HashKeyable allows instances of the class to be used as a Hash key in a
|
|
3
|
+
# consistent manner.
|
|
4
|
+
#
|
|
5
|
+
# The class that includes it must implement *to_h*, which defines properties
|
|
6
|
+
# that all of the instances must share in order to produce the same {#hash}.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# class C
|
|
10
|
+
# include Celerbrake::HashKeyable
|
|
11
|
+
#
|
|
12
|
+
# def initialize(key)
|
|
13
|
+
# @key = key
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# def to_h
|
|
17
|
+
# { 'key' => @key }
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# h = {}
|
|
22
|
+
# h[C.new('key1')] = 1
|
|
23
|
+
# h[C.new('key1')] #=> 1
|
|
24
|
+
# h[C.new('key2')] #=> nil
|
|
25
|
+
module HashKeyable
|
|
26
|
+
# @param [Object] other
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def eql?(other)
|
|
29
|
+
other.is_a?(self.class) && other.hash == hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
def hash
|
|
34
|
+
to_h.hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Ignorable contains methods that allow the includee to be ignored.
|
|
3
|
+
#
|
|
4
|
+
# @example
|
|
5
|
+
# class A
|
|
6
|
+
# include Celerbrake::Ignorable
|
|
7
|
+
# end
|
|
8
|
+
#
|
|
9
|
+
# a = A.new
|
|
10
|
+
# a.ignore!
|
|
11
|
+
# a.ignored? #=> true
|
|
12
|
+
#
|
|
13
|
+
# @since v3.2.0
|
|
14
|
+
# @api private
|
|
15
|
+
module Ignorable
|
|
16
|
+
attr_accessor :ignored
|
|
17
|
+
|
|
18
|
+
# Checks whether the instance was ignored.
|
|
19
|
+
# @return [Boolean]
|
|
20
|
+
# @see #ignore!
|
|
21
|
+
def ignored?
|
|
22
|
+
!!ignored
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Ignores an instance. Ignored instances must never reach the Celerbrake
|
|
26
|
+
# dashboard.
|
|
27
|
+
# @return [void]
|
|
28
|
+
# @see #ignored?
|
|
29
|
+
def ignore!
|
|
30
|
+
self.ignored = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A method that is meant to be used as a guard.
|
|
36
|
+
# @raise [Celerbrake::Error] when instance is ignored
|
|
37
|
+
def raise_if_ignored
|
|
38
|
+
return unless ignored?
|
|
39
|
+
|
|
40
|
+
raise Celerbrake::Error, "cannot access ignored #{self.class}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Inspectable provides custom inspect methods that reduce clutter printed in
|
|
3
|
+
# REPLs for notifier objects. These custom methods display only essential
|
|
4
|
+
# information such as project id/key and filters.
|
|
5
|
+
#
|
|
6
|
+
# @since v3.2.6
|
|
7
|
+
# @api private
|
|
8
|
+
module Inspectable
|
|
9
|
+
# @return [String] inspect output template
|
|
10
|
+
INSPECT_TEMPLATE =
|
|
11
|
+
"#<%<classname>s:0x%<id>s project_id=\"%<project_id>s\" " \
|
|
12
|
+
"project_key=\"%<project_key>s\" " \
|
|
13
|
+
"host=\"%<host>s\" filter_chain=%<filter_chain>s>".freeze
|
|
14
|
+
|
|
15
|
+
# @return [String] customized inspect to lessen the amount of clutter
|
|
16
|
+
def inspect
|
|
17
|
+
format(
|
|
18
|
+
INSPECT_TEMPLATE,
|
|
19
|
+
classname: self.class.name,
|
|
20
|
+
id: (object_id << 1).to_s(16).rjust(16, '0'),
|
|
21
|
+
project_id: @config.project_id,
|
|
22
|
+
project_key: @config.project_key,
|
|
23
|
+
host: @config.host,
|
|
24
|
+
filter_chain: @filter_chain.inspect,
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [String] {#inspect} for PrettyPrint
|
|
29
|
+
def pretty_print(q)
|
|
30
|
+
q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
|
|
31
|
+
q.text(
|
|
32
|
+
"project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
|
|
33
|
+
"host=\"#{@config.host}\" filter_chain=",
|
|
34
|
+
)
|
|
35
|
+
q.pp(@filter_chain)
|
|
36
|
+
q.text('>')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Loggable is included into any class that wants to be able to log.
|
|
3
|
+
#
|
|
4
|
+
# By default, Loggable defines a null logger that doesn't do anything. You are
|
|
5
|
+
# supposed to overwrite it via the {instance} method before calling {logger}.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class A
|
|
9
|
+
# include Loggable
|
|
10
|
+
#
|
|
11
|
+
# def initialize
|
|
12
|
+
# logger.debug('Initialized A')
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @since v4.0.0
|
|
17
|
+
# @api private
|
|
18
|
+
module Loggable
|
|
19
|
+
class << self
|
|
20
|
+
# @return [Logger]
|
|
21
|
+
attr_writer :instance
|
|
22
|
+
|
|
23
|
+
# @return [Logger]
|
|
24
|
+
def instance
|
|
25
|
+
@instance ||= ::Logger.new(File::NULL).tap { |l| l.level = ::Logger::WARN }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Logger] standard Ruby logger object
|
|
30
|
+
def logger
|
|
31
|
+
Loggable.instance
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Mergeable adds the `#merge` method, so that we don't need to define it in
|
|
3
|
+
# all of performance models every time we add a model.
|
|
4
|
+
#
|
|
5
|
+
# @since v4.9.0
|
|
6
|
+
# @api private
|
|
7
|
+
module Mergeable
|
|
8
|
+
def merge(_other)
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# MonotonicTime is a helper for getting monotonic time suitable for
|
|
3
|
+
# performance measurements. It guarantees that the time is strictly linearly
|
|
4
|
+
# increasing (unlike realtime).
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# MonotonicTime.time_in_ms #=> 287138801.144576
|
|
8
|
+
#
|
|
9
|
+
# @see http://pubs.opengroup.org/onlinepubs/9699919799/functions/clock_getres.html
|
|
10
|
+
# @since v4.2.4
|
|
11
|
+
# @api private
|
|
12
|
+
module MonotonicTime
|
|
13
|
+
class << self
|
|
14
|
+
# @return [Integer] current monotonic time in milliseconds
|
|
15
|
+
def time_in_ms
|
|
16
|
+
time_in_nanoseconds / (10.0**6)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Integer] current monotonic time in seconds
|
|
20
|
+
def time_in_s
|
|
21
|
+
time_in_nanoseconds / (10.0**9)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
|
27
|
+
|
|
28
|
+
def time_in_nanoseconds
|
|
29
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
elsif RUBY_ENGINE == 'jruby'
|
|
33
|
+
|
|
34
|
+
def time_in_nanoseconds
|
|
35
|
+
java.lang.System.nanoTime
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
else
|
|
39
|
+
|
|
40
|
+
def time_in_nanoseconds
|
|
41
|
+
time = Time.now
|
|
42
|
+
(time.to_i * (10**9)) + time.nsec
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# A class that is capable of unwinding nested exceptions and representing them
|
|
3
|
+
# as JSON-like hash.
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
6
|
+
# @since v1.0.4
|
|
7
|
+
class NestedException
|
|
8
|
+
# @return [Integer] the maximum number of nested exceptions that a notice
|
|
9
|
+
# can unwrap. Exceptions that have a longer cause chain will be ignored
|
|
10
|
+
MAX_NESTED_EXCEPTIONS = 3
|
|
11
|
+
|
|
12
|
+
# On Ruby 3.1+, the error highlighting gem can produce messages that can
|
|
13
|
+
# span multiple lines. We don't display multiline error messages in the
|
|
14
|
+
# title of the notice in the Celerbrake dashboard. Therefore, we want to strip
|
|
15
|
+
# out the higlighting part so that the errors look consistent. The full
|
|
16
|
+
# message with the exception will be attached to the notice body.
|
|
17
|
+
#
|
|
18
|
+
# @return [String]
|
|
19
|
+
RUBY_31_ERROR_HIGHLIGHTING_DIVIDER = "\n\n".freeze
|
|
20
|
+
|
|
21
|
+
# @return [Hash] the options for +String#encode+
|
|
22
|
+
ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
|
|
23
|
+
|
|
24
|
+
def initialize(exception)
|
|
25
|
+
@exception = exception
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def as_json
|
|
29
|
+
unwind_exceptions.map do |exception|
|
|
30
|
+
{ type: exception.class.name,
|
|
31
|
+
message: message(exception),
|
|
32
|
+
backtrace: Backtrace.parse(exception) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def unwind_exceptions
|
|
39
|
+
exception_list = []
|
|
40
|
+
exception = @exception
|
|
41
|
+
|
|
42
|
+
while exception && exception_list.size < MAX_NESTED_EXCEPTIONS
|
|
43
|
+
exception_list << exception
|
|
44
|
+
exception = (exception.cause if exception.respond_to?(:cause))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
exception_list
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def message(exception)
|
|
51
|
+
return unless (msg = exception.message)
|
|
52
|
+
|
|
53
|
+
msg
|
|
54
|
+
.encode(Encoding::UTF_8, **ENCODING_OPTIONS)
|
|
55
|
+
.split(RUBY_31_ERROR_HIGHLIGHTING_DIVIDER)
|
|
56
|
+
.first
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Represents a chunk of information that is meant to be either sent to
|
|
3
|
+
# Celerbrake or ignored completely.
|
|
4
|
+
#
|
|
5
|
+
# @since v1.0.0
|
|
6
|
+
class Notice
|
|
7
|
+
# @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
|
|
8
|
+
# Context tab in the dashboard
|
|
9
|
+
CONTEXT = {
|
|
10
|
+
os: RUBY_PLATFORM,
|
|
11
|
+
language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
|
|
12
|
+
notifier: Celerbrake::NOTIFIER_INFO,
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# @return [Integer] the maxium size of the JSON payload in bytes
|
|
16
|
+
MAX_NOTICE_SIZE = 64000
|
|
17
|
+
|
|
18
|
+
# @return [Integer] the maximum size of hashes, arrays and strings in the
|
|
19
|
+
# notice.
|
|
20
|
+
PAYLOAD_MAX_SIZE = 10000
|
|
21
|
+
|
|
22
|
+
# @return [Array<StandardError>] the list of possible exceptions that might
|
|
23
|
+
# be raised when an object is converted to JSON
|
|
24
|
+
JSON_EXCEPTIONS = [
|
|
25
|
+
IOError,
|
|
26
|
+
NotImplementedError,
|
|
27
|
+
JSON::GeneratorError,
|
|
28
|
+
Encoding::UndefinedConversionError,
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# @return [Array<Symbol>] the list of keys that can be be overwritten with
|
|
32
|
+
# {Celerbrake::Notice#[]=}
|
|
33
|
+
WRITABLE_KEYS = %i[notifier context environment session params].freeze
|
|
34
|
+
|
|
35
|
+
# @return [Array<Symbol>] parts of a Notice's payload that can be modified
|
|
36
|
+
# by the truncator
|
|
37
|
+
TRUNCATABLE_KEYS = %i[errors environment session params].freeze
|
|
38
|
+
|
|
39
|
+
# @return [String] the name of the host machine
|
|
40
|
+
HOSTNAME = Socket.gethostname.freeze
|
|
41
|
+
|
|
42
|
+
# @return [String]
|
|
43
|
+
DEFAULT_SEVERITY = 'error'.freeze
|
|
44
|
+
|
|
45
|
+
include Ignorable
|
|
46
|
+
include Loggable
|
|
47
|
+
include Stashable
|
|
48
|
+
|
|
49
|
+
# @api private
|
|
50
|
+
def initialize(exception, params = {})
|
|
51
|
+
@config = Celerbrake::Config.instance
|
|
52
|
+
@truncator = Celerbrake::Truncator.new(PAYLOAD_MAX_SIZE)
|
|
53
|
+
|
|
54
|
+
@payload = {
|
|
55
|
+
errors: NestedException.new(exception).as_json,
|
|
56
|
+
context: context(exception),
|
|
57
|
+
environment: {
|
|
58
|
+
program_name: $PROGRAM_NAME,
|
|
59
|
+
},
|
|
60
|
+
session: {},
|
|
61
|
+
params: params,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
stash[:exception] = exception
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Converts the notice to JSON. Calls +to_json+ on each object inside
|
|
68
|
+
# notice's payload. Truncates notices, JSON representation of which is
|
|
69
|
+
# bigger than {MAX_NOTICE_SIZE}.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash{String=>String}, nil]
|
|
72
|
+
# @api private
|
|
73
|
+
def to_json(*_args)
|
|
74
|
+
loop do
|
|
75
|
+
begin
|
|
76
|
+
json = @payload.to_json
|
|
77
|
+
rescue *JSON_EXCEPTIONS => ex
|
|
78
|
+
logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
|
|
79
|
+
else
|
|
80
|
+
return json if json && json.bytesize <= MAX_NOTICE_SIZE
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
break if truncate == 0
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Reads a value from notice's payload.
|
|
88
|
+
#
|
|
89
|
+
# @return [Object]
|
|
90
|
+
# @raise [Celerbrake::Error] if the notice is ignored
|
|
91
|
+
def [](key)
|
|
92
|
+
raise_if_ignored
|
|
93
|
+
@payload[key]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Writes a value to the payload hash. Restricts unrecognized writes.
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# notice[:params][:my_param] = 'foobar'
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @raise [Celerbrake::Error] if the notice is ignored
|
|
103
|
+
# @raise [Celerbrake::Error] if the +key+ is not recognized
|
|
104
|
+
# @raise [Celerbrake::Error] if the root value is not a Hash
|
|
105
|
+
def []=(key, value)
|
|
106
|
+
raise_if_ignored
|
|
107
|
+
|
|
108
|
+
unless WRITABLE_KEYS.include?(key)
|
|
109
|
+
raise Celerbrake::Error,
|
|
110
|
+
":#{key} is not recognized among #{WRITABLE_KEYS}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless value.respond_to?(:to_hash)
|
|
114
|
+
raise Celerbrake::Error, "Got #{value.class} value, wanted a Hash"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@payload[key] = value.to_hash
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def context(exception)
|
|
123
|
+
{
|
|
124
|
+
version: @config.app_version,
|
|
125
|
+
versions: @config.versions,
|
|
126
|
+
# We ensure that root_directory is always a String, so it can always be
|
|
127
|
+
# converted to JSON in a predictable manner (when it's a Pathname and in
|
|
128
|
+
# Rails environment, it converts to unexpected JSON).
|
|
129
|
+
rootDirectory: @config.root_directory.to_s,
|
|
130
|
+
environment: @config.environment,
|
|
131
|
+
|
|
132
|
+
# Make sure we always send hostname.
|
|
133
|
+
hostname: HOSTNAME,
|
|
134
|
+
|
|
135
|
+
severity: DEFAULT_SEVERITY,
|
|
136
|
+
error_message: @truncator.truncate(exception.message),
|
|
137
|
+
}.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def truncate
|
|
141
|
+
TRUNCATABLE_KEYS.each do |key|
|
|
142
|
+
@payload[key] = @truncator.truncate(@payload[key])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
new_max_size = @truncator.reduce_max_size
|
|
146
|
+
if new_max_size == 0
|
|
147
|
+
logger.error(
|
|
148
|
+
"#{LOG_LABEL} truncation failed. File an issue at " \
|
|
149
|
+
"https://github.com/celerbrake/celerbrake-ruby " \
|
|
150
|
+
"and attach the following payload: #{@payload}",
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_max_size
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# NoticeNotifier is reponsible for sending notices to Celerbrake. It supports
|
|
3
|
+
# synchronous and asynchronous delivery.
|
|
4
|
+
#
|
|
5
|
+
# @see Celerbrake::Config The list of options
|
|
6
|
+
# @since v1.0.0
|
|
7
|
+
# @api public
|
|
8
|
+
class NoticeNotifier
|
|
9
|
+
# @return [Array<Class>] filters to be executed first
|
|
10
|
+
DEFAULT_FILTERS = [
|
|
11
|
+
Celerbrake::Filters::SystemExitFilter,
|
|
12
|
+
Celerbrake::Filters::GemRootFilter,
|
|
13
|
+
|
|
14
|
+
# Optional filters (must be included by users):
|
|
15
|
+
# Celerbrake::Filters::ThreadFilter
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
include Inspectable
|
|
19
|
+
include Loggable
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@config = Celerbrake::Config.instance
|
|
23
|
+
@filter_chain = FilterChain.new
|
|
24
|
+
@async_sender = AsyncSender.new(:post, self.class.name)
|
|
25
|
+
@sync_sender = SyncSender.new
|
|
26
|
+
|
|
27
|
+
DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
|
|
28
|
+
|
|
29
|
+
add_filter(Celerbrake::Filters::ContextFilter.new)
|
|
30
|
+
add_filter(Celerbrake::Filters::ExceptionAttributesFilter.new)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @see Celerbrake.notify
|
|
34
|
+
def notify(exception, params = {}, &block)
|
|
35
|
+
send_notice(exception, params, default_sender, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @see Celerbrake.notify_sync
|
|
39
|
+
def notify_sync(exception, params = {}, &block)
|
|
40
|
+
send_notice(exception, params, @sync_sender, &block).value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @see Celerbrake.add_filte
|
|
44
|
+
def add_filter(filter = nil, &block)
|
|
45
|
+
@filter_chain.add_filter(block_given? ? block : filter)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @see Celerbrake.delete_filter
|
|
49
|
+
def delete_filter(filter_class)
|
|
50
|
+
@filter_chain.delete_filter(filter_class)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @see Celerbrake.build_notice
|
|
54
|
+
def build_notice(exception, params = {})
|
|
55
|
+
if @async_sender.closed?
|
|
56
|
+
raise Celerbrake::Error,
|
|
57
|
+
"Celerbrake is closed; can't build exception: " \
|
|
58
|
+
"#{exception.class}: #{exception}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if exception.is_a?(Celerbrake::Notice)
|
|
62
|
+
exception[:params].merge!(params)
|
|
63
|
+
exception
|
|
64
|
+
else
|
|
65
|
+
Notice.new(convert_to_exception(exception), params.dup)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @see Celerbrake.close
|
|
70
|
+
def close
|
|
71
|
+
@sync_sender.close
|
|
72
|
+
@async_sender.close
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @see Celerbrake.configured?
|
|
76
|
+
def configured?
|
|
77
|
+
@config.valid?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @see Celerbrake.merge_context
|
|
81
|
+
def merge_context(context)
|
|
82
|
+
Celerbrake::Context.current.merge!(context)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
# @since v4.14.0
|
|
87
|
+
def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
|
|
88
|
+
@filter_chain.includes?(filter_class)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def convert_to_exception(ex)
|
|
94
|
+
if ex.is_a?(Exception) || Backtrace.java_exception?(ex)
|
|
95
|
+
# Manually created exceptions don't have backtraces, so we create a fake
|
|
96
|
+
# one, whose first frame points to the place where Celerbrake was called
|
|
97
|
+
# (normally via `notify`).
|
|
98
|
+
ex.set_backtrace(clean_backtrace) unless ex.backtrace
|
|
99
|
+
return ex
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
e = RuntimeError.new(ex.to_s)
|
|
103
|
+
e.set_backtrace(clean_backtrace)
|
|
104
|
+
e
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def send_notice(exception, params, sender)
|
|
108
|
+
promise = @config.check_configuration
|
|
109
|
+
return promise if promise.rejected?
|
|
110
|
+
|
|
111
|
+
notice = build_notice(exception, params)
|
|
112
|
+
yield notice if block_given?
|
|
113
|
+
@filter_chain.refine(notice)
|
|
114
|
+
|
|
115
|
+
promise = Celerbrake::Promise.new
|
|
116
|
+
return promise.reject("#{notice} was marked as ignored") if notice.ignored?
|
|
117
|
+
|
|
118
|
+
sender.send(notice, promise)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def default_sender
|
|
122
|
+
return @async_sender if @async_sender.has_workers?
|
|
123
|
+
|
|
124
|
+
logger.warn(
|
|
125
|
+
"#{LOG_LABEL} falling back to sync delivery because there are no " \
|
|
126
|
+
"running async workers",
|
|
127
|
+
)
|
|
128
|
+
@sync_sender
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def clean_backtrace
|
|
132
|
+
caller_copy = Kernel.caller
|
|
133
|
+
clean_bt = caller_copy.drop_while { |frame| frame.include?('/lib/celerbrake') }
|
|
134
|
+
|
|
135
|
+
# If true, then it's likely an internal library error. In this case return
|
|
136
|
+
# at least some backtrace to simplify debugging.
|
|
137
|
+
return caller_copy if clean_bt.empty?
|
|
138
|
+
|
|
139
|
+
clean_bt
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|