deprecation_collector 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -1
- data/CHANGELOG.md +6 -1
- data/Gemfile +5 -1
- data/Gemfile.lock +16 -1
- data/README.md +22 -6
- data/lib/deprecation_collector/collectors.rb +104 -0
- data/lib/deprecation_collector/deprecation.rb +89 -0
- data/lib/deprecation_collector/version.rb +1 -1
- data/lib/deprecation_collector.rb +89 -225
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02cc02dc5d90e6b304ac5dadfc7ee09c1ac684ced0d3e0657095a47ffdc88e82
|
|
4
|
+
data.tar.gz: ce2b0e299aed5c65f302ffb4fb72d49e7ea1f44772a5b6d61eeae1807eecb1fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b468f3a95e56ced60c3de42996f1d6c4d9249588dfe972c321aaf3dcf80f81cf9048dc040b14c9a604f7c01d780385de2ec4e3ea1cb5dbda47ac541bd20d1a83
|
|
7
|
+
data.tar.gz: a245eb05033c494e514ba10e0ba63888bffb5e7fb86365a9e09cc425674be7c01df17d7c4870af60011c5a26600f07825e3a74971b5b690859762880f35b33e9
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion: 2.6
|
|
2
|
+
# spec/ have TargetRubyVersion: 2.6
|
|
3
|
+
TargetRubyVersion: 2.4
|
|
4
|
+
NewCops: enable
|
|
3
5
|
|
|
4
6
|
Style/StringLiterals:
|
|
5
7
|
Enabled: true
|
|
@@ -11,3 +13,8 @@ Style/StringLiteralsInInterpolation:
|
|
|
11
13
|
|
|
12
14
|
Layout/LineLength:
|
|
13
15
|
Max: 120
|
|
16
|
+
|
|
17
|
+
Metrics/ClassLength: { Max: 200 }
|
|
18
|
+
Metrics/MethodLength: { Max: 15 }
|
|
19
|
+
Metrics/CyclomaticComplexity: { Max: 9 }
|
|
20
|
+
Metrics/PerceivedComplexity: { Max: 9 }
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
deprecation_collector (0.0.
|
|
4
|
+
deprecation_collector (0.0.2)
|
|
5
5
|
redis (>= 2.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -150,6 +150,17 @@ GEM
|
|
|
150
150
|
unicode-display_width (>= 1.4.0, < 3.0)
|
|
151
151
|
rubocop-ast (1.18.0)
|
|
152
152
|
parser (>= 3.1.1.0)
|
|
153
|
+
rubocop-performance (1.14.3)
|
|
154
|
+
rubocop (>= 1.7.0, < 2.0)
|
|
155
|
+
rubocop-ast (>= 0.4.0)
|
|
156
|
+
rubocop-rails (2.15.2)
|
|
157
|
+
activesupport (>= 4.2.0)
|
|
158
|
+
rack (>= 1.1)
|
|
159
|
+
rubocop (>= 1.7.0, < 2.0)
|
|
160
|
+
rubocop-rake (0.6.0)
|
|
161
|
+
rubocop (~> 1.0)
|
|
162
|
+
rubocop-rspec (2.11.1)
|
|
163
|
+
rubocop (~> 1.19)
|
|
153
164
|
ruby-progressbar (1.11.0)
|
|
154
165
|
sprockets (4.0.3)
|
|
155
166
|
concurrent-ruby (~> 1.0)
|
|
@@ -178,6 +189,10 @@ DEPENDENCIES
|
|
|
178
189
|
rake (~> 13.0)
|
|
179
190
|
rspec (~> 3.0)
|
|
180
191
|
rubocop (~> 1.21)
|
|
192
|
+
rubocop-performance
|
|
193
|
+
rubocop-rails
|
|
194
|
+
rubocop-rake
|
|
195
|
+
rubocop-rspec
|
|
181
196
|
timecop
|
|
182
197
|
|
|
183
198
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# DeprecationCollector
|
|
2
|
+
[](https://badge.fury.io/rb/deprecation_collector)
|
|
2
3
|
|
|
3
4
|
Collects ruby and rails deprecation warnings.
|
|
4
5
|
(gem is a work-in-process, documentation will come later)
|
|
@@ -7,15 +8,30 @@ Collects ruby and rails deprecation warnings.
|
|
|
7
8
|
|
|
8
9
|
Install the gem and add to the application's Gemfile by executing:
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
$ gem install deprecation_collector
|
|
11
|
+
```sh
|
|
12
|
+
bundle add deprecation_collector
|
|
13
|
+
```
|
|
15
14
|
|
|
16
15
|
## Usage
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Add an initializer with configuration, like
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
DeprecationCollector.create_instance(redis: your_redis_connection)
|
|
21
|
+
Rails.application.config.to_prepare do
|
|
22
|
+
DeprecationCollector.install do |instance|
|
|
23
|
+
instance.app_revision = ::GIT_REVISION
|
|
24
|
+
instance.count = false
|
|
25
|
+
instance.save_full_backtrace = true
|
|
26
|
+
instance.raise_on_deprecation = false
|
|
27
|
+
instance.write_interval = (::Rails.env.production? && 15.minutes) || 1.minute
|
|
28
|
+
instance.exclude_realms = %i[kernel] if Rails.env.production?
|
|
29
|
+
instance.ignored_messages = [
|
|
30
|
+
"Ignoring db/schema_cache.yml because it has expired"
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
19
35
|
|
|
20
36
|
## Development
|
|
21
37
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :nodoc:
|
|
4
|
+
class DeprecationCollector
|
|
5
|
+
class << self
|
|
6
|
+
protected
|
|
7
|
+
|
|
8
|
+
def install_collectors
|
|
9
|
+
tap_activesupport if defined?(ActiveSupport::Deprecation)
|
|
10
|
+
tap_kernel
|
|
11
|
+
tap_warning_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def tap_warning_class
|
|
15
|
+
Warning.singleton_class.prepend(DeprecationCollector::WarningCollector)
|
|
16
|
+
Warning[:deprecated] = true if Warning.respond_to?(:[]=) # turn on ruby 2.7 deprecations
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tap_activesupport
|
|
20
|
+
# TODO: a more polite hook
|
|
21
|
+
ActiveSupport::Deprecation.behavior = lambda do |message, callstack, deprecation_horizon, gem_name|
|
|
22
|
+
# not polite to turn off all other possible behaviors, but otherwise may get duplicate calls
|
|
23
|
+
DeprecationCollector.collect(message, callstack, :rails)
|
|
24
|
+
ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[stock_activesupport_behavior].call(
|
|
25
|
+
message, callstack, deprecation_horizon, gem_name
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stock_activesupport_behavior
|
|
31
|
+
Rails.application&.config&.active_support&.deprecation || :log
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def tap_kernel
|
|
35
|
+
Kernel.class_eval do
|
|
36
|
+
# module is included in others thus prepend does not work
|
|
37
|
+
remove_method :warn
|
|
38
|
+
class << self
|
|
39
|
+
remove_method :warn
|
|
40
|
+
end
|
|
41
|
+
module_function(define_method(:warn) do |*messages, **kwargs|
|
|
42
|
+
KernelWarningCollector.warn(*messages, backtrace: caller, **kwargs)
|
|
43
|
+
end)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Ruby sometimes has two warnings for one actual occurence
|
|
49
|
+
# Example:
|
|
50
|
+
# caller.rb:1: warning: Passing the keyword argument as the last hash parameter is deprecated
|
|
51
|
+
# calleee.rb:1: warning: The called method `method_name' is defined here
|
|
52
|
+
module MultipartWarningJoiner
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def two_part_warning?(str)
|
|
56
|
+
# see ruby src - `rb_warn`, `rb_compile_warn`
|
|
57
|
+
str.end_with?(
|
|
58
|
+
"uses the deprecated method signature, which takes one parameter\n", # respond_to?
|
|
59
|
+
# 2.7 kwargs:
|
|
60
|
+
"maybe ** should be added to the call\n",
|
|
61
|
+
"Passing the keyword argument as the last hash parameter is deprecated\n", # бывает и не двойной
|
|
62
|
+
"Splitting the last argument into positional and keyword parameters is deprecated\n"
|
|
63
|
+
) ||
|
|
64
|
+
str.include?("warning: already initialized constant") ||
|
|
65
|
+
str.include?("warning: method redefined; discarding old")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def handle(new_str)
|
|
69
|
+
old_str = Thread.current[:multipart_warning_str]
|
|
70
|
+
Thread.current[:multipart_warning_str] = nil
|
|
71
|
+
if old_str
|
|
72
|
+
return yield(old_str + new_str) if new_str.include?("is defined here") || new_str.include?(" was here")
|
|
73
|
+
|
|
74
|
+
yield(old_str)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return (Thread.current[:multipart_warning_str] = new_str) if two_part_warning?(new_str)
|
|
78
|
+
|
|
79
|
+
yield(new_str)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# taps into ruby core Warning#warn
|
|
84
|
+
module WarningCollector
|
|
85
|
+
def warn(str)
|
|
86
|
+
backtrace = caller
|
|
87
|
+
MultipartWarningJoiner.handle(str) do |multi_str|
|
|
88
|
+
DeprecationCollector.collect(multi_str, backtrace, :warning)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# for tapping into Kernel#warn
|
|
94
|
+
module KernelWarningCollector
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
def warn(*messages, backtrace: nil, **_kwargs)
|
|
98
|
+
backtrace ||= caller
|
|
99
|
+
str = messages.map(&:to_s).join("\n").strip
|
|
100
|
+
DeprecationCollector.collect(str, backtrace, :kernel)
|
|
101
|
+
# not passing to `super` - it will pass to Warning#warn, we do not want that
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DeprecationCollector
|
|
4
|
+
# :nodoc:
|
|
5
|
+
class Deprecation
|
|
6
|
+
attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :full_backtrace
|
|
7
|
+
|
|
8
|
+
CLEANUP_REGEXES = {
|
|
9
|
+
# rails views generated methods names are unique per-worker
|
|
10
|
+
/_app_views_(\w+)__(\d+)_(\d+)/ => "_app_views_\\1__",
|
|
11
|
+
|
|
12
|
+
# repl line numbers are not important, may be ignore all repl at all
|
|
13
|
+
/\A\((pry|irb)\):\d+/ => '(\1)'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(message, realm = nil, backtrace = [], cleanup_prefixes = [])
|
|
17
|
+
# backtrace is Thread::Backtrace::Location or array of strings for other realms
|
|
18
|
+
@message = message.dup
|
|
19
|
+
@realm = realm
|
|
20
|
+
@occurences = 0
|
|
21
|
+
@gem_traceline = find_gem_traceline(backtrace)
|
|
22
|
+
@app_traceline = find_app_traceline(backtrace)
|
|
23
|
+
|
|
24
|
+
cleanup_prefixes.each do |path|
|
|
25
|
+
@gem_traceline.delete_prefix!(path)
|
|
26
|
+
@message.gsub!(path, "")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
CLEANUP_REGEXES.each_pair do |regex, replace|
|
|
30
|
+
@gem_traceline&.gsub!(regex, replace)
|
|
31
|
+
@app_traceline&.gsub!(regex, replace)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def touch
|
|
38
|
+
@occurences += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ignored?
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def message_for_digest
|
|
46
|
+
# some gems like rest-client put data in warnings, need to aggregate
|
|
47
|
+
# + some bactrace per-worker unique method names may be there
|
|
48
|
+
@message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, "___").gsub(/\((pry|irb)\):\d+/, '(\1)')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def digest
|
|
52
|
+
@digest ||= Digest::MD5.hexdigest(digest_base)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def digest_base
|
|
56
|
+
"1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def as_json(_options = {})
|
|
60
|
+
{
|
|
61
|
+
message: message,
|
|
62
|
+
realm: realm,
|
|
63
|
+
app_traceline: app_traceline,
|
|
64
|
+
gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
|
|
65
|
+
full_backtrace: full_backtrace,
|
|
66
|
+
ruby_version: RUBY_VERSION,
|
|
67
|
+
rails_version: (defined?(Rails) && Rails.version),
|
|
68
|
+
hostname: Socket.gethostname,
|
|
69
|
+
revision: DeprecationCollector.instance.app_revision,
|
|
70
|
+
count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
|
|
71
|
+
digest_base: digest_base # for debug purposes
|
|
72
|
+
}.compact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
def find_app_traceline(backtrace)
|
|
78
|
+
app_root = DeprecationCollector.instance.app_root_prefix
|
|
79
|
+
backtrace.find do |line|
|
|
80
|
+
line = line.to_s
|
|
81
|
+
(!line.start_with?("/") || line.start_with?(app_root)) && !line.include?("/gems/")
|
|
82
|
+
end&.to_s&.dup&.delete_prefix(app_root)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def find_gem_traceline(backtrace)
|
|
86
|
+
backtrace.find { |line| !line.to_s.include?("kernel_warn") }&.to_s&.dup || backtrace.first.to_s.dup
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "deprecation_collector/version"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
require_relative "deprecation_collector/deprecation"
|
|
5
|
+
require_relative "deprecation_collector/collectors"
|
|
6
|
+
require "time"
|
|
7
|
+
require "redis"
|
|
6
8
|
|
|
9
|
+
# singleton class for collector
|
|
7
10
|
class DeprecationCollector
|
|
8
|
-
# NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
|
|
9
|
-
FLUSH_INTERVAL = (::Rails.env.production? && 15.minutes) || 1.minute
|
|
10
|
-
|
|
11
11
|
@instance_mutex = Mutex.new
|
|
12
12
|
@installed = false
|
|
13
13
|
private_class_method :new
|
|
@@ -15,9 +15,13 @@ class DeprecationCollector
|
|
|
15
15
|
def self.instance
|
|
16
16
|
return @instance if defined?(@instance) && @instance
|
|
17
17
|
|
|
18
|
+
create_instance
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.create_instance
|
|
18
22
|
@instance_mutex.synchronize do
|
|
19
|
-
#
|
|
20
|
-
@instance ||= new(
|
|
23
|
+
# no real need to reuse the mutex, but it is used only once here anyway
|
|
24
|
+
@instance ||= new(mutex: @instance_mutex)
|
|
21
25
|
end
|
|
22
26
|
@instance
|
|
23
27
|
end
|
|
@@ -28,7 +32,7 @@ class DeprecationCollector
|
|
|
28
32
|
|
|
29
33
|
# inside dev env may be called multiple times
|
|
30
34
|
def self.install
|
|
31
|
-
|
|
35
|
+
create_instance # to make it created, configuration comes later
|
|
32
36
|
|
|
33
37
|
@instance_mutex.synchronize do
|
|
34
38
|
unless @installed
|
|
@@ -37,213 +41,63 @@ class DeprecationCollector
|
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
yield instance if block_given?
|
|
44
|
+
instance.fetch_known_digests
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
ActiveSupport::Deprecation.behavior = lambda do |message, callstack, deprecation_horizon, gem_name|
|
|
43
|
-
# not polite to turn off all other possible behaviors, but otherwise may get duplicate calls
|
|
44
|
-
DeprecationCollector.collect(message, callstack, :rails)
|
|
45
|
-
ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[Rails.application&.config&.active_support&.deprecation || :log].call(
|
|
46
|
-
message, callstack, deprecation_horizon, gem_name
|
|
47
|
-
)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
Kernel.class_eval do
|
|
51
|
-
# module is included in others thus prepend does not work
|
|
52
|
-
remove_method :warn
|
|
53
|
-
class << self
|
|
54
|
-
remove_method :warn
|
|
55
|
-
end
|
|
56
|
-
module_function(define_method(:warn) do |*messages, **kwargs|
|
|
57
|
-
KernelWarningCollector.warn(*messages, backtrace: caller, **kwargs)
|
|
58
|
-
end)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
Warning.singleton_class.prepend(DeprecationCollector::WarningCollector)
|
|
62
|
-
Warning[:deprecated] = true if Warning.respond_to?(:[]=) # turn on ruby 2.7 deprecations
|
|
46
|
+
install_collectors
|
|
63
47
|
end
|
|
64
48
|
|
|
65
49
|
@instance
|
|
66
50
|
end
|
|
67
51
|
|
|
68
|
-
|
|
69
|
-
module_function
|
|
70
|
-
|
|
71
|
-
# Ruby sometimes has two warnings for one actual occurence
|
|
72
|
-
# Example:
|
|
73
|
-
# caller.rb:1: warning: Passing the keyword argument as the last hash parameter is deprecated
|
|
74
|
-
# calleee.rb:1: warning: The called method `method_name' is defined here
|
|
75
|
-
def two_part_warning?(str)
|
|
76
|
-
# see ruby src - `rb_warn`, `rb_compile_warn`
|
|
77
|
-
str.end_with?(
|
|
78
|
-
"uses the deprecated method signature, which takes one parameter\n", # respond_to?
|
|
79
|
-
# 2.7 kwargs:
|
|
80
|
-
"maybe ** should be added to the call\n",
|
|
81
|
-
"Passing the keyword argument as the last hash parameter is deprecated\n", # бывает и не двойной
|
|
82
|
-
"Splitting the last argument into positional and keyword parameters is deprecated\n"
|
|
83
|
-
) ||
|
|
84
|
-
str.include?("warning: already initialized constant") ||
|
|
85
|
-
str.include?("warning: method redefined; discarding old")
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def handle(new_str)
|
|
89
|
-
old_str = Thread.current[:multipart_warning_str]
|
|
90
|
-
Thread.current[:multipart_warning_str] = nil
|
|
91
|
-
if old_str
|
|
92
|
-
return yield(old_str + new_str) if new_str.include?('is defined here') || new_str.include?(' was here')
|
|
93
|
-
yield(old_str)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if two_part_warning?(new_str)
|
|
97
|
-
Thread.current[:multipart_warning_str] = new_str
|
|
98
|
-
return
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
yield(new_str)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# taps into ruby core Warning#warn
|
|
106
|
-
module WarningCollector
|
|
107
|
-
def warn(str)
|
|
108
|
-
backtrace = caller
|
|
109
|
-
MultipartWarningJoiner.handle(str) do |multi_str|
|
|
110
|
-
DeprecationCollector.collect(multi_str, backtrace, :warning)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
module KernelWarningCollector
|
|
116
|
-
module_function
|
|
117
|
-
|
|
118
|
-
def warn(*messages, backtrace: nil, **_kwargs)
|
|
119
|
-
backtrace ||= caller
|
|
120
|
-
str = messages.map(&:to_s).join("\n").strip
|
|
121
|
-
DeprecationCollector.collect(str, backtrace, :kernel)
|
|
122
|
-
# not passing to `super` - it will pass to Warning#warn, we do not want that
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
class Deprecation
|
|
127
|
-
attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :full_backtrace
|
|
128
|
-
|
|
129
|
-
def initialize(message, realm = nil, backtrace = [], cleanup_prefixes = [])
|
|
130
|
-
# backtrace is Thread::Backtrace::Location or array of strings for other realms
|
|
131
|
-
@message = message.dup
|
|
132
|
-
@realm = realm
|
|
133
|
-
@occurences = 0
|
|
134
|
-
@gem_traceline = backtrace.find { |line| !line.to_s.include?('kernel_warn') }&.to_s&.dup ||
|
|
135
|
-
backtrace.first.to_s.dup
|
|
136
|
-
cleanup_prefixes.each do |path|
|
|
137
|
-
@gem_traceline.delete_prefix!(path)
|
|
138
|
-
@message.gsub!(path, '')
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
app_root = "#{DeprecationCollector.instance.app_root}/" # rubocop:disable Rails/FilePath
|
|
142
|
-
@app_traceline = backtrace.find do |line|
|
|
143
|
-
line = line.to_s
|
|
144
|
-
(!line.start_with?('/') || line.start_with?(app_root)) && !line.include?('/gems/')
|
|
145
|
-
end&.to_s&.dup&.delete_prefix(app_root)
|
|
146
|
-
|
|
147
|
-
# rails views generated methods names are unique per-worker
|
|
148
|
-
@gem_traceline&.gsub!(/_app_views_(\w+)__(\d+)_(\d+)/, "_app_views_\\1__")
|
|
149
|
-
@app_traceline&.gsub!(/_app_views_(\w+)__(\d+)_(\d+)/, "_app_views_\\1__")
|
|
150
|
-
|
|
151
|
-
# repl line numbers are not important, may be ignore all repl at all
|
|
152
|
-
@app_traceline&.gsub!(/\A\((pry|irb)\):\d+/, '(\1)')
|
|
153
|
-
@gem_traceline&.gsub!(/\A\((pry|irb)\):\d+/, '(\1)') # may contain app traceline, so filter too
|
|
154
|
-
|
|
155
|
-
@full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def touch
|
|
159
|
-
@occurences += 1
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def ignored?
|
|
163
|
-
false
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def message_for_digest
|
|
167
|
-
# some gems like rest-client put data in warnings, need to aggregate
|
|
168
|
-
# + some bactrace per-worker unique method names may be there
|
|
169
|
-
@message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, '___').gsub(/\((pry|irb)\):\d+/, '(\1)')
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def digest
|
|
173
|
-
@digest ||= Digest::MD5.hexdigest(digest_base)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def digest_base
|
|
177
|
-
"1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def as_json(_options = {})
|
|
181
|
-
{
|
|
182
|
-
message: message,
|
|
183
|
-
realm: realm,
|
|
184
|
-
app_traceline: app_traceline,
|
|
185
|
-
gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
|
|
186
|
-
full_backtrace: full_backtrace,
|
|
187
|
-
ruby_version: RUBY_VERSION,
|
|
188
|
-
rails_version: Rails.version,
|
|
189
|
-
hostname: Socket.gethostname,
|
|
190
|
-
revision: DeprecationCollector.instance.app_revision,
|
|
191
|
-
count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
|
|
192
|
-
digest_base: digest_base # for debug purposes
|
|
193
|
-
}.compact
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
52
|
+
# NB: count is expensive in production env (but possible if needed) - produces a lot of redis writes
|
|
197
53
|
attr_accessor :count, :raise_on_deprecation, :save_full_backtrace,
|
|
198
54
|
:exclude_realms,
|
|
199
55
|
:write_interval, :write_interval_jitter,
|
|
200
56
|
:app_revision, :app_root
|
|
57
|
+
attr_writer :redis
|
|
201
58
|
|
|
202
|
-
def initialize(
|
|
203
|
-
@redis = redis
|
|
59
|
+
def initialize(mutex: nil)
|
|
204
60
|
# on cruby hash itself is threadsafe, but we need to prevent races
|
|
205
61
|
@deprecations_mutex = mutex || Mutex.new
|
|
206
62
|
@deprecations = {}
|
|
207
63
|
@known_digests = Set.new
|
|
208
64
|
@last_write_time = current_time
|
|
65
|
+
@enabled = true
|
|
66
|
+
|
|
67
|
+
load_default_config
|
|
68
|
+
end
|
|
209
69
|
|
|
210
|
-
|
|
70
|
+
def load_default_config
|
|
71
|
+
@redis = defined?($redis) && $redis # rubocop:disable Style/GlobalVars
|
|
211
72
|
@count = false
|
|
212
73
|
@raise_on_deprecation = false
|
|
213
74
|
@exclude_realms = []
|
|
214
|
-
@write_interval = FLUSH_INTERVAL
|
|
215
|
-
@write_interval_jitter = 60
|
|
216
|
-
@enabled = true
|
|
217
75
|
@ignore_message_regexp = nil
|
|
218
|
-
@app_root = defined?(Rails) && Rails.root.present? || Dir.pwd
|
|
219
|
-
|
|
220
|
-
|
|
76
|
+
@app_root = (defined?(Rails) && Rails.root.present? && Rails.root) || Dir.pwd
|
|
77
|
+
# NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
|
|
78
|
+
@write_interval = 900 # 15.minutes
|
|
79
|
+
@write_interval_jitter = 60
|
|
221
80
|
end
|
|
222
81
|
|
|
223
82
|
def ignored_messages=(val)
|
|
224
83
|
@ignore_message_regexp = (val && Regexp.union(val)) || nil
|
|
225
84
|
end
|
|
226
85
|
|
|
86
|
+
def app_root_prefix
|
|
87
|
+
"#{app_root}/"
|
|
88
|
+
end
|
|
89
|
+
|
|
227
90
|
def cleanup_prefixes
|
|
228
|
-
@cleanup_prefixes ||= Gem.path + [
|
|
91
|
+
@cleanup_prefixes ||= Gem.path + [app_root_prefix]
|
|
229
92
|
end
|
|
230
93
|
|
|
231
94
|
def collect(message, backtrace, realm = :unknown)
|
|
232
|
-
return
|
|
233
|
-
return if exclude_realms.include?(realm)
|
|
234
|
-
return if @ignore_message_regexp&.match?(message)
|
|
95
|
+
return if !@enabled || exclude_realms.include?(realm) || @ignore_message_regexp&.match?(message)
|
|
235
96
|
raise "Deprecation: #{message}" if @raise_on_deprecation
|
|
236
97
|
|
|
237
98
|
deprecation = Deprecation.new(message, realm, backtrace, cleanup_prefixes)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
@deprecations_mutex.synchronize do
|
|
241
|
-
(@deprecations[deprecation.digest] ||= deprecation).touch
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
|
|
245
|
-
|
|
246
|
-
$stderr.puts(message) if Rails.env.development? # rubocop:disable Style/StderrPuts
|
|
99
|
+
store_deprecation(deprecation)
|
|
100
|
+
log_deprecation_if_needed(deprecation)
|
|
247
101
|
end
|
|
248
102
|
|
|
249
103
|
def unsent_data?
|
|
@@ -254,40 +108,27 @@ class DeprecationCollector
|
|
|
254
108
|
@count
|
|
255
109
|
end
|
|
256
110
|
|
|
257
|
-
def
|
|
258
|
-
@
|
|
111
|
+
def redis
|
|
112
|
+
raise "DeprecationCollector#redis is not set" unless @redis
|
|
113
|
+
|
|
114
|
+
@redis
|
|
259
115
|
end
|
|
260
116
|
|
|
261
|
-
def write_to_redis(force: false)
|
|
262
|
-
return unless @enabled
|
|
117
|
+
def write_to_redis(force: false) # rubocop:disable Metrics/AbcSize
|
|
118
|
+
return unless force || (@enabled && (current_time > @last_write_time + @write_interval))
|
|
263
119
|
|
|
264
120
|
deprecations_to_flush = nil
|
|
265
121
|
@deprecations_mutex.synchronize do
|
|
266
|
-
# check in this section to prevent multiple check requests
|
|
267
|
-
unless enabled_in_redis?
|
|
268
|
-
@enabled = false
|
|
269
|
-
@deprecations = {}
|
|
270
|
-
return
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
return unless force || current_time > @last_write_time + @write_interval
|
|
274
|
-
|
|
275
122
|
deprecations_to_flush = @deprecations
|
|
276
123
|
@deprecations = {}
|
|
277
124
|
@last_write_time = current_time
|
|
125
|
+
# checking in this section to prevent multiple parallel check requests
|
|
126
|
+
return (@enabled = false) unless enabled_in_redis?
|
|
278
127
|
end
|
|
279
128
|
|
|
280
|
-
|
|
281
|
-
if count?
|
|
282
|
-
@redis.pipelined do
|
|
283
|
-
deprecations_to_flush.each_pair do |digest, deprecation|
|
|
284
|
-
@redis.hincrby("deprecations:counter", digest, deprecation.occurences)
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
129
|
+
write_count_to_redis(deprecations_to_flush) if count?
|
|
288
130
|
|
|
289
131
|
# make as few writes as possible, other workers may already have reported our warning
|
|
290
|
-
# TODO: at some point turn off writes?
|
|
291
132
|
fetch_known_digests
|
|
292
133
|
deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
|
|
293
134
|
return unless deprecations_to_flush.any?
|
|
@@ -296,6 +137,11 @@ class DeprecationCollector
|
|
|
296
137
|
@redis.mapped_hmset("deprecations:data", deprecations_to_flush.transform_values(&:to_json))
|
|
297
138
|
end
|
|
298
139
|
|
|
140
|
+
# prevent fresh process from wiring frequent already known messages
|
|
141
|
+
def fetch_known_digests
|
|
142
|
+
@known_digests.merge(@redis.hkeys("deprecations:data"))
|
|
143
|
+
end
|
|
144
|
+
|
|
299
145
|
def flush_redis(enable: false)
|
|
300
146
|
@redis.del("deprecations:data", "deprecations:counter", "deprecations:notes")
|
|
301
147
|
@redis.del("deprecations:enabled") if enable
|
|
@@ -339,49 +185,59 @@ class DeprecationCollector
|
|
|
339
185
|
def read_one(digest)
|
|
340
186
|
decode_deprecation(
|
|
341
187
|
digest,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
188
|
+
*@redis.pipelined do
|
|
189
|
+
@redis.hget("deprecations:data", digest)
|
|
190
|
+
@redis.hget("deprecations:counter", digest)
|
|
191
|
+
@redis.hget("deprecations:notes", digest)
|
|
192
|
+
end
|
|
345
193
|
)
|
|
346
194
|
end
|
|
347
195
|
|
|
348
196
|
def delete_deprecations(remove_digests)
|
|
197
|
+
return 0 unless remove_digests.any?
|
|
198
|
+
|
|
349
199
|
@redis.pipelined do
|
|
350
200
|
@redis.hdel("deprecations:data", *remove_digests)
|
|
351
201
|
@redis.hdel("deprecations:notes", *remove_digests)
|
|
352
202
|
@redis.hdel("deprecations:counter", *remove_digests) if @count
|
|
353
|
-
end
|
|
203
|
+
end.first
|
|
354
204
|
end
|
|
355
205
|
|
|
356
206
|
def cleanup
|
|
357
207
|
cursor = 0
|
|
358
|
-
removed = 0
|
|
359
|
-
total = 0
|
|
208
|
+
removed = total = 0
|
|
360
209
|
loop do
|
|
361
|
-
cursor, data_pairs = @redis.hscan("deprecations:data", cursor)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
data_pairs.each do |(digest, data)|
|
|
367
|
-
data = JSON.parse(data, symbolize_names: true)
|
|
368
|
-
remove_digests << digest if !block_given? || yield(data)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
if remove_digests.any?
|
|
372
|
-
delete_deprecations(remove_digests)
|
|
373
|
-
removed += remove_digests.size
|
|
374
|
-
end
|
|
375
|
-
end
|
|
210
|
+
cursor, data_pairs = @redis.hscan("deprecations:data", cursor) # NB: some pages may be empty
|
|
211
|
+
total += data_pairs.size
|
|
212
|
+
removed += delete_deprecations(
|
|
213
|
+
data_pairs.select { |_digest, data| !block_given? || yield(JSON.parse(data, symbolize_names: true)) }.keys
|
|
214
|
+
)
|
|
376
215
|
break if cursor == "0"
|
|
377
216
|
end
|
|
378
217
|
"#{removed} removed, #{total - removed} left"
|
|
379
218
|
end
|
|
380
219
|
|
|
381
|
-
|
|
220
|
+
protected
|
|
221
|
+
|
|
222
|
+
def store_deprecation(deprecation)
|
|
223
|
+
return if deprecation.ignored?
|
|
224
|
+
|
|
225
|
+
@deprecations_mutex.synchronize do
|
|
226
|
+
(@deprecations[deprecation.digest] ||= deprecation).touch
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def log_deprecation_if_needed(deprecation)
|
|
233
|
+
return unless defined?(Rails) && Rails.env.development? && !deprecation.ignored?
|
|
234
|
+
|
|
235
|
+
$stderr.puts(deprecation.message) # rubocop:disable Style/StderrPuts
|
|
236
|
+
end
|
|
382
237
|
|
|
383
238
|
def current_time
|
|
384
239
|
return Time.zone.now if Time.respond_to?(:zone) && Time.zone
|
|
240
|
+
|
|
385
241
|
Time.now
|
|
386
242
|
end
|
|
387
243
|
|
|
@@ -392,4 +248,12 @@ class DeprecationCollector
|
|
|
392
248
|
data[:count] = count.to_i if count
|
|
393
249
|
data
|
|
394
250
|
end
|
|
251
|
+
|
|
252
|
+
def write_count_to_redis(deprecations_to_flush)
|
|
253
|
+
@redis.pipelined do
|
|
254
|
+
deprecations_to_flush.each_pair do |digest, deprecation|
|
|
255
|
+
@redis.hincrby("deprecations:counter", digest, deprecation.occurences)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
395
259
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deprecation_collector
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vasily Fedoseyev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-08-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -43,6 +43,8 @@ files:
|
|
|
43
43
|
- Rakefile
|
|
44
44
|
- deprecation_collector.gemspec
|
|
45
45
|
- lib/deprecation_collector.rb
|
|
46
|
+
- lib/deprecation_collector/collectors.rb
|
|
47
|
+
- lib/deprecation_collector/deprecation.rb
|
|
46
48
|
- lib/deprecation_collector/version.rb
|
|
47
49
|
- sig/deprecation_collector.rbs
|
|
48
50
|
homepage: https://github.com/Vasfed/deprecation_collector
|