rails_tracepoint_stack 0.3.5 → 0.5.0
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/README.md +204 -0
- data/changelog.md +156 -0
- data/lib/generators/rails_tracepoint_stack/install/install_generator.rb +29 -0
- data/lib/rails_tracepoint_stack/depth_tracker.rb +54 -0
- data/lib/rails_tracepoint_stack/filter/gem_path.rb +20 -1
- data/lib/rails_tracepoint_stack/limits.rb +45 -0
- data/lib/rails_tracepoint_stack/log_formatter.rb +30 -4
- data/lib/rails_tracepoint_stack/renderer/summary.rb +37 -0
- data/lib/rails_tracepoint_stack/renderer/tree.rb +142 -0
- data/lib/rails_tracepoint_stack/sink/collector.rb +138 -0
- data/lib/rails_tracepoint_stack/sink/log.rb +17 -0
- data/lib/rails_tracepoint_stack/skill_installer.rb +39 -0
- data/lib/rails_tracepoint_stack/templates/skill.md +142 -0
- data/lib/rails_tracepoint_stack/trace.rb +33 -3
- data/lib/rails_tracepoint_stack/trace_record.rb +45 -0
- data/lib/rails_tracepoint_stack/trace_session.rb +77 -0
- data/lib/rails_tracepoint_stack/tracer.rb +100 -7
- data/lib/rails_tracepoint_stack/truncator.rb +85 -0
- data/lib/rails_tracepoint_stack/version.rb +1 -1
- data/lib/rails_tracepoint_stack.rb +41 -0
- metadata +26 -8
|
@@ -3,30 +3,123 @@ require "rails_tracepoint_stack/logger"
|
|
|
3
3
|
require "rails_tracepoint_stack/trace_filter"
|
|
4
4
|
require "rails_tracepoint_stack/trace"
|
|
5
5
|
require "rails_tracepoint_stack/log_formatter"
|
|
6
|
+
require "rails_tracepoint_stack/sink/log"
|
|
7
|
+
require "rails_tracepoint_stack/depth_tracker"
|
|
6
8
|
|
|
7
9
|
module RailsTracepointStack
|
|
8
10
|
class Tracer
|
|
9
11
|
include RailsTracepointStack::TraceFilter
|
|
10
12
|
extend Forwardable
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
DEFAULT_EVENTS = [:call].freeze
|
|
15
|
+
|
|
12
16
|
def_delegators :@tracer, :enable, :disable
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
# How many traces the filters dropped. A capture that keeps nothing is
|
|
19
|
+
# ambiguous on its own: this tells the difference between code that never
|
|
20
|
+
# ran and code that ran entirely inside gems.
|
|
21
|
+
attr_reader :filtered_count
|
|
22
|
+
|
|
23
|
+
# A nil thread watches every thread in the process, which is what the
|
|
24
|
+
# global tracer wants. Passing one confines tracing to it, so a capture
|
|
25
|
+
# running inside a threaded server does not pick up other requests.
|
|
26
|
+
# own_block is the block the caller wrapped around the code under
|
|
27
|
+
# inspection. It is the caller's own code, so it is skipped before a depth
|
|
28
|
+
# is assigned - otherwise it would indent the whole trace by a level it
|
|
29
|
+
# never shows.
|
|
30
|
+
def initialize(
|
|
31
|
+
sink: RailsTracepointStack::Sink::Log.new,
|
|
32
|
+
events: DEFAULT_EVENTS,
|
|
33
|
+
thread: nil,
|
|
34
|
+
own_block: nil
|
|
35
|
+
)
|
|
36
|
+
@sink = sink
|
|
37
|
+
@events = events
|
|
38
|
+
@thread = thread
|
|
39
|
+
# Ruby has grown extra entries on source_location over time (columns, end
|
|
40
|
+
# positions), so only the file and the line are compared.
|
|
41
|
+
@own_block_location = own_block&.source_location&.first(2)
|
|
42
|
+
@filtered_count = 0
|
|
15
43
|
generate_tracer
|
|
16
44
|
end
|
|
17
45
|
|
|
18
46
|
private
|
|
19
47
|
|
|
20
48
|
def generate_tracer
|
|
21
|
-
@tracer ||= TracePoint.new(
|
|
49
|
+
@tracer ||= TracePoint.new(*@events) do |tracepoint|
|
|
50
|
+
next if out_of_scope_thread?
|
|
51
|
+
|
|
22
52
|
trace = RailsTracepointStack::Trace.new(trace_point: tracepoint)
|
|
23
53
|
|
|
24
|
-
next if
|
|
54
|
+
next if own_block?(trace)
|
|
55
|
+
|
|
56
|
+
if ignore_trace?(trace: trace)
|
|
57
|
+
@filtered_count += 1
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
trace.depth = depth_for(trace)
|
|
62
|
+
@sink.record(trace)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Matching on location alone would also drop a block written on the same
|
|
67
|
+
# line as the capture call. The caller's block is the first to arrive from
|
|
68
|
+
# that location, so only that one is skipped, along with its return.
|
|
69
|
+
def own_block?(trace)
|
|
70
|
+
return false if @own_block_location.nil?
|
|
25
71
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
72
|
+
case trace.kind
|
|
73
|
+
when :b_call then claim_own_block(trace)
|
|
74
|
+
when :b_return then release_own_block(trace)
|
|
75
|
+
else false
|
|
29
76
|
end
|
|
30
77
|
end
|
|
78
|
+
|
|
79
|
+
def claim_own_block(trace)
|
|
80
|
+
return false if @own_block_seen
|
|
81
|
+
return false unless [trace.file_path, trace.line_number] == @own_block_location
|
|
82
|
+
|
|
83
|
+
@own_block_seen = true
|
|
84
|
+
@own_block_position = raw_stack_position
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def release_own_block(trace)
|
|
89
|
+
return false unless @own_block_position && raw_stack_position == @own_block_position
|
|
90
|
+
|
|
91
|
+
@own_block_position = nil
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def out_of_scope_thread?
|
|
96
|
+
!@thread.nil? && Thread.current != @thread
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Only kept traces pay for reading the stack position, so the cost stays
|
|
100
|
+
# proportional to the app code being traced rather than to everything the
|
|
101
|
+
# VM runs underneath it.
|
|
102
|
+
def depth_for(trace)
|
|
103
|
+
tracker = depth_tracker
|
|
104
|
+
raw_position = raw_stack_position
|
|
105
|
+
|
|
106
|
+
case trace.kind
|
|
107
|
+
when :call, :b_call then tracker.enter(raw_position)
|
|
108
|
+
when :return, :b_return then tracker.leave(raw_position)
|
|
109
|
+
when :raise then tracker.raised(raw_position)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def raw_stack_position
|
|
114
|
+
caller_locations(1)&.length || 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def depth_tracker
|
|
118
|
+
Thread.current[depth_tracker_key] ||= RailsTracepointStack::DepthTracker.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def depth_tracker_key
|
|
122
|
+
@depth_tracker_key ||= :"rails_tracepoint_stack_depth_#{object_id}"
|
|
123
|
+
end
|
|
31
124
|
end
|
|
32
125
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module RailsTracepointStack
|
|
4
|
+
# Shrinks an already-serialized value so one fat argument (a big payload, a
|
|
5
|
+
# long SQL string, a loaded association) cannot dominate the output. Runs
|
|
6
|
+
# over the plain structures LogFormatter.safe_value produces, so it only
|
|
7
|
+
# ever sees strings, numbers, booleans, nil, arrays and hashes.
|
|
8
|
+
module Truncator
|
|
9
|
+
ELLIPSIS = "…".freeze
|
|
10
|
+
|
|
11
|
+
# Per-string and per-collection limits still leave room for one value to
|
|
12
|
+
# run to thousands of characters: twenty items each truncated to two
|
|
13
|
+
# hundred is still four thousand. This caps the value as a whole, and does
|
|
14
|
+
# it before the per-item truncation so the summary can report the real
|
|
15
|
+
# size rather than the already-shrunk one.
|
|
16
|
+
def self.bounded(value, limits)
|
|
17
|
+
capped = cap(value, limits)
|
|
18
|
+
return capped unless capped.equal?(value)
|
|
19
|
+
|
|
20
|
+
call(value, limits)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.cap(value, limits)
|
|
24
|
+
max = limits.max_value_length
|
|
25
|
+
return value if max.nil?
|
|
26
|
+
return value unless value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
size = JSON.generate(value).length
|
|
29
|
+
return value if size <= max
|
|
30
|
+
|
|
31
|
+
summarize(value, size)
|
|
32
|
+
rescue SystemStackError, StandardError
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.summarize(value, size)
|
|
37
|
+
case value
|
|
38
|
+
when Array then "[#{value.size} items, #{size} chars — over max_value_length]"
|
|
39
|
+
when Hash then "{#{value.size} keys, #{size} chars — over max_value_length}"
|
|
40
|
+
else "[#{size} chars — over max_value_length]"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.call(value, limits)
|
|
45
|
+
case value
|
|
46
|
+
when String then truncate_string(value, limits)
|
|
47
|
+
when Array then truncate_array(value, limits)
|
|
48
|
+
when Hash then truncate_hash(value, limits)
|
|
49
|
+
else value
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.truncate_string(value, limits)
|
|
54
|
+
max = limits.max_string_length
|
|
55
|
+
return value if max.nil? || value.length <= max
|
|
56
|
+
|
|
57
|
+
"#{value[0, max]}#{ELLIPSIS} (#{value.length} chars)"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.truncate_array(value, limits)
|
|
61
|
+
max = limits.max_collection_size
|
|
62
|
+
kept = (max.nil? || value.length <= max) ? value : value.first(max)
|
|
63
|
+
result = kept.map { |item| call(item, limits) }
|
|
64
|
+
|
|
65
|
+
return result if kept.length == value.length
|
|
66
|
+
|
|
67
|
+
result << "(+#{value.length - kept.length} more)"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.truncate_hash(value, limits)
|
|
71
|
+
max = limits.max_collection_size
|
|
72
|
+
dropped = (max.nil? || value.size <= max) ? 0 : value.size - max
|
|
73
|
+
kept = dropped.zero? ? value : value.first(max).to_h
|
|
74
|
+
|
|
75
|
+
result = kept.each_with_object({}) do |(key, item), memo|
|
|
76
|
+
memo[key] = call(item, limits)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return result if dropped.zero?
|
|
80
|
+
|
|
81
|
+
result[ELLIPSIS] = "(+#{dropped} more)"
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
require 'rails_tracepoint_stack/configuration'
|
|
2
2
|
require 'rails_tracepoint_stack/log_formatter'
|
|
3
3
|
require 'rails_tracepoint_stack/tracer'
|
|
4
|
+
require 'rails_tracepoint_stack/limits'
|
|
5
|
+
require 'rails_tracepoint_stack/skill_installer'
|
|
6
|
+
require 'rails_tracepoint_stack/sink/collector'
|
|
7
|
+
require 'rails_tracepoint_stack/trace_session'
|
|
4
8
|
|
|
5
9
|
$rails_tracer_rtps = nil
|
|
6
10
|
|
|
@@ -17,6 +21,43 @@ module RailsTracepointStack
|
|
|
17
21
|
yield(configuration)
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
CAPTURE_EVENTS = [:call, :return, :raise].freeze
|
|
25
|
+
BLOCK_EVENTS = [:b_call, :b_return].freeze
|
|
26
|
+
|
|
27
|
+
# Traces a block and hands back everything that happened inside it, instead
|
|
28
|
+
# of writing to a log. Meant to be run as a one-off: capture, read, done.
|
|
29
|
+
# Blocks are off by default: a block written inside a loop fires once per
|
|
30
|
+
# element, so watching them costs more traces than watching methods does.
|
|
31
|
+
# Turn them on to see scopes, lambdas and anything else whose logic lives in
|
|
32
|
+
# a block rather than a method.
|
|
33
|
+
def self.capture(threads: :current, blocks: false, **limit_options, &traced)
|
|
34
|
+
raise ArgumentError, "Block not given to #capture" unless traced
|
|
35
|
+
|
|
36
|
+
collector = RailsTracepointStack::Sink::Collector.new(
|
|
37
|
+
limits: RailsTracepointStack::Limits.new(**limit_options)
|
|
38
|
+
)
|
|
39
|
+
session = collector.session
|
|
40
|
+
tracer = RailsTracepointStack::Tracer.new(
|
|
41
|
+
sink: collector,
|
|
42
|
+
events: blocks ? CAPTURE_EVENTS + BLOCK_EVENTS : CAPTURE_EVENTS,
|
|
43
|
+
thread: (threads == :all) ? nil : Thread.current,
|
|
44
|
+
own_block: traced
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
tracer.enable
|
|
48
|
+
begin
|
|
49
|
+
session.result = traced.call(session)
|
|
50
|
+
rescue Exception => error
|
|
51
|
+
session.error = error
|
|
52
|
+
raise
|
|
53
|
+
ensure
|
|
54
|
+
tracer.disable
|
|
55
|
+
session.filtered_count = tracer.filtered_count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
session
|
|
59
|
+
end
|
|
60
|
+
|
|
20
61
|
def self.enable_trace
|
|
21
62
|
raise ArgumentError, "Block not given to #enable_trace" unless block_given?
|
|
22
63
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_tracepoint_stack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carlos Daniel Pohlod
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -70,33 +70,50 @@ dependencies:
|
|
|
70
70
|
- - ">="
|
|
71
71
|
- !ruby/object:Gem::Version
|
|
72
72
|
version: 1.39.1
|
|
73
|
-
description:
|
|
74
|
-
code
|
|
75
|
-
|
|
73
|
+
description: Traces a block of your Rails app and returns the call tree of your own
|
|
74
|
+
code - arguments, return values, raised exceptions and call depth - with gems, the
|
|
75
|
+
framework and stdlib filtered out. Bounded output meant to be read directly, by
|
|
76
|
+
a developer or by an AI coding agent debugging the app.
|
|
76
77
|
email: carlospohlod@gmail.com
|
|
77
78
|
executables: []
|
|
78
79
|
extensions: []
|
|
79
80
|
extra_rdoc_files: []
|
|
80
81
|
files:
|
|
82
|
+
- README.md
|
|
83
|
+
- changelog.md
|
|
84
|
+
- lib/generators/rails_tracepoint_stack/install/install_generator.rb
|
|
81
85
|
- lib/rails_tracepoint_stack.rb
|
|
82
86
|
- lib/rails_tracepoint_stack/configuration.rb
|
|
87
|
+
- lib/rails_tracepoint_stack/depth_tracker.rb
|
|
83
88
|
- lib/rails_tracepoint_stack/filter/custom_trace_selector_filter.rb
|
|
84
89
|
- lib/rails_tracepoint_stack/filter/gem_path.rb
|
|
85
90
|
- lib/rails_tracepoint_stack/filter/rb_config.rb
|
|
86
91
|
- lib/rails_tracepoint_stack/filter/trace_from_dependencies_filter.rb
|
|
87
92
|
- lib/rails_tracepoint_stack/filter/trace_from_ruby_code_filter.rb
|
|
88
93
|
- lib/rails_tracepoint_stack/filter/trace_to_ignore_filter.rb
|
|
94
|
+
- lib/rails_tracepoint_stack/limits.rb
|
|
89
95
|
- lib/rails_tracepoint_stack/log_formatter.rb
|
|
90
96
|
- lib/rails_tracepoint_stack/logger.rb
|
|
97
|
+
- lib/rails_tracepoint_stack/renderer/summary.rb
|
|
98
|
+
- lib/rails_tracepoint_stack/renderer/tree.rb
|
|
99
|
+
- lib/rails_tracepoint_stack/sink/collector.rb
|
|
100
|
+
- lib/rails_tracepoint_stack/sink/log.rb
|
|
101
|
+
- lib/rails_tracepoint_stack/skill_installer.rb
|
|
102
|
+
- lib/rails_tracepoint_stack/templates/skill.md
|
|
91
103
|
- lib/rails_tracepoint_stack/trace.rb
|
|
92
104
|
- lib/rails_tracepoint_stack/trace_filter.rb
|
|
105
|
+
- lib/rails_tracepoint_stack/trace_record.rb
|
|
106
|
+
- lib/rails_tracepoint_stack/trace_session.rb
|
|
93
107
|
- lib/rails_tracepoint_stack/tracer.rb
|
|
108
|
+
- lib/rails_tracepoint_stack/truncator.rb
|
|
94
109
|
- lib/rails_tracepoint_stack/version.rb
|
|
95
|
-
homepage: https://github.
|
|
110
|
+
homepage: https://carlosdanielpohlod.github.io/rails_tracepoint_stack/
|
|
96
111
|
licenses:
|
|
97
112
|
- MIT
|
|
98
113
|
metadata:
|
|
99
|
-
|
|
114
|
+
source_code_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack
|
|
115
|
+
documentation_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack#readme
|
|
116
|
+
bug_tracker_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/issues
|
|
100
117
|
changelog_uri: https://github.com/carlosdanielpohlod/rails_tracepoint_stack/blob/main/changelog.md
|
|
101
118
|
post_install_message:
|
|
102
119
|
rdoc_options: []
|
|
@@ -116,5 +133,6 @@ requirements: []
|
|
|
116
133
|
rubygems_version: 3.4.19
|
|
117
134
|
signing_key:
|
|
118
135
|
specification_version: 4
|
|
119
|
-
summary:
|
|
136
|
+
summary: 'Runtime call tree for a Rails app: which methods ran, with what params,
|
|
137
|
+
and what they returned.'
|
|
120
138
|
test_files: []
|