txray 0.1.0 → 0.2.1
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/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +43 -0
- data/README.md +110 -27
- data/Rakefile +1 -0
- data/doc/monitor.png +0 -0
- data/doc/monitor.svg +229 -0
- data/lib/generators/txray/install_generator.rb +28 -0
- data/lib/generators/txray/templates/txray.yml +37 -0
- data/lib/txray/cli.rb +3 -0
- data/lib/txray/config.rb +28 -1
- data/lib/txray/monitor.rb +25 -1
- data/lib/txray/reporters/github.rb +71 -5
- data/lib/txray/reporters/live.rb +157 -69
- data/lib/txray/reporters/text.rb +65 -16
- data/lib/txray/reporters.rb +4 -0
- data/lib/txray/runtime.rb +47 -20
- data/lib/txray/scanner.rb +14 -1
- data/lib/txray/source_file.rb +56 -10
- data/lib/txray/version.rb +1 -1
- metadata +7 -2
data/lib/txray/runtime.rb
CHANGED
|
@@ -89,6 +89,22 @@ module Txray
|
|
|
89
89
|
@sink.write(event)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def open_transaction
|
|
93
|
+
Stack.push(app_backtrace.first)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def close_transaction(outcome)
|
|
97
|
+
transaction = Stack.pop
|
|
98
|
+
return if transaction.nil? || ignoring?
|
|
99
|
+
|
|
100
|
+
duration = transaction.duration_ms
|
|
101
|
+
slow = duration >= @options[:threshold_ms]
|
|
102
|
+
return unless slow || transaction.violations.any?
|
|
103
|
+
|
|
104
|
+
report(transaction.to_event(outcome || :commit))
|
|
105
|
+
announce_slow(duration) if slow
|
|
106
|
+
end
|
|
107
|
+
|
|
92
108
|
private
|
|
93
109
|
|
|
94
110
|
def announce(rule, message)
|
|
@@ -121,31 +137,25 @@ module Txray
|
|
|
121
137
|
end
|
|
122
138
|
|
|
123
139
|
def watch_transactions
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
if notifies_transaction_start?
|
|
141
|
+
subscribe("start_transaction.active_record") { open_transaction }
|
|
142
|
+
subscribe("transaction.active_record") { |event| close_transaction(event.payload[:outcome]) }
|
|
143
|
+
else
|
|
144
|
+
patch_transaction_manager
|
|
145
|
+
end
|
|
130
146
|
end
|
|
131
147
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
return if ignoring?
|
|
135
|
-
|
|
136
|
-
duration = transaction&.duration_ms || event.duration.round(1)
|
|
137
|
-
slow = duration >= @options[:threshold_ms]
|
|
138
|
-
violations = transaction&.violations.to_a
|
|
139
|
-
return unless slow || violations.any?
|
|
148
|
+
def notifies_transaction_start?
|
|
149
|
+
return false unless defined?(ActiveRecord::VERSION::STRING)
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
report((transaction&.to_event(outcome) || fallback_event(duration, outcome)).merge(duration_ms: duration))
|
|
143
|
-
announce_slow(duration) if slow
|
|
151
|
+
Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.2")
|
|
144
152
|
end
|
|
145
153
|
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
def patch_transaction_manager
|
|
155
|
+
return if @transactions_patched
|
|
156
|
+
|
|
157
|
+
ActiveRecord::ConnectionAdapters::DatabaseStatements.prepend(TransactionTimer)
|
|
158
|
+
@transactions_patched = true
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
def announce_slow(duration)
|
|
@@ -179,6 +189,23 @@ module Txray
|
|
|
179
189
|
end
|
|
180
190
|
end
|
|
181
191
|
|
|
192
|
+
module TransactionTimer
|
|
193
|
+
def within_new_transaction(*, **, &)
|
|
194
|
+
return super unless Txray::Runtime.installed?
|
|
195
|
+
|
|
196
|
+
Txray::Runtime.open_transaction
|
|
197
|
+
outcome = :commit
|
|
198
|
+
begin
|
|
199
|
+
super
|
|
200
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
201
|
+
outcome = :rollback
|
|
202
|
+
raise
|
|
203
|
+
ensure
|
|
204
|
+
Txray::Runtime.close_transaction(outcome)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
182
209
|
module NetHttpGuard
|
|
183
210
|
def request(req, body = nil, &)
|
|
184
211
|
return super unless Txray::Runtime.transaction_open?
|
data/lib/txray/scanner.rb
CHANGED
|
@@ -43,11 +43,24 @@ module Txray
|
|
|
43
43
|
def files
|
|
44
44
|
roots = @paths.empty? ? @config.includes : @paths
|
|
45
45
|
verify(roots) unless @paths.empty?
|
|
46
|
-
roots.flat_map { |root| expand(root).reject { |path| excluded?(path, root) } }
|
|
46
|
+
roots.flat_map { |root| expand(root).reject { |path| excluded?(path, root) } }
|
|
47
|
+
.map { |path| relative(path) }.uniq.sort
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
52
|
+
def relative(path)
|
|
53
|
+
absolute = resolve(path)
|
|
54
|
+
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
55
|
+
absolute.start_with?(root) ? absolute.delete_prefix(root) : path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve(path)
|
|
59
|
+
File.realpath(path)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
File.expand_path(path)
|
|
62
|
+
end
|
|
63
|
+
|
|
51
64
|
def verify(roots)
|
|
52
65
|
missing = roots.reject { |root| File.exist?(root) }
|
|
53
66
|
raise Error, "no such file or directory: #{missing.join(", ")}" if missing.any?
|
data/lib/txray/source_file.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Txray
|
|
4
4
|
class SourceFile
|
|
5
|
-
DIRECTIVE =
|
|
5
|
+
DIRECTIVE = /\A#\s*txray:(?<action>disable|enable)\b(?<rules>[\w,\s-]*)/
|
|
6
6
|
|
|
7
7
|
attr_reader :path, :root
|
|
8
8
|
|
|
@@ -11,33 +11,79 @@ module Txray
|
|
|
11
11
|
result = Prism.parse(code)
|
|
12
12
|
return nil if result.failure?
|
|
13
13
|
|
|
14
|
-
new(path: path, root: result.value, comments: result.comments)
|
|
14
|
+
new(path: path, root: result.value, comments: result.comments, code: code)
|
|
15
15
|
rescue StandardError
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def initialize(path:, root:, comments: [])
|
|
19
|
+
def initialize(path:, root:, comments: [], code: "")
|
|
20
20
|
@path = path
|
|
21
21
|
@root = root
|
|
22
|
-
@
|
|
22
|
+
@lines = code.lines
|
|
23
|
+
@inline = {}
|
|
24
|
+
@regions = []
|
|
25
|
+
index(comments)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def disabled?(line, rule_id)
|
|
26
|
-
|
|
29
|
+
return true if covers?(@inline[line], rule_id)
|
|
30
|
+
|
|
31
|
+
@regions.any? { |region| line.between?(region[:from], region[:to]) && covers?(region[:rules], rule_id) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def covers?(rules, rule_id)
|
|
27
37
|
return false if rules.nil?
|
|
28
38
|
|
|
29
39
|
rules.empty? || rules.include?(rule_id)
|
|
30
40
|
end
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
def index(comments)
|
|
43
|
+
open = {}
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
match = DIRECTIVE.match(comment.slice)
|
|
45
|
+
comments.each do |comment|
|
|
46
|
+
match = DIRECTIVE.match(comment.slice.to_s.strip)
|
|
37
47
|
next unless match
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
apply(match, comment, open)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
open.each { |key, from| @regions << region(key, from, Float::INFINITY) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply(match, comment, open)
|
|
56
|
+
rules = named_rules(match[:rules])
|
|
57
|
+
line = comment.location.start_line
|
|
58
|
+
|
|
59
|
+
if trailing?(comment)
|
|
60
|
+
@inline[line] = rules if match[:action] == "disable"
|
|
61
|
+
elsif match[:action] == "disable"
|
|
62
|
+
(rules.empty? ? [ :all ] : rules).each { |key| open[key] ||= line }
|
|
63
|
+
else
|
|
64
|
+
close(open, rules, line)
|
|
40
65
|
end
|
|
41
66
|
end
|
|
67
|
+
|
|
68
|
+
def close(open, rules, line)
|
|
69
|
+
keys = rules.empty? ? open.keys.dup : rules
|
|
70
|
+
keys.each do |key|
|
|
71
|
+
from = open.delete(key)
|
|
72
|
+
@regions << region(key, from, line) if from
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def region(key, from, to)
|
|
77
|
+
{ from: from, to: to, rules: key == :all ? [] : [ key ] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def named_rules(text)
|
|
81
|
+
text.to_s.split(/[,\s]+/).reject { |rule| rule.empty? || rule == "all" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def trailing?(comment)
|
|
85
|
+
prefix = @lines[comment.location.start_line - 1].to_s[0, comment.location.start_column].to_s
|
|
86
|
+
!prefix.strip.empty?
|
|
87
|
+
end
|
|
42
88
|
end
|
|
43
89
|
end
|
data/lib/txray/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: txray
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Theo Wecker
|
|
@@ -37,10 +37,15 @@ extensions: []
|
|
|
37
37
|
extra_rdoc_files: []
|
|
38
38
|
files:
|
|
39
39
|
- CHANGELOG.md
|
|
40
|
+
- CONTRIBUTING.md
|
|
40
41
|
- LICENSE.txt
|
|
41
42
|
- README.md
|
|
42
43
|
- Rakefile
|
|
44
|
+
- doc/monitor.png
|
|
45
|
+
- doc/monitor.svg
|
|
43
46
|
- exe/txray
|
|
47
|
+
- lib/generators/txray/install_generator.rb
|
|
48
|
+
- lib/generators/txray/templates/txray.yml
|
|
44
49
|
- lib/txray.rb
|
|
45
50
|
- lib/txray/analyzer.rb
|
|
46
51
|
- lib/txray/catalog.rb
|
|
@@ -94,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
99
|
- !ruby/object:Gem::Version
|
|
95
100
|
version: '0'
|
|
96
101
|
requirements: []
|
|
97
|
-
rubygems_version:
|
|
102
|
+
rubygems_version: 3.6.9
|
|
98
103
|
specification_version: 4
|
|
99
104
|
summary: Static analysis that finds slow work hidden inside database transactions.
|
|
100
105
|
test_files: []
|