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 +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -4
- data/Gemfile.lock +1 -1
- data/Rakefile +5 -4
- data/deprecation_collector.gemspec +1 -1
- data/gemfiles/rails_6.gemfile.lock +1 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_none.gemfile.lock +1 -1
- data/lib/deprecation_collector/deprecation.rb +2 -1
- data/lib/deprecation_collector/storage.rb +205 -0
- data/lib/deprecation_collector/version.rb +1 -1
- data/lib/deprecation_collector/web/application.rb +27 -19
- data/lib/deprecation_collector/web/helpers.rb +27 -16
- data/lib/deprecation_collector/web/router.rb +69 -52
- data/lib/deprecation_collector/web/utils.rb +10 -7
- data/lib/deprecation_collector/web/views/index.html.template.rb +15 -5
- data/lib/deprecation_collector/web/views/show.html.template.rb +72 -3
- data/lib/deprecation_collector/web.rb +3 -1
- data/lib/deprecation_collector.rb +78 -118
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f62a11c42b2d8d6ee4236c98960b452d5aa2c0ec76d83d6215101de55edf6a8
|
4
|
+
data.tar.gz: aff231737c13f8e47c63e60706cbee894d50da276caff00ec4898c766da9ee19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
31
|
-
gem
|
30
|
+
gem "rack"
|
31
|
+
gem "webrick"
|
32
32
|
|
33
|
-
gem
|
33
|
+
gem "slim" # not used in production, for compiling templates
|
data/Gemfile.lock
CHANGED
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
|
18
|
+
require "slim"
|
18
19
|
# Slim::Template.new { '.lala' }.precompiled_template
|
19
|
-
Dir[
|
20
|
-
target = file.sub(/\.slim\z/,
|
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[
|
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"
|
@@ -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,22 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
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
|
-
|
17
|
-
|
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] ==
|
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
|
40
|
+
get "/dump.json" do
|
42
41
|
render json: collector_instance.dump
|
43
42
|
end
|
44
43
|
|
45
|
-
get
|
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
|
52
|
-
|
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
|
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
|
70
|
+
delete "/all" do
|
63
71
|
collector_instance.flush_redis
|
64
72
|
redirect_to deprecations_path
|
65
73
|
end
|
66
74
|
|
67
|
-
post
|
75
|
+
post "/enable" do
|
68
76
|
collector_instance.enable
|
69
77
|
redirect_to deprecations_path
|
70
78
|
end
|
71
79
|
|
72
|
-
delete
|
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
|
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
|
84
|
-
trigger_kwargs_error_warning({ foo: nil }) if RUBY_VERSION.start_with?(
|
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
|
-
|
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
|
56
|
-
return
|
57
|
-
return
|
58
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
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
|
-
|
82
|
+
[tag, "bg-secondary"]
|
72
83
|
end
|
73
84
|
end
|
74
85
|
end
|