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,92 @@
|
|
|
1
|
+
require 'date'
|
|
2
|
+
|
|
3
|
+
module Celerbrake
|
|
4
|
+
module Filters
|
|
5
|
+
# Attaches git checkout info to `context`. The info includes:
|
|
6
|
+
# * username
|
|
7
|
+
# * email
|
|
8
|
+
# * revision
|
|
9
|
+
# * time
|
|
10
|
+
#
|
|
11
|
+
# This information is used to track deploys automatically.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since v2.12.0
|
|
15
|
+
class GitLastCheckoutFilter
|
|
16
|
+
# @return [Integer]
|
|
17
|
+
attr_reader :weight
|
|
18
|
+
|
|
19
|
+
# @return [Integer] least possible amount of columns in git's `logs/HEAD`
|
|
20
|
+
# file (checkout information is omitted)
|
|
21
|
+
MIN_HEAD_COLS = 6
|
|
22
|
+
|
|
23
|
+
include Loggable
|
|
24
|
+
|
|
25
|
+
# @param [String] root_directory
|
|
26
|
+
def initialize(root_directory)
|
|
27
|
+
@git_path = File.join(root_directory, '.git')
|
|
28
|
+
@weight = 116
|
|
29
|
+
@last_checkout = nil
|
|
30
|
+
@deploy_username = ENV.fetch('CELERBRAKE_DEPLOY_USERNAME', nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @macro call_filter
|
|
34
|
+
def call(notice)
|
|
35
|
+
return if notice[:context].key?(:lastCheckout)
|
|
36
|
+
|
|
37
|
+
if @last_checkout
|
|
38
|
+
notice[:context][:lastCheckout] = @last_checkout
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return unless File.exist?(@git_path)
|
|
43
|
+
return unless (checkout = last_checkout)
|
|
44
|
+
|
|
45
|
+
notice[:context][:lastCheckout] = checkout
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def last_checkout
|
|
51
|
+
return unless (line = last_checkout_line)
|
|
52
|
+
|
|
53
|
+
parts = line.chomp.split("\t").first.split
|
|
54
|
+
if parts.size < MIN_HEAD_COLS
|
|
55
|
+
logger.error(
|
|
56
|
+
"#{LOG_LABEL} Celerbrake::#{self.class.name}: can't parse line: #{line}",
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
author = parts[2..-4]
|
|
62
|
+
@last_checkout = {
|
|
63
|
+
username: @deploy_username || author[0..1].join(' '),
|
|
64
|
+
email: parts[-3][1..-2],
|
|
65
|
+
revision: parts[1],
|
|
66
|
+
time: timestamp(parts[-2].to_i),
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def last_checkout_line
|
|
71
|
+
head_path = File.join(@git_path, 'logs', 'HEAD')
|
|
72
|
+
return unless File.exist?(head_path)
|
|
73
|
+
|
|
74
|
+
last_line = nil
|
|
75
|
+
File.foreach(head_path) do |line|
|
|
76
|
+
last_line = line if checkout_line?(line)
|
|
77
|
+
end
|
|
78
|
+
last_line
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def checkout_line?(line)
|
|
82
|
+
line.include?("\tclone:") ||
|
|
83
|
+
line.include?("\tpull:") ||
|
|
84
|
+
line.include?("\tcheckout:")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def timestamp(utime)
|
|
88
|
+
Time.at(utime).to_datetime.rfc3339
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# Attaches git repository URL to `context`.
|
|
4
|
+
# @api private
|
|
5
|
+
# @since v2.12.0
|
|
6
|
+
class GitRepositoryFilter
|
|
7
|
+
# @return [Integer]
|
|
8
|
+
attr_reader :weight
|
|
9
|
+
|
|
10
|
+
# @param [String] root_directory
|
|
11
|
+
def initialize(root_directory)
|
|
12
|
+
@git_path = File.join(root_directory, '.git')
|
|
13
|
+
@repository = nil
|
|
14
|
+
@git_version = detect_git_version
|
|
15
|
+
@weight = 116
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @macro call_filter
|
|
19
|
+
def call(notice)
|
|
20
|
+
return if notice[:context].key?(:repository)
|
|
21
|
+
|
|
22
|
+
attach_repository(notice)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def attach_repository(notice)
|
|
26
|
+
if @repository
|
|
27
|
+
notice[:context][:repository] = @repository
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return unless File.exist?(@git_path)
|
|
32
|
+
return unless @git_version
|
|
33
|
+
|
|
34
|
+
@repository =
|
|
35
|
+
if @git_version >= Gem::Version.new('2.7.0')
|
|
36
|
+
`cd #{@git_path} && git config --get remote.origin.url`.chomp
|
|
37
|
+
else
|
|
38
|
+
"`git remote get-url` is unsupported in git #{@git_version}. " \
|
|
39
|
+
'Consider an upgrade to 2.7+'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return unless @repository
|
|
43
|
+
|
|
44
|
+
notice[:context][:repository] = @repository
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def detect_git_version
|
|
50
|
+
return unless which('git')
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
Gem::Version.new(`git --version`.split[2])
|
|
54
|
+
rescue Errno::EAGAIN
|
|
55
|
+
# Bugfix for the case when the system cannot allocate memory for
|
|
56
|
+
# a fork() call: https://github.com/celerbrake/celerbrake-ruby/issues/680
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Cross-platform way to tell if an executable is accessible.
|
|
62
|
+
def which(cmd)
|
|
63
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
|
64
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).find do |path|
|
|
65
|
+
exts.find do |ext|
|
|
66
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
|
67
|
+
File.executable?(exe) && !File.directory?(exe)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# Attaches current git revision to `context`.
|
|
4
|
+
# @api private
|
|
5
|
+
# @since v2.11.0
|
|
6
|
+
class GitRevisionFilter
|
|
7
|
+
# @return [Integer]
|
|
8
|
+
attr_reader :weight
|
|
9
|
+
|
|
10
|
+
# @return [String]
|
|
11
|
+
PREFIX = 'ref: '.freeze
|
|
12
|
+
|
|
13
|
+
# @param [String] root_directory
|
|
14
|
+
def initialize(root_directory)
|
|
15
|
+
@git_path = File.join(root_directory, '.git')
|
|
16
|
+
@revision = nil
|
|
17
|
+
@weight = 116
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @macro call_filter
|
|
21
|
+
def call(notice)
|
|
22
|
+
return if notice[:context].key?(:revision)
|
|
23
|
+
|
|
24
|
+
if @revision
|
|
25
|
+
notice[:context][:revision] = @revision
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return unless File.exist?(@git_path)
|
|
30
|
+
|
|
31
|
+
@revision = find_revision
|
|
32
|
+
return unless @revision
|
|
33
|
+
|
|
34
|
+
notice[:context][:revision] = @revision
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def find_revision
|
|
40
|
+
head_path = File.join(@git_path, 'HEAD')
|
|
41
|
+
return unless File.exist?(head_path)
|
|
42
|
+
|
|
43
|
+
head = File.read(head_path)
|
|
44
|
+
return head unless head.start_with?(PREFIX)
|
|
45
|
+
|
|
46
|
+
head = head.chomp[PREFIX.size..-1]
|
|
47
|
+
|
|
48
|
+
ref_path = File.join(@git_path, head)
|
|
49
|
+
return File.read(ref_path).chomp if File.exist?(ref_path)
|
|
50
|
+
|
|
51
|
+
find_from_packed_refs(head)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_from_packed_refs(head)
|
|
55
|
+
packed_refs_path = File.join(@git_path, 'packed-refs')
|
|
56
|
+
return head unless File.exist?(packed_refs_path)
|
|
57
|
+
|
|
58
|
+
File.readlines(packed_refs_path).each do |line|
|
|
59
|
+
next if %w[# ^].include?(line[0])
|
|
60
|
+
next unless (parts = line.split).size == 2
|
|
61
|
+
return parts.first if parts.last == head
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# A default Celerbrake notice filter. Filters everything in the payload of a
|
|
4
|
+
# notice, but specified keys.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# filter = Celerbrake::Filters::KeysAllowlist.new(
|
|
8
|
+
# [:email, /credit/i, 'password']
|
|
9
|
+
# )
|
|
10
|
+
# celerbrake.add_filter(filter)
|
|
11
|
+
# celerbrake.notify(StandardError.new('App crashed!'), {
|
|
12
|
+
# user: 'John',
|
|
13
|
+
# password: 's3kr3t',
|
|
14
|
+
# email: 'john@example.com',
|
|
15
|
+
# account_id: 42
|
|
16
|
+
# })
|
|
17
|
+
#
|
|
18
|
+
# # The dashboard will display this parameters as filtered, but other
|
|
19
|
+
# # values won't be affected:
|
|
20
|
+
# # { user: 'John',
|
|
21
|
+
# # password: '[Filtered]',
|
|
22
|
+
# # email: 'john@example.com',
|
|
23
|
+
# # account_id: 42 }
|
|
24
|
+
#
|
|
25
|
+
# @see KeysBlocklist
|
|
26
|
+
# @see KeysFilter
|
|
27
|
+
class KeysAllowlist
|
|
28
|
+
include KeysFilter
|
|
29
|
+
|
|
30
|
+
def initialize(*)
|
|
31
|
+
super
|
|
32
|
+
@weight = -100
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Boolean] true if the key doesn't match any pattern, false
|
|
36
|
+
# otherwise.
|
|
37
|
+
def should_filter?(key)
|
|
38
|
+
@patterns.none? do |pattern|
|
|
39
|
+
if pattern.is_a?(Regexp)
|
|
40
|
+
key.match(pattern)
|
|
41
|
+
else
|
|
42
|
+
key.to_s == pattern.to_s
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# A default Celerbrake notice filter. Filters only specific keys listed in the
|
|
4
|
+
# list of parameters in the payload of a notice.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# filter = Celerbrake::Filters::KeysBlocklist.new(
|
|
8
|
+
# [:email, /credit/i, 'password']
|
|
9
|
+
# )
|
|
10
|
+
# celerbrake.add_filter(filter)
|
|
11
|
+
# celerbrake.notify(StandardError.new('App crashed!'), {
|
|
12
|
+
# user: 'John'
|
|
13
|
+
# password: 's3kr3t',
|
|
14
|
+
# email: 'john@example.com',
|
|
15
|
+
# credit_card: '5555555555554444'
|
|
16
|
+
# })
|
|
17
|
+
#
|
|
18
|
+
# # The dashboard will display this parameter as is, but all other
|
|
19
|
+
# # values will be filtered:
|
|
20
|
+
# # { user: 'John',
|
|
21
|
+
# # password: '[Filtered]',
|
|
22
|
+
# # email: '[Filtered]',
|
|
23
|
+
# # credit_card: '[Filtered]' }
|
|
24
|
+
#
|
|
25
|
+
# @see KeysAllowlist
|
|
26
|
+
# @see KeysFilter
|
|
27
|
+
# @api private
|
|
28
|
+
class KeysBlocklist
|
|
29
|
+
include KeysFilter
|
|
30
|
+
|
|
31
|
+
def initialize(*)
|
|
32
|
+
super
|
|
33
|
+
@weight = -110
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean] true if the key matches at least one pattern, false
|
|
37
|
+
# otherwise
|
|
38
|
+
def should_filter?(key)
|
|
39
|
+
@patterns.any? do |pattern|
|
|
40
|
+
if pattern.is_a?(Regexp)
|
|
41
|
+
key.match(pattern)
|
|
42
|
+
else
|
|
43
|
+
key.to_s == pattern.to_s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# Namespace for all standard filters. Custom filters can also go under this
|
|
3
|
+
# namespace.
|
|
4
|
+
module Filters
|
|
5
|
+
# This is a filter helper that endows a class ability to filter notices'
|
|
6
|
+
# payload based on the return value of the +should_filter?+ method that a
|
|
7
|
+
# class that includes this module must implement.
|
|
8
|
+
#
|
|
9
|
+
# @see Notice
|
|
10
|
+
# @see KeysAllowlist
|
|
11
|
+
# @see KeysBlocklist
|
|
12
|
+
# @api private
|
|
13
|
+
module KeysFilter
|
|
14
|
+
# @return [String] The label to replace real values of filtered payload
|
|
15
|
+
FILTERED = '[Filtered]'.freeze
|
|
16
|
+
|
|
17
|
+
# @return [Array<String,Symbol,Regexp>] the array of classes instances of
|
|
18
|
+
# which can compared with payload keys
|
|
19
|
+
VALID_PATTERN_CLASSES = [String, Symbol, Regexp].freeze
|
|
20
|
+
|
|
21
|
+
# @return [Array<Symbol>] parts of a Notice's payload that can be modified
|
|
22
|
+
# by blocklist/allowlist filters
|
|
23
|
+
FILTERABLE_KEYS = %i[environment session params].freeze
|
|
24
|
+
|
|
25
|
+
# @return [Array<Symbol>] parts of a Notice's *context* payload that can
|
|
26
|
+
# be modified by blocklist/allowlist filters
|
|
27
|
+
FILTERABLE_CONTEXT_KEYS = %i[
|
|
28
|
+
user
|
|
29
|
+
|
|
30
|
+
# Provided by Celerbrake::Rack::HttpHeadersFilter
|
|
31
|
+
headers
|
|
32
|
+
referer
|
|
33
|
+
httpMethod
|
|
34
|
+
|
|
35
|
+
# Provided by Celerbrake::Rack::ContextFilter
|
|
36
|
+
userAddr
|
|
37
|
+
userAgent
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
include Loggable
|
|
41
|
+
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
attr_reader :weight
|
|
44
|
+
|
|
45
|
+
# Creates a new KeysBlocklist or KeysAllowlist filter that uses the given
|
|
46
|
+
# +patterns+ for filtering a notice's payload.
|
|
47
|
+
#
|
|
48
|
+
# @param [Array<String,Regexp,Symbol>] patterns
|
|
49
|
+
def initialize(patterns)
|
|
50
|
+
@patterns = patterns
|
|
51
|
+
@valid_patterns = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @!macro call_filter
|
|
55
|
+
# This is a mandatory method required by any filter integrated with
|
|
56
|
+
# FilterChain.
|
|
57
|
+
#
|
|
58
|
+
# @param [Notice] notice the notice to be filtered
|
|
59
|
+
# @return [void]
|
|
60
|
+
# @see FilterChain
|
|
61
|
+
def call(notice)
|
|
62
|
+
unless @valid_patterns
|
|
63
|
+
eval_proc_patterns!
|
|
64
|
+
validate_patterns
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
FILTERABLE_KEYS.each do |key|
|
|
68
|
+
notice[key] = filter_hash(notice[key])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
FILTERABLE_CONTEXT_KEYS.each { |key| filter_context_key(notice, key) }
|
|
72
|
+
|
|
73
|
+
return unless notice[:context][:url]
|
|
74
|
+
|
|
75
|
+
filter_url(notice)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @raise [NotImplementedError] if called directly
|
|
79
|
+
def should_filter?(_key)
|
|
80
|
+
raise NotImplementedError, 'method must be implemented in the included class'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def filter_hash(hash) # rubocop:disable Metrics/AbcSize
|
|
86
|
+
return hash unless hash.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
hash_copy = hash.dup
|
|
89
|
+
|
|
90
|
+
hash.each_key do |key|
|
|
91
|
+
if should_filter?(key.to_s)
|
|
92
|
+
hash_copy[key] = FILTERED
|
|
93
|
+
elsif hash_copy[key].is_a?(Hash)
|
|
94
|
+
hash_copy[key] = filter_hash(hash_copy[key])
|
|
95
|
+
elsif hash[key].is_a?(Array)
|
|
96
|
+
hash_copy[key].each_with_index do |h, i|
|
|
97
|
+
hash_copy[key][i] = filter_hash(h)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
hash_copy
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def filter_url_params(url)
|
|
106
|
+
url.query = URI.decode_www_form(url.query).to_h.map do |key, val|
|
|
107
|
+
should_filter?(key) ? "#{key}=[Filtered]" : "#{key}=#{val}"
|
|
108
|
+
end.join('&')
|
|
109
|
+
|
|
110
|
+
url.to_s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def filter_url(notice)
|
|
114
|
+
begin
|
|
115
|
+
url = URI(notice[:context][:url])
|
|
116
|
+
rescue URI::InvalidURIError
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
return unless url.query
|
|
121
|
+
|
|
122
|
+
notice[:context][:url] = filter_url_params(url)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def eval_proc_patterns!
|
|
126
|
+
return unless @patterns.any? { |pattern| pattern.is_a?(Proc) }
|
|
127
|
+
|
|
128
|
+
@patterns = @patterns.flat_map do |pattern|
|
|
129
|
+
next(pattern) unless pattern.respond_to?(:call)
|
|
130
|
+
|
|
131
|
+
pattern.call
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_patterns
|
|
136
|
+
@valid_patterns = @patterns.all? do |pattern|
|
|
137
|
+
VALID_PATTERN_CLASSES.any? { |c| pattern.is_a?(c) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return if @valid_patterns
|
|
141
|
+
|
|
142
|
+
logger.error(
|
|
143
|
+
"#{LOG_LABEL} one of the patterns in #{self.class} is invalid. " \
|
|
144
|
+
"Known patterns: #{@patterns}",
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def filter_context_key(notice, key)
|
|
149
|
+
return unless notice[:context][key]
|
|
150
|
+
return if notice[:context][key] == FILTERED
|
|
151
|
+
unless should_filter?(key)
|
|
152
|
+
return notice[:context][key] = filter_hash(notice[:context][key])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
notice[:context][key] = FILTERED
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# Replaces root directory with a label.
|
|
4
|
+
# @api private
|
|
5
|
+
class RootDirectoryFilter
|
|
6
|
+
# @return [String]
|
|
7
|
+
PROJECT_ROOT_LABEL = '/PROJECT_ROOT'.freeze
|
|
8
|
+
|
|
9
|
+
# @return [Integer]
|
|
10
|
+
attr_reader :weight
|
|
11
|
+
|
|
12
|
+
def initialize(root_directory)
|
|
13
|
+
@root_directory = root_directory
|
|
14
|
+
@weight = 100
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @macro call_filter
|
|
18
|
+
def call(notice)
|
|
19
|
+
notice[:errors].each do |error|
|
|
20
|
+
error[:backtrace].each do |frame|
|
|
21
|
+
next unless (file = frame[:file])
|
|
22
|
+
|
|
23
|
+
file.sub!(/\A#{@root_directory}/, PROJECT_ROOT_LABEL)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# SqlFilter filters out sensitive data from {Celerbrake::Query}. Sensitive
|
|
4
|
+
# data is everything that is not table names or fields (e.g. column values
|
|
5
|
+
# and such).
|
|
6
|
+
#
|
|
7
|
+
# Supports the following SQL dialects:
|
|
8
|
+
# * PostgreSQL
|
|
9
|
+
# * MySQL
|
|
10
|
+
# * SQLite
|
|
11
|
+
# * Cassandra
|
|
12
|
+
# * Oracle
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
# @since v3.2.0
|
|
16
|
+
class SqlFilter
|
|
17
|
+
# @return [String] the label to replace real values of filtered query
|
|
18
|
+
FILTERED = '?'.freeze
|
|
19
|
+
|
|
20
|
+
# @return [String] the string that will replace the query in case we
|
|
21
|
+
# cannot filter it
|
|
22
|
+
ERROR_MSG = 'Error: Celerbrake::Query was not filtered'.freeze
|
|
23
|
+
|
|
24
|
+
# @return [Hash{Symbol=>Regexp}] matchers for certain features of SQL
|
|
25
|
+
ALL_FEATURES = {
|
|
26
|
+
# rubocop:disable Layout/LineLength
|
|
27
|
+
single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
|
|
28
|
+
double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
|
|
29
|
+
dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
|
|
30
|
+
uuids: /\{?(?:[0-9a-fA-F]-*){32}\}?/,
|
|
31
|
+
numeric_literals: /\b-?(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
|
|
32
|
+
boolean_literals: /\b(?:true|false|null)\b/i,
|
|
33
|
+
hexadecimal_literals: /0x[0-9a-fA-F]+/,
|
|
34
|
+
comments: /(?:#|--).*?(?=\r|\n|$)/i,
|
|
35
|
+
multi_line_comments: %r{/\*(?:[^/]|/[^*])*?(?:\*/|/\*.*)},
|
|
36
|
+
oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'<.*?(?:>'|$)|q'\(.*?(?:\)'|$)/,
|
|
37
|
+
# rubocop:enable Layout/LineLength
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
# @return [Regexp] the regexp that is applied after the feature regexps
|
|
41
|
+
# were used
|
|
42
|
+
POST_FILTER = /(?<=[values|in ]\().+(?=\))/i.freeze
|
|
43
|
+
|
|
44
|
+
# @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
|
|
45
|
+
# to a certain dialect
|
|
46
|
+
DIALECT_FEATURES = {
|
|
47
|
+
default: ALL_FEATURES.keys,
|
|
48
|
+
mysql: %i[
|
|
49
|
+
single_quotes double_quotes numeric_literals boolean_literals
|
|
50
|
+
hexadecimal_literals comments multi_line_comments
|
|
51
|
+
].freeze,
|
|
52
|
+
postgres: %i[
|
|
53
|
+
single_quotes dollar_quotes uuids numeric_literals boolean_literals
|
|
54
|
+
comments multi_line_comments
|
|
55
|
+
].freeze,
|
|
56
|
+
sqlite: %i[
|
|
57
|
+
single_quotes numeric_literals boolean_literals hexadecimal_literals
|
|
58
|
+
comments multi_line_comments
|
|
59
|
+
].freeze,
|
|
60
|
+
oracle: %i[
|
|
61
|
+
single_quotes oracle_quoted_strings numeric_literals comments
|
|
62
|
+
multi_line_comments
|
|
63
|
+
].freeze,
|
|
64
|
+
cassandra: %i[
|
|
65
|
+
single_quotes uuids numeric_literals boolean_literals
|
|
66
|
+
hexadecimal_literals comments multi_line_comments
|
|
67
|
+
].freeze,
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
# @return [Hash{Symbol=>Regexp}] a set of regexps to check for unmatches
|
|
71
|
+
# quotes after filtering (should be none)
|
|
72
|
+
UNMATCHED_PAIR = {
|
|
73
|
+
mysql: %r{'|"|/\*|\*/},
|
|
74
|
+
mysql2: %r{'|"|/\*|\*/},
|
|
75
|
+
postgres: %r{'|/\*|\*/|\$(?!\?)},
|
|
76
|
+
sqlite: %r{'|/\*|\*/},
|
|
77
|
+
cassandra: %r{'|/\*|\*/},
|
|
78
|
+
oracle: %r{'|/\*|\*/},
|
|
79
|
+
oracle_enhanced: %r{'|/\*|\*/},
|
|
80
|
+
}.freeze
|
|
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
|
+
/FROM pg_index/i,
|
|
91
|
+
/FROM pg_class/i,
|
|
92
|
+
/FROM pg_type/i,
|
|
93
|
+
].freeze
|
|
94
|
+
|
|
95
|
+
def initialize(dialect)
|
|
96
|
+
@dialect =
|
|
97
|
+
case dialect
|
|
98
|
+
when /mysql/i then :mysql
|
|
99
|
+
when /postgres/i then :postgres
|
|
100
|
+
when /sqlite/i then :sqlite
|
|
101
|
+
when /oracle/i then :oracle
|
|
102
|
+
when /cassandra/i then :cassandra
|
|
103
|
+
else
|
|
104
|
+
:default
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
features = DIALECT_FEATURES[@dialect].map { |f| ALL_FEATURES[f] }
|
|
108
|
+
@regexp = Regexp.union(features)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param [Celerbrake::Query] metric
|
|
112
|
+
def call(metric)
|
|
113
|
+
return unless metric.respond_to?(:query)
|
|
114
|
+
|
|
115
|
+
query = metric.query
|
|
116
|
+
if IGNORED_QUERIES.any? { |q| q =~ query }
|
|
117
|
+
metric.ignore!
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
q = query.gsub(@regexp, FILTERED)
|
|
122
|
+
q.gsub!(POST_FILTER, FILTERED) if q =~ POST_FILTER
|
|
123
|
+
q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
|
|
124
|
+
metric.query = q
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
module Filters
|
|
3
|
+
# Skip over SystemExit exceptions, because they're just noise.
|
|
4
|
+
# @api private
|
|
5
|
+
class SystemExitFilter
|
|
6
|
+
# @return [String]
|
|
7
|
+
SYSTEM_EXIT_TYPE = 'SystemExit'.freeze
|
|
8
|
+
|
|
9
|
+
# @return [Integer]
|
|
10
|
+
attr_reader :weight
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@weight = 130
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @macro call_filter
|
|
17
|
+
def call(notice)
|
|
18
|
+
return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
|
|
19
|
+
|
|
20
|
+
notice.ignore!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|