deprecation_collector 0.4.0 → 0.5.1

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: 7eda71f5ba3b7d7c1aa9a1c622a29e4329b8ab08bccc53d243aabea3a80a70cb
4
- data.tar.gz: 213fa97a6ceede04c3f37160defb2a6732d84a32f25681a9486a567eae6d7fed
3
+ metadata.gz: 60dd6c4c552576ae19c4cb2a84232c5f7e6b83b0d8fb8e11b3a98bc5abf69bdf
4
+ data.tar.gz: f9f638c079489229bed7e525caaed0fb995b19ceff250146644d405462739e39
5
5
  SHA512:
6
- metadata.gz: d4b4cf6feb4d736114f7324a0b34a65e32ee7f3edd610ee5aabb41daa069fd2417a746e927761c28ec22b37a9ceb1f768c2fa92064c11e0d4e4474801dd9f008
7
- data.tar.gz: 9e6641fc8b17e5d4d2a5d5d7981afecca83feffbc549d88206105a4f752feeb951290fa9dc77f8d24166519df12da917d83f81685eb2d79b6ca87912d09ff599
6
+ metadata.gz: b80b9bcfdbbccaf112b447f3ab04960f042ce98c74b53b35b76e9c4f3740a37fa1cc894ce632e7206c76bcb7eaaf8e13f596bcaa53398eb611fcd245a8b77214
7
+ data.tar.gz: 35992cd452d01b83a5884299873bd2d80c2e5d42f8977d77b72e0635f24d5bda475200eed936e74102181205575d62e511085606af3868b059377780e9efc8e1
data/.rubocop.yml CHANGED
@@ -10,6 +10,7 @@ AllCops:
10
10
  SuggestExtensions: false
11
11
  Exclude:
