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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/lib/celerbrake-ruby/async_sender.rb +57 -0
  3. data/lib/celerbrake-ruby/backlog.rb +123 -0
  4. data/lib/celerbrake-ruby/backtrace.rb +197 -0
  5. data/lib/celerbrake-ruby/benchmark.rb +39 -0
  6. data/lib/celerbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/celerbrake-ruby/config/processor.rb +77 -0
  8. data/lib/celerbrake-ruby/config/validator.rb +97 -0
  9. data/lib/celerbrake-ruby/config.rb +291 -0
  10. data/lib/celerbrake-ruby/context.rb +51 -0
  11. data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
  12. data/lib/celerbrake-ruby/file_cache.rb +54 -0
  13. data/lib/celerbrake-ruby/filter_chain.rb +112 -0
  14. data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
  15. data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
  16. data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  17. data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
  18. data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  19. data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
  20. data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
  21. data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
  22. data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
  23. data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
  24. data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
  25. data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
  26. data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
  27. data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
  28. data/lib/celerbrake-ruby/grouppable.rb +12 -0
  29. data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
  30. data/lib/celerbrake-ruby/ignorable.rb +43 -0
  31. data/lib/celerbrake-ruby/inspectable.rb +39 -0
  32. data/lib/celerbrake-ruby/loggable.rb +34 -0
  33. data/lib/celerbrake-ruby/mergeable.rb +12 -0
  34. data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
  35. data/lib/celerbrake-ruby/nested_exception.rb +59 -0
  36. data/lib/celerbrake-ruby/notice.rb +157 -0
  37. data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
  38. data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
  39. data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
  40. data/lib/celerbrake-ruby/promise.rb +110 -0
  41. data/lib/celerbrake-ruby/query.rb +59 -0
  42. data/lib/celerbrake-ruby/queue.rb +65 -0
  43. data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
  44. data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
  45. data/lib/celerbrake-ruby/remote_settings.rb +128 -0
  46. data/lib/celerbrake-ruby/request.rb +48 -0
  47. data/lib/celerbrake-ruby/response.rb +125 -0
  48. data/lib/celerbrake-ruby/stashable.rb +15 -0
  49. data/lib/celerbrake-ruby/stat.rb +66 -0
  50. data/lib/celerbrake-ruby/sync_sender.rb +145 -0
  51. data/lib/celerbrake-ruby/tdigest.rb +379 -0
  52. data/lib/celerbrake-ruby/thread_pool.rb +139 -0
  53. data/lib/celerbrake-ruby/time_truncate.rb +17 -0
  54. data/lib/celerbrake-ruby/timed_trace.rb +56 -0
  55. data/lib/celerbrake-ruby/truncator.rb +121 -0
  56. data/lib/celerbrake-ruby/version.rb +16 -0
  57. data/lib/celerbrake-ruby.rb +592 -0
  58. 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