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.
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
- subscribe("start_transaction.active_record") { open_transaction }
125
- subscribe("transaction.active_record") { |event| close_transaction(event) }
126
- end
127
-
128
- def open_transaction
129
- Stack.push(app_backtrace.first)
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 close_transaction(event)
133
- transaction = Stack.pop
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
- outcome = event.payload[:outcome] || :commit
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 fallback_event(duration, outcome)
147
- { type: "transaction", at: Time.now.to_f, pid: Process.pid, duration_ms: duration,
148
- outcome: outcome.to_s, source: app_backtrace.first, violations: [] }
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) } }.uniq.sort
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?
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Txray
4
4
  class SourceFile
5
- DIRECTIVE = /#\s*txray:disable(?<rules>[\w,\s-]*)/
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
- @directives = build_directives(comments)
22
+ @lines = code.lines
23
+ @inline = {}
24
+ @regions = []
25
+ index(comments)
23
26
  end
24
27
 
25
28
  def disabled?(line, rule_id)
26
- rules = @directives[line] || @directives[line - 1]
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
- private
42
+ def index(comments)
43
+ open = {}
33
44
 
34
- def build_directives(comments)
35
- comments.each_with_object({}) do |comment, directives|
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
- directives[comment.location.start_line] = match[:rules].to_s.split(/[,\s]+/).reject(&:empty?)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Txray
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
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.0
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: 4.0.8
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: []