12
12
  - gemfiles/*
13
+ - lib/deprecation_collector/web/views/*.template.rb
13
14
 
14
15
  Style/StringLiterals:
15
16
  Enabled: true
@@ -30,3 +31,4 @@ Metrics/PerceivedComplexity: { Max: 9 }
30
31
 
31
32
  RSpec/ExampleLength: { Enabled: false }
32
33
  RSpec/MultipleExpectations: { Enabled: false }
34
+ RSpec/MessageSpies: { Enabled: false }
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ == 0.5.0
2
+ - more work on ui
3
+ - refactored to separate deprecations storage from other logic
4
+ - when redis is not provided - print all messages to stderr
5
+ - added `key_prefix` option (default `'deprecations'`, location may change in the future) to allow multiple independent apps to write to one redis
6
+ - added `app_name` option to record app name as separate field
7
+
1
8
  == 0.4.0
2
9
  - a bit better ui
3
10
  - simple import/export
data/Gemfile CHANGED
@@ -20,14 +20,14 @@ unless defined?(Appraisal)
20
20
  end
21
21
 
22
22
  gem "rails", "~>6.0.0"
23
- gem 'simplecov'
23
+ gem "simplecov"
24
24
  end
25
25
 
26
26
  gem "fakeredis"
27
27
  gem "redis", "~>4.8"
28
28
 
29
29
  # for web tests
30
- gem 'rack'
31
- gem 'webrick'
30
+ gem "rack"
31
+ gem "webrick"
32
32
 
33
- gem 'slim' # not used in production, for compiling templates
33
+ gem "slim" # not used in production, for compiling templates
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
data/Rakefile CHANGED
@@ -8,16 +8,17 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  begin
9
9
  require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
- rescue LoadError
11
+ rescue LoadError # rubocop:disable Lint/SuppressedException
12
12
  end
13
13
 
14
14
  task default: %i[spec rubocop]
15
15
 
16
+ desc "Compile slim templates (so that slim is not needed as dependency)"
16
17
  task :precompile_templates do
17
- require 'slim'
18
+ require "slim"
18
19
  # Slim::Template.new { '.lala' }.precompiled_template
19
- Dir['lib/deprecation_collector/web/views/*.slim'].each do |file|
20
- target = file.sub(/\.slim\z/, '.template.rb')
20
+ Dir["lib/deprecation_collector/web/views/*.slim"].each do |file|
21
+ target = file.sub(/\.slim\z/, ".template.rb")
21
22
  puts "Compiling #{file} -> #{target}"
22
23
  content = Slim::Template.new(file).precompiled_template # maybe send(:precompiled, []) is more correct
23
24
 
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) ||
26
26
  f.match(%r{\Alib/deprecation_collector/web/views/.+\.slim\z})
27
27
  end
28
- end + Dir['lib/deprecation_collector/web/views/*.slim'].map { |template| template.sub(/\.slim\z/, '.template.rb') }
28
+ end + Dir["lib/deprecation_collector/web/views/*.slim"].map { |template| template.sub(/\.slim\z/, ".template.rb") }
29
29
  spec.require_paths = ["lib"]
30
30
 
31
31
  spec.add_dependency "redis", ">= 3.0"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- deprecation_collector (0.4.0)
4
+ deprecation_collector (0.5.1)
5
5
  redis (>= 3.0)
6
6
 
7
7
  GEM
@@ -4,7 +4,7 @@ class DeprecationCollector
4
4
  # :nodoc:
5
5
  class Deprecation
6
6
  attr_reader :message, :realm, :gem_traceline, :app_traceline, :occurences, :first_timestamp, :full_backtrace
7
- attr_accessor :context, :custom_fingerprint
7
+ attr_accessor :context, :custom_fingerprint, :app_name
8
8
 
9
9
  CLEANUP_REGEXES = {
10
10
  # rails views generated methods names are unique per-worker
@@ -61,6 +61,7 @@ class DeprecationCollector
61
61
 
62
62
  def as_json(_options = {})
63
63
  {
64
+ app: app_name,
64
65
  message: message,
65
66
  realm: realm,
66
67
  app_traceline: app_traceline,
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ class DeprecationCollector
6
+ module Storage
7
+ # :nodoc:
8
+ class Base
9
+ # rubocop:disable Style/SingleLineMethods
10
+ def initialize(**); end
11
+ def support_disabling?; false; end
12
+ def enabled?; true; end
13
+ def enable; end
14
+ def disable; end
15
+
16
+ def unsent_deprecations; []; end
17
+ def fetch_known_digests; end
18
+
19
+ def delete(digests); end
20
+ def clear(enable: false); end
21
+ def flush(**); end
22
+
23
+ def store(_deprecation); raise("Not implemented"); end
24
+ # rubocop:enable Style/SingleLineMethods
25
+ end
26
+
27
+ # dummy strategy that outputs every deprecation into stderr
28
+ class StdErr < Base
29
+ def store(deprecation)
30
+ DeprecationCollector.instance.send(:log_deprecation, deprecation)
31
+ end
32
+ end
33
+
34
+ # storing in redis with deduplication by fingerprint
35
+ class Redis < Base
36
+ attr_accessor :write_interval, :write_interval_jitter, :redis, :count
37
+
38
+ def initialize(redis: nil, mutex: nil, count: false, write_interval: 900, write_interval_jitter: 60,
39
+ key_prefix: nil)
40
+ super
41
+ @key_prefix = key_prefix || "deprecations"
42
+ @redis = redis
43
+ @last_write_time = current_time
44
+ @count = count
45
+ @write_interval = write_interval
46
+ @write_interval_jitter = write_interval_jitter
47
+ # on cruby hash itself is threadsafe, but we need to prevent races
48
+ @deprecations_mutex = mutex || Mutex.new
49
+ @deprecations = {}
50
+ @known_digests = Set.new
51
+ end
52
+
53
+ def support_disabling?
54
+ true
55
+ end
56
+
57
+ def unsent_deprecations
58
+ @deprecations
59
+ end
60
+
61
+ def enabled?
62
+ @redis.get(enabled_flag_key) != "false"
63
+ end
64
+
65
+ def enable
66
+ @redis.set(enabled_flag_key, "true")
67
+ end
68
+
69
+ def disable
70
+ @redis.set(enabled_flag_key, "false")
71
+ end
72
+
73
+ def delete(remove_digests)
74
+ return 0 unless remove_digests.any?
75
+
76
+ @redis.pipelined do |pipe|
77
+ pipe.hdel(data_hash_key, *remove_digests)
78
+ pipe.hdel(notes_hash_key, *remove_digests)
79
+ pipe.hdel(counter_hash_key, *remove_digests) if @count
80
+ end.first
81
+ end
82
+
83
+ def clear(enable: false)
84
+ @redis.del(data_hash_key, counter_hash_key, notes_hash_key)
85
+ @redis.del(enabled_flag_key) if enable
86
+ @known_digests.clear
87
+ @deprecations.clear
88
+ end
89
+
90
+ def fetch_known_digests
91
+ # FIXME: use `.merge!`?
92
+ @known_digests.merge(@redis.hkeys(data_hash_key))
93
+ end
94
+
95
+ def store(deprecation)
96
+ fresh = !@deprecations.key?(deprecation.digest)
97
+ @deprecations_mutex.synchronize do
98
+ (@deprecations[deprecation.digest] ||= deprecation).touch
99
+ end
100
+
101
+ flush if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
102
+ fresh
103
+ end
104
+
105
+ def flush(force: false)
106
+ return unless force || (current_time > @last_write_time + @write_interval)
107
+
108
+ deprecations_to_flush = nil
109
+ @deprecations_mutex.synchronize do
110
+ deprecations_to_flush = @deprecations
111
+ @deprecations = {}
112
+ @last_write_time = current_time
113
+ # checking in this section to prevent multiple parallel check requests
114
+ return DeprecationCollector.instance.instance_variable_set(:@enabled, false) unless enabled?
115
+ end
116
+
117
+ write_count_to_redis(deprecations_to_flush) if @count
118
+
119
+ # make as few writes as possible, other workers may already have reported our warning
120
+ fetch_known_digests
121
+ deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
122
+ return unless deprecations_to_flush.any?
123
+
124
+ @known_digests.merge(deprecations_to_flush.keys)
125
+ @redis.mapped_hmset(data_hash_key, deprecations_to_flush.transform_values(&:to_json))
126
+ end
127
+
128
+ def read_each
129
+ cursor = 0
130
+ loop do
131
+ cursor, data_pairs = @redis.hscan(data_hash_key, cursor)
132
+
133
+ if data_pairs.any?
134
+ data_pairs.zip(
135
+ @redis.hmget(counter_hash_key, data_pairs.map(&:first)),
136
+ @redis.hmget(notes_hash_key, data_pairs.map(&:first))
137
+ ).each do |(digest, data), count, notes|
138
+ yield(digest, data, count, notes)
139
+ end
140
+ end
141
+ break if cursor == "0"
142
+ end
143
+ end
144
+
145
+ def read_one(digest)
146
+ [
147
+ digest,
148
+ *@redis.pipelined do |pipe|
149
+ pipe.hget(data_hash_key, digest)
150
+ pipe.hget(counter_hash_key, digest)
151
+ pipe.hget(notes_hash_key, digest)
152
+ end
153
+ ]
154
+ end
155
+
156
+ def import(dump_hash)
157
+ @redis.mapped_hmset(data_hash_key, dump_hash.transform_values(&:to_json))
158
+ end
159
+
160
+ def cleanup(&_block)
161
+ cursor = 0
162
+ removed = total = 0
163
+ loop do
164
+ cursor, data_pairs = @redis.hscan(data_hash_key, cursor) # NB: some pages may be empty
165
+ total += data_pairs.size
166
+ removed += delete(
167
+ data_pairs.to_h.select { |_digest, data| yield(JSON.parse(data, symbolize_names: true)) }.keys
168
+ )
169
+ break if cursor == "0"
170
+ end
171
+ "#{removed} removed, #{total - removed} left"
172
+ end
173
+
174
+ def key_prefix=(val)
175
+ @enabled_flag_key = @data_hash_key = @counter_hash_key = @notes_hash_key = nil
176
+ @key_prefix = val
177
+ end
178
+
179
+ protected
180
+
181
+ def enabled_flag_key
182
+ @enabled_flag_key ||= "#{@key_prefix}:enabled" # usually deprecations:enabled
183
+ end
184
+
185
+ def data_hash_key
186
+ @data_hash_key ||= "#{@key_prefix}:data" # usually deprecations:data
187
+ end
188
+
189
+ def counter_hash_key
190
+ @counter_hash_key ||= "#{@key_prefix}:counter" # usually deprecations:counter
191
+ end
192
+
193
+ def notes_hash_key
194
+ @notes_hash_key ||= "#{@key_prefix}:notes" # usually deprecations:notes
195
+ end
196
+
197
+ def current_time
198
+ return Time.zone.now if Time.respond_to?(:zone) && Time.zone
199
+
200
+ Time.now
201
+ end
202
+
203
+ def write_count_to_redis(deprecations_to_flush)
204
+ @redis.pipelined do |pipe|
205
+ deprecations_to_flush.each_pair do |digest, deprecation|
206
+ pipe.hincrby(counter_hash_key, digest, deprecation.occurences)
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeprecationCollector
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -1,22 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'router'
4
- require_relative 'helpers'
5
- require 'pp'
3
+ require_relative "router"
4
+ require_relative "helpers"
6
5
 
7
6
  class DeprecationCollector
8
7
  class Web
8
+ # :nodoc:
9
9
  class Application
10
10
  extend Web::Router
11
11
  helpers Helpers
12
12
 
13
13
  attr_reader :web
14
+
14
15
  def initialize(web)
15
16
  @web = web
16
- unless defined?(Temple::Utils) || ENV['DEPRECATION_COLLECTOR_RELOAD_WEB_TEMPLATES']
17
- # used for escaping in compiled slim templates
18
- require_relative 'utils'
19
- end
17
+ # used for escaping in compiled slim templates
18
+ require_relative "utils" unless defined?(Temple::Utils) || ENV["DEPRECATION_COLLECTOR_RELOAD_WEB_TEMPLATES"]
20
19
  end
21
20
 
22
21
  def call(env)
@@ -25,7 +24,7 @@ class DeprecationCollector
25
24
 
26
25
  root do # index
27
26
  @deprecations = collector_instance.read_each.to_a.compact
28
- @deprecations = @deprecations.sort_by { |dep| dep[:message] } unless params[:sort] == '0'
27
+ @deprecations = @deprecations.sort_by { |dep| dep[:message] } unless params[:sort] == "0"
29
28
 
30
29
  if params[:reject]
31
30
  @deprecations = @deprecations.reject { |dep| dep[:message].match?(Regexp.union(Array(params[:reject]))) }
@@ -38,50 +37,59 @@ class DeprecationCollector
38
37
  render slim: "index.html"
39
38
  end
40
39
 
41
- get '/dump.json' do
40
+ get "/dump.json" do
42
41
  render json: collector_instance.dump
43
42
  end
44
43
 
45
- get '/import' do
44
+ get "/import" do
46
45
  return "Import not enabled" unless import_enabled?
47
46
 
48
47
  render slim: "import.html"
49
48
  end
50
49
 
51
- post '/import' do
52
- halt 422, "need multipart json file" unless env['CONTENT_TYPE']&.start_with?('multipart/form-data') && params.dig(:file, :tempfile)
50
+ post "/import" do
51
+ unless env["CONTENT_TYPE"]&.start_with?("multipart/form-data") && params.dig(:file, :tempfile)
52
+ halt 422, "need multipart json file"
53
+ end
53
54
  collector_instance.import_dump(File.read(params[:file][:tempfile]))
54
55
  redirect_to deprecations_path
55
56
  end
56
57
 
57
- get '/:id' do # show
58
+ get "/:id.json" do # show
59
+ @deprecation = collector_instance.read_one(params[:id])
60
+ halt 404 unless @deprecation
61
+ render json: JSON.pretty_generate(@deprecation)
62
+ end
63
+
64
+ get "/:id" do # show
58
65
  @deprecation = collector_instance.read_one(params[:id])
66
+ halt 404 unless @deprecation
59
67
  render slim: "show.html"
60
68
  end
61
69
 
62
- delete '/all' do
70
+ delete "/all" do
63
71
  collector_instance.flush_redis
64
72
  redirect_to deprecations_path
65
73
  end
66
74
 
67
- post '/enable' do
75
+ post "/enable" do
68
76
  collector_instance.enable
69
77
  redirect_to deprecations_path
70
78
  end
71
79
 
72
- delete '/disable' do
80
+ delete "/disable" do
73
81
  collector_instance.disable
74
82
  redirect_to deprecations_path
75
83
  end
76
84
 
77
85
  # NB: order for wildcards is important
78
- delete '/:id' do # destroy
86
+ delete "/:id" do # destroy
79
87
  collector_instance.delete_deprecations([params[:id]])
80
88
  redirect_to deprecations_path
81
89
  end
82
90
 
83
- post '/trigger' do # trigger
84
- trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?('2.7')
91
+ post "/trigger" do # trigger
92
+ trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?("2.7")
85
93
  trigger_rails_deprecation
86
94
  collector_instance.collect(
87
95
  "TestFoo#assign_attributes called (test attr_spy) trigger_rails_deprecation", caller_locations, :attr_spy
@@ -2,6 +2,7 @@
2
2
 
3
3
  class DeprecationCollector
4
4
  class Web
5
+ # :nodoc:
5
6
  module Helpers
6
7
  def collector_instance
7
8
  @collector_instance || DeprecationCollector.instance
@@ -17,15 +18,15 @@ class DeprecationCollector
17
18
  end
18
19
 
19
20
  def current_path
20
- @current_path ||= request.path_info.gsub(/^\//, "")
21
+ @current_path ||= request.path_info.gsub(%r{^/}, "")
21
22
  end
22
23
 
23
24
  def deprecations_path
24
- "#{root_path}"
25
+ root_path # /
25
26
  end
26
27
 
27
- def deprecation_path(id)
28
- "#{root_path}#{id}"
28
+ def deprecation_path(id, format: nil)
29
+ ["#{root_path}#{id}", format].compact.join('.')
29
30
  end
30
31
 
31
32
  def enable_deprecations_path
@@ -48,27 +49,41 @@ class DeprecationCollector
48
49
 
49
50
  def trigger_rails_deprecation
50
51
  return unless defined?(ActiveSupport::Deprecation)
52
+
51
53
  -> { ActiveSupport::Deprecation.warn("Test deprecation") } []
52
54
  end
53
55
 
54
56
  def current_color_theme
55
- return 'dark' if params['dark']
56
- return 'light' if params['light']
57
- return 'dark' if request.get_header('HTTP_Sec_CH_Prefers_Color_Scheme').to_s.downcase.include?("dark")
58
- 'auto'
57
+ return "dark" if params["dark"]
58
+ return "light" if params["light"]
59
+ return "dark" if request.get_header("HTTP_Sec_CH_Prefers_Color_Scheme").to_s.downcase.include?("dark")
60
+
61
+ "auto"
62
+ end
63
+
64
+ def detect_tag(deprecation)
65
+ msg = deprecation[:message]
66
+ return :kwargs if msg.include?("Using the last argument as keyword parameters is deprecated") ||
67
+ msg.include?("Passing the keyword argument as the last hash parameter is deprecated")
68
+ end
69
+
70
+ def test_deprecation?(deprecation)
71
+ %w[trigger_kwargs_error_warning trigger_rails_deprecation].any? { |method| deprecation[:message]}
59
72
  end
60
73
 
61
74
  def deprecation_tags(deprecation)
62
- {}.tap do |tags|
63
- tags[:kwargs] = 'bg-secondary' if deprecation[:message].include?("Using the last argument as keyword parameters is deprecated") ||
64
- deprecation[:message].include?("Passing the keyword argument as the last hash parameter is deprecated")
75
+ tags = Set.new
76
+ if (detected_tag = detect_tag(deprecation))
77
+ tags << detected_tag
78
+ end
79
+ tags << :test if test_deprecation?(deprecation)
80
+ tags << deprecation[:realm] if deprecation[:realm] && deprecation[:realm] != "rails"
81
+ tags.merge(deprecation.dig(:notes, :tags) || [])
65
82
 
66
- tags[:test] = 'bg-success' if deprecation[:message].include?("trigger_kwargs_error_warning") ||
67
- deprecation[:message].include?("trigger_rails_deprecation")
68
-
69
- tags[deprecation[:realm]] = 'bg-secondary' if deprecation[:realm] && deprecation[:realm] != 'rails'
83
+ tags.to_h do |tag|
84
+ next [tag, "bg-success"] if tag == :test
70
85
 
71
- deprecation.dig(:notes, :tags)&.each { |tag| tags[tag] = 'bg-secondary' }
86
+ [tag, "bg-secondary"]
72
87
  end
73
88
  end
74
89
  end