deprecation_collector 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7936a3e78d550c3940f0abe364171df3b39cc1e19931cfba23b8cfdb060122ca
4
- data.tar.gz: 2a4d06a0baee48c61c027c7791fef7894b5af5d1791fe0e3c1bd3afaf2fa61ab
3
+ metadata.gz: 2342f445e6c89ba4ea74094dd39894d99ad6e546781405fe43a6ecba71396264
4
+ data.tar.gz: d5d6edaa942f57a726f9336a06541dabc3eadae2731d01953443a88d84fc7c2e
5
5
  SHA512:
6
- metadata.gz: c5a10c07173fe8b73003630c2fa0839b07c62f0471b72269aabdcece19935f6693fc6b0a3a3c8634b1731b7cc9db0aae186ff2067a43047692e15f32180d5210
7
- data.tar.gz: b6e598c579eb46756b9a78336fd3b223d4224b01f5a677b55375732279564c9b609dc2e87083a6cb47cb5e86c567c73102465080e275818564d94c1d29b29408
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
@@ -11,6 +11,10 @@ gem "rspec", "~> 3.0"
11
11
  gem "timecop"
12
12
 
13
13
  gem "rubocop", "~> 1.21"
14
+ gem "rubocop-performance"
15
+ gem "rubocop-rails"
16
+ gem "rubocop-rake"
17
+ gem "rubocop-rspec"
14
18
 
15
19
  # TODO: appraisals
16
- gem "rails", '6.0'
20
+ gem "rails", "6.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deprecation_collector (0.0.1)
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.6.0)
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
+ [![Gem Version](https://badge.fury.io/rb/deprecation_collector.svg)](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
- $ bundle add deprecation_collector
11
-
12
- If bundler is not being used to manage dependencies, install the gem by executing:
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/[USERNAME]/deprecation_collector.
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeprecationCollector
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.4"
5
5
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "deprecation_collector/version"
4
- require 'time'
5
- require 'redis'
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($redis, mutex: @instance_mutex)
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
- instance # to make it created, configuration comes later
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
- # TODO: a more polite hook
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
- module MultipartWarningJoiner
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(redis, mutex: nil)
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
- # default config:
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
- fetch_known_digests # prevent fresh process from wiring frequent already known messages
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 + ["#{app_root}/"] # rubocop:disable Rails/FilePath
92
+ @cleanup_prefixes ||= Gem.path + [app_root_prefix]
229
93
  end
230
94
 
231
95
  def collect(message, backtrace, realm = :unknown)
232
- return unless @enabled
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
- return if deprecation.ignored?
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 fetch_known_digests
258
- @known_digests.merge(@redis.hkeys('deprecations:data'))
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 || force
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
- # count is expensive in production env (but possible if needed) - a lot of redis writes
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
- @redis.hget("deprecations:data", digest),
343
- @redis.hget("deprecations:counter", digest),
344
- @redis.hget("deprecations:notes", digest)
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
- if data_pairs.any?
364
- remove_digests = []
365
- total += data_pairs.size
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
- private
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.1
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-05-29 00:00:00.000000000 Z
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