deprecation_collector 0.4.0 → 0.5.0

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: 7f62a11c42b2d8d6ee4236c98960b452d5aa2c0ec76d83d6215101de55edf6a8
4
+ data.tar.gz: aff231737c13f8e47c63e60706cbee894d50da276caff00ec4898c766da9ee19
5
5
  SHA512:
6
- metadata.gz: d4b4cf6feb4d736114f7324a0b34a65e32ee7f3edd610ee5aabb41daa069fd2417a746e927761c28ec22b37a9ceb1f768c2fa92064c11e0d4e4474801dd9f008
7
- data.tar.gz: 9e6641fc8b17e5d4d2a5d5d7981afecca83feffbc549d88206105a4f752feeb951290fa9dc77f8d24166519df12da917d83f81685eb2d79b6ca87912d09ff599
6
+ metadata.gz: d053f33b41ca1dd5b20b81069bef9cf178f4f10fe7a00f193d8a5bdb4e1aa30267223df74bfe3abd234dd08ba2ccfaef757b5febbe8546393c808c1b7b68ecc7
7
+ data.tar.gz: b4c259673e8b2d4b19de8e3f259563ad19e34381c43b1dfb141e9193bc1f57606eb2b48c81dfb88b699440bbf4f5922a2af92c4ead1c066a90e468bb82a973c0
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.0)
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.0)
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.0)
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.0)
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,205 @@
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 enabled?; true; end
11
+ def enable; end
12
+ def disable; end
13
+
14
+ def unsent_deprecations; []; end
15
+ def fetch_known_digests; end
16
+
17
+ def delete(digests); end
18
+ def clear(enable: false); end
19
+
20
+ def store(_deprecation); raise("Not implemented"); end
21
+ # rubocop:enable Style/SingleLineMethods
22
+ end
23
+
24
+ # dummy strategy that outputs every deprecation into stderr
25
+ class StdErr < Base
26
+ def store(deprecation)
27
+ DeprecationCollector.instance.send(:log_deprecation, deprecation)
28
+ end
29
+ end
30
+
31
+ # storing in redis with deduplication by fingerprint
32
+ class Redis < Base
33
+ attr_accessor :write_interval, :write_interval_jitter, :redis, :count
34
+
35
+ def initialize(redis, mutex: nil, count: false, write_interval: 900, write_interval_jitter: 60,
36
+ key_prefix: nil)
37
+ super()
38
+ @key_prefix = key_prefix || "deprecations"
39
+ @redis = redis
40
+ @last_write_time = current_time
41
+ @count = count
42
+ @write_interval = write_interval
43
+ @write_interval_jitter = write_interval_jitter
44
+ # on cruby hash itself is threadsafe, but we need to prevent races
45
+ @deprecations_mutex = mutex || Mutex.new
46
+ @deprecations = {}
47
+ @known_digests = Set.new
48
+ end
49
+
50
+ def unsent_deprecations
51
+ @deprecations
52
+ end
53
+
54
+ def enabled?
55
+ @redis.get(enabled_flag_key) != "false"
56
+ end
57
+
58
+ def enable
59
+ @redis.set(enabled_flag_key, "true")
60
+ end
61
+
62
+ def disable
63
+ @redis.set(enabled_flag_key, "false")
64
+ end
65
+
66
+ def delete(remove_digests)
67
+ return 0 unless remove_digests.any?
68
+
69
+ @redis.pipelined do |pipe|
70
+ pipe.hdel(data_hash_key, *remove_digests)
71
+ pipe.hdel(notes_hash_key, *remove_digests)
72
+ pipe.hdel(counter_hash_key, *remove_digests) if @count
73
+ end.first
74
+ end
75
+
76
+ def clear(enable: false)
77
+ @redis.del(data_hash_key, counter_hash_key, notes_hash_key)
78
+ @redis.del(enabled_flag_key) if enable
79
+ @known_digests.clear
80
+ @deprecations.clear
81
+ end
82
+
83
+ def fetch_known_digests
84
+ # FIXME: use `.merge!`?
85
+ @known_digests.merge(@redis.hkeys(data_hash_key))
86
+ end
87
+
88
+ def store(deprecation)
89
+ fresh = !@deprecations.key?(deprecation.digest)
90
+ @deprecations_mutex.synchronize do
91
+ (@deprecations[deprecation.digest] ||= deprecation).touch
92
+ end
93
+
94
+ flush if current_time - @last_write_time > (@write_interval + rand(@write_interval_jitter))
95
+ fresh
96
+ end
97
+
98
+ def flush(force: false)
99
+ return unless force || (current_time > @last_write_time + @write_interval)
100
+
101
+ deprecations_to_flush = nil
102
+ @deprecations_mutex.synchronize do
103
+ deprecations_to_flush = @deprecations
104
+ @deprecations = {}
105
+ @last_write_time = current_time
106
+ # checking in this section to prevent multiple parallel check requests
107
+ return DeprecationCollector.instance.instance_variable_set(:@enabled, false) unless enabled?
108
+ end
109
+
110
+ write_count_to_redis(deprecations_to_flush) if @count
111
+
112
+ # make as few writes as possible, other workers may already have reported our warning
113
+ fetch_known_digests
114
+ deprecations_to_flush.reject! { |digest, _val| @known_digests.include?(digest) }
115
+ return unless deprecations_to_flush.any?
116
+
117
+ @known_digests.merge(deprecations_to_flush.keys)
118
+ @redis.mapped_hmset(data_hash_key, deprecations_to_flush.transform_values(&:to_json))
119
+ end
120
+
121
+ def read_each
122
+ cursor = 0
123
+ loop do
124
+ cursor, data_pairs = @redis.hscan(data_hash_key, cursor)
125
+
126
+ if data_pairs.any?
127
+ data_pairs.zip(
128
+ @redis.hmget(counter_hash_key, data_pairs.map(&:first)),
129
+ @redis.hmget(notes_hash_key, data_pairs.map(&:first))
130
+ ).each do |(digest, data), count, notes|
131
+ yield(digest, data, count, notes)
132
+ end
133
+ end
134
+ break if cursor == "0"
135
+ end
136
+ end
137
+
138
+ def read_one(digest)
139
+ [
140
+ digest,
141
+ *@redis.pipelined do |pipe|
142
+ pipe.hget(data_hash_key, digest)
143
+ pipe.hget(counter_hash_key, digest)
144
+ pipe.hget(notes_hash_key, digest)
145
+ end
146
+ ]
147
+ end
148
+
149
+ def import(dump_hash)
150
+ @redis.mapped_hmset(data_hash_key, dump_hash.transform_values(&:to_json))
151
+ end
152
+
153
+ def cleanup(&_block)
154
+ cursor = 0
155
+ removed = total = 0
156
+ loop do
157
+ cursor, data_pairs = @redis.hscan(data_hash_key, cursor) # NB: some pages may be empty
158
+ total += data_pairs.size
159
+ removed += delete(
160
+ data_pairs.to_h.select { |_digest, data| yield(JSON.parse(data, symbolize_names: true)) }.keys
161
+ )
162
+ break if cursor == "0"
163
+ end
164
+ "#{removed} removed, #{total - removed} left"
165
+ end
166
+
167
+ def key_prefix=(val)
168
+ @enabled_flag_key = @data_hash_key = @counter_hash_key = @notes_hash_key = nil
169
+ @key_prefix = val
170
+ end
171
+
172
+ protected
173
+
174
+ def enabled_flag_key
175
+ @enabled_flag_key ||= "#{@key_prefix}:enabled" # usually deprecations:enabled
176
+ end
177
+
178
+ def data_hash_key
179
+ @data_hash_key ||= "#{@key_prefix}:data" # usually deprecations:data
180
+ end
181
+
182
+ def counter_hash_key
183
+ @counter_hash_key ||= "#{@key_prefix}:counter" # usually deprecations:counter
184
+ end
185
+
186
+ def notes_hash_key
187
+ @notes_hash_key ||= "#{@key_prefix}:notes" # usually deprecations:notes
188
+ end
189
+
190
+ def current_time
191
+ return Time.zone.now if Time.respond_to?(:zone) && Time.zone
192
+
193
+ Time.now
194
+ end
195
+
196
+ def write_count_to_redis(deprecations_to_flush)
197
+ @redis.pipelined do |pipe|
198
+ deprecations_to_flush.each_pair do |digest, deprecation|
199
+ pipe.hincrby(counter_hash_key, digest, deprecation.occurences)
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ 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.0"
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,37 @@ 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
+ return :test if msg.include?("trigger_kwargs_error_warning") || msg.include?("trigger_rails_deprecation")
59
69
  end
60
70
 
61
71
  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")
72
+ tags = Set.new
73
+ if (detected_tag = detect_tag(deprecation))
74
+ tags << detected_tag
75
+ end
76
+ tags << deprecation[:realm] if deprecation[:realm] && deprecation[:realm] != "rails"
77
+ tags.merge(deprecation.dig(:notes, :tags) || [])
65
78
 
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'
79
+ tags.to_h do |tag|
80
+ next [tag, "bg-success"] if tag == :test
70
81
 
71
- deprecation.dig(:notes, :tags)&.each { |tag| tags[tag] = 'bg-secondary' }
82
+ [tag, "bg-secondary"]
72
83
  end
73
84
  end
74
85
  end