deprecation_collector 0.0.1 → 0.0.4
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 +9 -1
- data/Gemfile +5 -1
- data/Gemfile.lock +17 -2
- data/README.md +25 -7
- data/lib/deprecation_collector/collectors.rb +104 -0
- data/lib/deprecation_collector/deprecation.rb +91 -0
- data/lib/deprecation_collector/version.rb +1 -1
- data/lib/deprecation_collector.rb +94 -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: 2342f445e6c89ba4ea74094dd39894d99ad6e546781405fe43a6ecba71396264
|
4
|
+
data.tar.gz: d5d6edaa942f57a726f9336a06541dabc3eadae2731d01953443a88d84fc7c2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ac2e881d1687dc3756904840f21f98cc62dc497cac31804fcfeac8e4e01c780b5370497ed52da2d1ec22ca14a68ee3be4dc163c5d48cd47c38b775ef0df7b1c
|
7
|
+
data.tar.gz: 5725ea9137d79a83ea8d1df1ae9b458fe0f94479d5e7c9643ac15368ef853f75fac38cce3b7e8543fd4d03df884fa3999019d08902ba437a196f6b5b3c78dd06
|
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
@@ -1,5 +1,13 @@
|
|
1
|
+
== 0.0.4
|
2
|
+
- added first_timestamp to deprecations (unix timestamp of first occurrence, not accurate because a worker with later timestamp may dump its deprecations earlier)
|
1
3
|
|
4
|
+
== 0.0.3
|
5
|
+
- Fixed selective deprecation cleanup (`DeprecationCollector.instance.cleanup { |d| d[:message].include?('foo') }`)
|
6
|
+
|
7
|
+
== 0.0.2
|
8
|
+
|
9
|
+
- Reorganized code
|
2
10
|
|
3
11
|
== 0.0.1
|
4
12
|
|
5
|
-
Initial release
|
13
|
+
- Initial release
|
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.4)
|
5
5
|
redis (>= 2.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -123,7 +123,7 @@ GEM
|
|
123
123
|
thor (>= 0.20.3, < 2.0)
|
124
124
|
rainbow (3.1.1)
|
125
125
|
rake (13.0.6)
|
126
|
-
redis (4.
|
126
|
+
redis (4.7.1)
|
127
127
|
regexp_parser (2.5.0)
|
128
128
|
rexml (3.2.5)
|
129
129
|
rspec (3.11.0)
|
@@ -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,21 +1,39 @@
|
|
1
1
|
# DeprecationCollector
|
2
|
+
[](https://badge.fury.io/rb/deprecation_collector)
|
2
3
|
|
3
4
|
Collects ruby and rails deprecation warnings.
|
5
|
+
Designed to be suitable for use in production under load.
|
6
|
+
|
4
7
|
(gem is a work-in-process, documentation will come later)
|
5
8
|
|
6
9
|
## Installation
|
7
10
|
|
8
11
|
Install the gem and add to the application's Gemfile by executing:
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
$ gem install deprecation_collector
|
13
|
+
```sh
|
14
|
+
bundle add deprecation_collector
|
15
|
+
```
|
15
16
|
|
16
17
|
## Usage
|
17
18
|
|
18
|
-
|
19
|
+
Add an initializer with configuration, like
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
Rails.application.config.to_prepare do
|
23
|
+
DeprecationCollector.install do |instance|
|
24
|
+
instance.redis = Redis.new # default is $redis
|
25
|
+
instance.app_revision = ::GIT_REVISION
|
26
|
+
instance.count = false
|
27
|
+
instance.save_full_backtrace = true
|
28
|
+
instance.raise_on_deprecation = false
|
29
|
+
instance.write_interval = (::Rails.env.production? && 15.minutes) || 1.minute
|
30
|
+
instance.exclude_realms = %i[kernel] if Rails.env.production?
|
31
|
+
instance.ignored_messages = [
|
32
|
+
"Ignoring db/schema_cache.yml because it has expired"
|
33
|
+
]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
19
37
|
|
20
38
|
## Development
|
21
39
|
|
@@ -25,7 +43,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
25
43
|
|
26
44
|
## Contributing
|
27
45
|
|
28
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
46
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Vasfed/deprecation_collector.
|
29
47
|
|
30
48
|
## License
|
31
49
|
|
@@ -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,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DeprecationCollector
|
4
|
+
# :nodoc:
|
5
|
+
class Deprecation
|
6
|
+
attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :first_timestamp, :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
|
+
@first_timestamp = Time.now.to_i
|
24
|
+
|
25
|
+
cleanup_prefixes.each do |path|
|
26
|
+
@gem_traceline.delete_prefix!(path)
|
27
|
+
@message.gsub!(path, "")
|
28
|
+
end
|
29
|
+
|
30
|
+
CLEANUP_REGEXES.each_pair do |regex, replace|
|
31
|
+
@gem_traceline&.gsub!(regex, replace)
|
32
|
+
@app_traceline&.gsub!(regex, replace)
|
33
|
+
end
|
34
|
+
|
35
|
+
@full_backtrace = backtrace.map(&:to_s) if DeprecationCollector.instance.save_full_backtrace
|
36
|
+
end
|
37
|
+
|
38
|
+
def touch
|
39
|
+
@occurences += 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def ignored?
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def message_for_digest
|
47
|
+
# some gems like rest-client put data in warnings, need to aggregate
|
48
|
+
# + some bactrace per-worker unique method names may be there
|
49
|
+
@message.gsub(/"(?:[^"\\]|\\.)*"/, '""').gsub(/__\d+_\d+/, "___").gsub(/\((pry|irb)\):\d+/, '(\1)')
|
50
|
+
end
|
51
|
+
|
52
|
+
def digest
|
53
|
+
@digest ||= Digest::MD5.hexdigest(digest_base)
|
54
|
+
end
|
55
|
+
|
56
|
+
def digest_base
|
57
|
+
"1:#{RUBY_VERSION}:#{Rails.version}:#{message_for_digest}:#{gem_traceline}:#{app_traceline}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_json(_options = {})
|
61
|
+
{
|
62
|
+
message: message,
|
63
|
+
realm: realm,
|
64
|
+
app_traceline: app_traceline,
|
65
|
+
gem_traceline: (gem_traceline != app_traceline && gem_traceline) || nil,
|
66
|
+
full_backtrace: full_backtrace,
|
67
|
+
ruby_version: RUBY_VERSION,
|
68
|
+
rails_version: (defined?(Rails) && Rails.version),
|
69
|
+
hostname: Socket.gethostname,
|
70
|
+
revision: DeprecationCollector.instance.app_revision,
|
71
|
+
count: @occurences, # output anyway for frequency estimation (during write_interval inside single process)
|
72
|
+
first_timestamp: first_timestamp, # this may not be accurate, a worker with later timestamp may dump earlier
|
73
|
+
digest_base: digest_base # for debug purposes
|
74
|
+
}.compact
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def find_app_traceline(backtrace)
|
80
|
+
app_root = DeprecationCollector.instance.app_root_prefix
|
81
|
+
backtrace.find do |line|
|
82
|
+
line = line.to_s
|
83
|
+
(!line.start_with?("/") || line.start_with?(app_root)) && !line.include?("/gems/")
|
84
|
+
end&.to_s&.dup&.delete_prefix(app_root)
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_gem_traceline(backtrace)
|
88
|
+
backtrace.find { |line| !line.to_s.include?("kernel_warn") }&.to_s&.dup || backtrace.first.to_s.dup
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,13 +1,14 @@
|
|
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"
|
8
|
+
require "json"
|
6
9
|
|
10
|
+
# singleton class for collector
|
7
11
|
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
12
|
@instance_mutex = Mutex.new
|
12
13
|
@installed = false
|
13
14
|
private_class_method :new
|
@@ -15,9 +16,13 @@ class DeprecationCollector
|
|
15
16
|
def self.instance
|
16
17
|
return @instance if defined?(@instance) && @instance
|
17
18
|
|
19
|
+
create_instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create_instance
|
18
23
|
@instance_mutex.synchronize do
|
19
|
-
#
|
20
|
-
@instance ||= new(
|
24
|
+
# no real need to reuse the mutex, but it is used only once here anyway
|
25
|
+
@instance ||= new(mutex: @instance_mutex)
|
21
26
|
end
|
22
27
|
@instance
|
23
28
|
end
|
@@ -28,7 +33,7 @@ class DeprecationCollector
|
|
28
33
|
|
29
34
|
# inside dev env may be called multiple times
|
30
35
|
def self.install
|
31
|
-
|
36
|
+
create_instance # to make it created, configuration comes later
|
32
37
|
|
33
38
|
@instance_mutex.synchronize do
|
34
39
|
unless @installed
|
@@ -37,213 +42,63 @@ class DeprecationCollector
|
|
37
42
|
end
|
38
43
|
|
39
44
|
yield instance if block_given?
|
45
|
+
instance.fetch_known_digests
|
40
46
|
|
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
|
47
|
+
install_collectors
|
63
48
|
end
|
64
49
|
|
65
50
|
@instance
|
66
51
|
end
|
67
52
|
|
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
|
-
|
53
|
+
# NB: count is expensive in production env (but possible if needed) - produces a lot of redis writes
|
197
54
|
attr_accessor :count, :raise_on_deprecation, :save_full_backtrace,
|
198
55
|
:exclude_realms,
|
199
56
|
:write_interval, :write_interval_jitter,
|
200
57
|
:app_revision, :app_root
|
58
|
+
attr_writer :redis
|
201
59
|
|
202
|
-
def initialize(
|
203
|
-
@redis = redis
|
60
|
+
def initialize(mutex: nil)
|
204
61
|
# on cruby hash itself is threadsafe, but we need to prevent races
|
205
62
|
@deprecations_mutex = mutex || Mutex.new
|
206
63
|
@deprecations = {}
|
207
64
|
@known_digests = Set.new
|
208
65
|
@last_write_time = current_time
|
66
|
+
@enabled = true
|
209
67
|
|
210
|
-
|
68
|
+
load_default_config
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_default_config
|
72
|
+
@redis = defined?($redis) && $redis # rubocop:disable Style/GlobalVars
|
211
73
|
@count = false
|
212
74
|
@raise_on_deprecation = false
|
213
75
|
@exclude_realms = []
|
214
|
-
@write_interval = FLUSH_INTERVAL
|
215
|
-
@write_interval_jitter = 60
|
216
|
-
@enabled = true
|
217
76
|
@ignore_message_regexp = nil
|
218
|
-
@app_root = defined?(Rails) && Rails.root.present? || Dir.pwd
|
219
|
-
|
220
|
-
|
77
|
+
@app_root = (defined?(Rails) && Rails.root.present? && Rails.root) || Dir.pwd
|
78
|
+
# NB: in production with hugreds of workers may easily overload redis with writes, so more delay needed:
|
79
|
+
@write_interval = 900 # 15.minutes
|
80
|
+
@write_interval_jitter = 60
|
221
81
|
end
|
222
82
|
|
223
83
|
def ignored_messages=(val)
|
224
84
|
@ignore_message_regexp = (val && Regexp.union(val)) || nil
|
225
85
|
end
|
226
86
|
|
87
|
+
def app_root_prefix
|
88
|
+
"#{app_root}/"
|
89
|
+
end
|
90
|
+
|
227
91
|
def cleanup_prefixes
|
228
|
-
@cleanup_prefixes ||= Gem.path + [
|
92
|
+
@cleanup_prefixes ||= Gem.path + [app_root_prefix]
|
229
93
|
end
|
230
94
|
|
231
95
|
def collect(message, backtrace, realm = :unknown)
|
232
|
-
return
|
233
|
-
return if exclude_realms.include?(realm)
|
234
|
-
return if @ignore_message_regexp&.match?(message)
|
96
|
+
return if !@enabled || exclude_realms.include?(realm) || @ignore_message_regexp&.match?(message)
|
235
97
|
raise "Deprecation: #{message}" if @raise_on_deprecation
|
236
98
|
|
237
99
|
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
|
100
|
+
store_deprecation(deprecation)
|
101
|
+
log_deprecation_if_needed(deprecation)
|
247
102
|
end
|
248
103
|
|
249
104
|
def unsent_data?
|
@@ -254,40 +109,27 @@ class DeprecationCollector
|
|
254
109
|
@count
|
255
110
|
end
|
256
111
|
|
257
|
-
def
|
258
|
-
@
|
112
|
+
def redis
|
113
|
+
raise "DeprecationCollector#redis is not set" unless @redis
|
114
|
+
|
115
|
+
@redis
|
259
116
|
end
|
260
117
|
|
261
|
-
def write_to_redis(force: false)
|
262
|
-
return unless @enabled
|
118
|
+
def write_to_redis(force: false) # rubocop:disable Metrics/AbcSize
|
119
|
+
return unless force || (@enabled && (current_time > @last_write_time + @write_interval))
|
263
120
|
|
264
121
|
deprecations_to_flush = nil
|
265
122
|
@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
123
|
deprecations_to_flush = @deprecations
|
276
124
|
@deprecations = {}
|
277
125
|
@last_write_time = current_time
|
126
|
+
# checking in this section to prevent multiple parallel check requests
|
127
|
+
return (@enabled = false) unless enabled_in_redis?
|
278
128
|
end
|
279
129
|
|
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
|
130
|
+
write_count_to_redis(deprecations_to_flush) if count?
|
288
131
|
|
289
132
|
# make as few writes as possible, other workers may already have reported our warning
|
290
|
-
# TODO: at some point turn off writes?
|
291
133
|
fetch_known_digests
|
292
134
|
deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
|
293
135
|
return unless deprecations_to_flush.any?
|
@@ -296,6 +138,11 @@ class DeprecationCollector
|
|
296
138
|
@redis.mapped_hmset("deprecations:data", deprecations_to_flush.transform_values(&:to_json))
|
297
139
|
end
|
298
140
|
|
141
|
+
# prevent fresh process from wiring frequent already known messages
|
142
|
+
def fetch_known_digests
|
143
|
+
@known_digests.merge(@redis.hkeys("deprecations:data"))
|
144
|
+
end
|
145
|
+
|
299
146
|
def flush_redis(enable: false)
|
300
147
|
@redis.del("deprecations:data", "deprecations:counter", "deprecations:notes")
|
301
148
|
@redis.del("deprecations:enabled") if enable
|
@@ -317,6 +164,10 @@ class DeprecationCollector
|
|
317
164
|
@redis.set("deprecations:enabled", "false")
|
318
165
|
end
|
319
166
|
|
167
|
+
def dump
|
168
|
+
read_each.to_a.to_json
|
169
|
+
end
|
170
|
+
|
320
171
|
def read_each
|
321
172
|
return to_enum(:read_each) unless block_given?
|
322
173
|
|
@@ -339,49 +190,59 @@ class DeprecationCollector
|
|
339
190
|
def read_one(digest)
|
340
191
|
decode_deprecation(
|
341
192
|
digest,
|
342
|
-
|
343
|
-
|
344
|
-
|
193
|
+
*@redis.pipelined do
|
194
|
+
@redis.hget("deprecations:data", digest)
|
195
|
+
@redis.hget("deprecations:counter", digest)
|
196
|
+
@redis.hget("deprecations:notes", digest)
|
197
|
+
end
|
345
198
|
)
|
346
199
|
end
|
347
200
|
|
348
201
|
def delete_deprecations(remove_digests)
|
202
|
+
return 0 unless remove_digests.any?
|
203
|
+
|
349
204
|
@redis.pipelined do
|
350
205
|
@redis.hdel("deprecations:data", *remove_digests)
|
351
206
|
@redis.hdel("deprecations:notes", *remove_digests)
|
352
207
|
@redis.hdel("deprecations:counter", *remove_digests) if @count
|
353
|
-
end
|
208
|
+
end.first
|
354
209
|
end
|
355
210
|
|
356
211
|
def cleanup
|
357
212
|
cursor = 0
|
358
|
-
removed = 0
|
359
|
-
total = 0
|
213
|
+
removed = total = 0
|
360
214
|
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
|
215
|
+
cursor, data_pairs = @redis.hscan("deprecations:data", cursor) # NB: some pages may be empty
|
216
|
+
total += data_pairs.size
|
217
|
+
removed += delete_deprecations(
|
218
|
+
data_pairs.to_h.select { |_digest, data| !block_given? || yield(JSON.parse(data, symbolize_names: true)) }.keys
|
219
|
+
)
|
376
220
|
break if cursor == "0"
|
377
221
|
end
|
378
222
|
"#{removed} removed, #{total - removed} left"
|
379
223
|
end
|
380
224
|
|
381
|
-
|
225
|
+
protected
|
226
|
+
|
227
|
+
def store_deprecation(deprecation)
|
228
|
+
return if deprecation.ignored?
|
229
|
+
|
230
|
+
@deprecations_mutex.synchronize do
|
231
|
+
(@deprecations[deprecation.digest] ||= deprecation).touch
|
232
|
+
end
|
233
|
+
|
234
|
+
write_to_redis if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
|
235
|
+
end
|
236
|
+
|
237
|
+
def log_deprecation_if_needed(deprecation)
|
238
|
+
return unless defined?(Rails) && Rails.env.development? && !deprecation.ignored?
|
239
|
+
|
240
|
+
$stderr.puts(deprecation.message) # rubocop:disable Style/StderrPuts
|
241
|
+
end
|
382
242
|
|
383
243
|
def current_time
|
384
244
|
return Time.zone.now if Time.respond_to?(:zone) && Time.zone
|
245
|
+
|
385
246
|
Time.now
|
386
247
|
end
|
387
248
|
|
@@ -392,4 +253,12 @@ class DeprecationCollector
|
|
392
253
|
data[:count] = count.to_i if count
|
393
254
|
data
|
394
255
|
end
|
256
|
+
|
257
|
+
def write_count_to_redis(deprecations_to_flush)
|
258
|
+
@redis.pipelined do
|
259
|
+
deprecations_to_flush.each_pair do |digest, deprecation|
|
260
|
+
@redis.hincrby("deprecations:counter", digest, deprecation.occurences)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
395
264
|
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.4
|
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-29 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
|