rails_tracepoint_stack 0.4.0 → 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 +30 -0
- data/changelog.md +24 -0
- data/lib/rails_tracepoint_stack/limits.rb +4 -0
- data/lib/rails_tracepoint_stack/log_formatter.rb +26 -4
- data/lib/rails_tracepoint_stack/renderer/tree.rb +29 -2
- data/lib/rails_tracepoint_stack/sink/collector.rb +73 -4
- data/lib/rails_tracepoint_stack/templates/skill.md +20 -0
- data/lib/rails_tracepoint_stack/trace.rb +5 -2
- data/lib/rails_tracepoint_stack/trace_record.rb +15 -0
- data/lib/rails_tracepoint_stack/tracer.rb +46 -3
- data/lib/rails_tracepoint_stack/truncator.rb +35 -0
- data/lib/rails_tracepoint_stack/version.rb +1 -1
- data/lib/rails_tracepoint_stack.rb +11 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7853cf9027a5db8f962c4cf6753ae98cac10e3cdd0344b618fac59a112837e6d
|
|
4
|
+
data.tar.gz: 29beeae6b2e5eacbca3121a90bef1670e759477b5cfc48aa837dba6ebcb48fe0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49af2952c9312ea180cbf2afde354b1d9c68a0a4f6d94a991534e8f19b6fed4f477afda01cdb4d93e766751a9195f183f12f93c0fdd51705b3c984127271cb7c
|
|
7
|
+
data.tar.gz: 79ff4005947c9a73f08dc03261894e6044653c3e8e1979043546e56425d0fb718587562707957c868c55930ad02606fca3abe5edd29d19b9d1b57d0b4b69938c
|
data/README.md
CHANGED
|
@@ -100,14 +100,44 @@ A real request produces tens of thousands of traces, so captures are bounded.
|
|
|
100
100
|
| `max_traces` | 5000 | Stops collecting; `session.truncated?` becomes true |
|
|
101
101
|
| `max_string_length` | 200 | Shortens long strings |
|
|
102
102
|
| `max_collection_size` | 20 | Shortens long arrays and hashes |
|
|
103
|
+
| `max_value_length` | 1000 | Replaces any single value larger than this with its size |
|
|
103
104
|
| `capture_params` | `true` | Set false to show only the flow |
|
|
104
105
|
| `capture_return` | `true` | Set false to show only the calls |
|
|
105
106
|
| `threads` | `:current` | `:all` also records background threads |
|
|
107
|
+
| `blocks` | `false` | `true` also traces blocks — see below |
|
|
106
108
|
|
|
107
109
|
```ruby
|
|
108
110
|
RailsTracepointStack.capture(max_depth: 3, capture_return: false) { ... }
|
|
109
111
|
```
|
|
110
112
|
|
|
113
|
+
### Blocks and scopes
|
|
114
|
+
|
|
115
|
+
A Rails scope is a lambda, so the logic inside it is not a method call and does
|
|
116
|
+
not appear by default. `blocks: true` traces block entry and exit as well:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
RailsTracepointStack.capture(blocks: true) { News.latest(User.current) }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
News.latest (app/models/news.rb:88) {"user":"Anonymous"}
|
|
124
|
+
block { } (app/models/news.rb:46) {"user":"Anonymous"}
|
|
125
|
+
Project.allowed_to_condition (app/models/project.rb:189) {"permission":"view_news"}
|
|
126
|
+
↻ 40× AccessControl.permission { } (lib/redmine/access_control.rb:37) {"p":"#<Permission …>"}
|
|
127
|
+
-> false
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It is off by default because a block written inside a loop runs once per
|
|
131
|
+
element: in Redmine, one call produced 48 block events and 43 of them came from
|
|
132
|
+
a single `detect` predicate. Runs of the same block at the same depth collapse
|
|
133
|
+
into one line with a `↻ N×` count, which keeps only the first run's arguments —
|
|
134
|
+
if you need every iteration, leave blocks off and read the loop instead.
|
|
135
|
+
|
|
136
|
+
Blocks are named after the method they were written in. A block with no
|
|
137
|
+
enclosing method — a scope, a lambda held in a constant — has neither a class
|
|
138
|
+
nor a method name to show, so it renders as `block { }` plus its location.
|
|
139
|
+
```
|
|
140
|
+
|
|
111
141
|
## Tracing the whole process
|
|
112
142
|
|
|
113
143
|
For code you cannot wrap in a block — boot, a rake task, a request handled by a
|
data/changelog.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- `capture(blocks: true)` traces block entry and exit, which is what makes a
|
|
8
|
+
Rails scope, a lambda held in a constant, or any yielded block visible.
|
|
9
|
+
Off by default: a block inside a loop runs once per element. Runs of the
|
|
10
|
+
same block at the same depth collapse into one record with a `↻ N×` count
|
|
11
|
+
- `max_value_length` (default 1000) caps any single value, whatever its shape
|
|
12
|
+
|
|
13
|
+
**BugFix**
|
|
14
|
+
|
|
15
|
+
- `to_tree` raised `Encoding::CompatibilityError` whenever a traced value held
|
|
16
|
+
bytes that are not text — a file read in binary mode, a digest, a response
|
|
17
|
+
body. Strings are now converted to UTF-8 with invalid bytes replaced
|
|
18
|
+
- A class method on an ActiveRecord model rendered as its singleton class,
|
|
19
|
+
which prints the model's entire schema for a single call. The class name now
|
|
20
|
+
comes from the module's own name
|
|
21
|
+
- Per-string and per-collection limits still let one value through at thousands
|
|
22
|
+
of characters, since twenty items truncated to two hundred each is four
|
|
23
|
+
thousand
|
|
24
|
+
- The block handed to `capture` was traced as part of the result and consumed a
|
|
25
|
+
level of depth, indenting everything under it
|
|
26
|
+
|
|
3
27
|
## 0.4.0
|
|
4
28
|
|
|
5
29
|
**Features**
|
|
@@ -6,11 +6,13 @@ module RailsTracepointStack
|
|
|
6
6
|
DEFAULT_MAX_TRACES = 5_000
|
|
7
7
|
DEFAULT_MAX_STRING_LENGTH = 200
|
|
8
8
|
DEFAULT_MAX_COLLECTION_SIZE = 20
|
|
9
|
+
DEFAULT_MAX_VALUE_LENGTH = 1_000
|
|
9
10
|
|
|
10
11
|
attr_reader :max_depth,
|
|
11
12
|
:max_traces,
|
|
12
13
|
:max_string_length,
|
|
13
14
|
:max_collection_size,
|
|
15
|
+
:max_value_length,
|
|
14
16
|
:capture_params,
|
|
15
17
|
:capture_return
|
|
16
18
|
|
|
@@ -19,6 +21,7 @@ module RailsTracepointStack
|
|
|
19
21
|
max_traces: DEFAULT_MAX_TRACES,
|
|
20
22
|
max_string_length: DEFAULT_MAX_STRING_LENGTH,
|
|
21
23
|
max_collection_size: DEFAULT_MAX_COLLECTION_SIZE,
|
|
24
|
+
max_value_length: DEFAULT_MAX_VALUE_LENGTH,
|
|
22
25
|
capture_params: true,
|
|
23
26
|
capture_return: true
|
|
24
27
|
)
|
|
@@ -26,6 +29,7 @@ module RailsTracepointStack
|
|
|
26
29
|
@max_traces = max_traces
|
|
27
30
|
@max_string_length = max_string_length
|
|
28
31
|
@max_collection_size = max_collection_size
|
|
32
|
+
@max_value_length = max_value_length
|
|
29
33
|
@capture_params = capture_params
|
|
30
34
|
@capture_return = capture_return
|
|
31
35
|
end
|
|
@@ -25,14 +25,36 @@ module RailsTracepointStack
|
|
|
25
25
|
)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
REPLACEMENT = "�".freeze
|
|
29
|
+
|
|
28
30
|
def self.stringify(value)
|
|
29
31
|
return nil if value.nil?
|
|
30
32
|
|
|
31
|
-
value.to_s
|
|
33
|
+
utf8(value.to_s)
|
|
32
34
|
rescue SystemStackError, StandardError => error
|
|
33
35
|
inspect_fallback(value, error)
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Traced code holds bytes that are not text: a file read in binary mode, a
|
|
39
|
+
# digest, a response body. Left as they are, those strings poison every
|
|
40
|
+
# consumer downstream - JSON refuses to encode them, and joining them with
|
|
41
|
+
# the rest of the output raises Encoding::CompatibilityError. Normalizing
|
|
42
|
+
# here keeps the damage to the one value that caused it.
|
|
43
|
+
def self.utf8(value)
|
|
44
|
+
return value unless value.is_a?(String)
|
|
45
|
+
|
|
46
|
+
converted =
|
|
47
|
+
if value.encoding == Encoding::UTF_8
|
|
48
|
+
value
|
|
49
|
+
else
|
|
50
|
+
value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: REPLACEMENT)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
converted.valid_encoding? ? converted : converted.scrub(REPLACEMENT)
|
|
54
|
+
rescue SystemStackError, StandardError
|
|
55
|
+
value.scrub(REPLACEMENT)
|
|
56
|
+
end
|
|
57
|
+
|
|
36
58
|
def self.safe_value(value, ancestry = {})
|
|
37
59
|
case value
|
|
38
60
|
when nil, true, false, Numeric
|
|
@@ -40,7 +62,7 @@ module RailsTracepointStack
|
|
|
40
62
|
when String
|
|
41
63
|
# Callers may keep the result around after the traced code moved on,
|
|
42
64
|
# so a live reference to a mutable string would drift.
|
|
43
|
-
value.
|
|
65
|
+
utf8(value).dup.freeze
|
|
44
66
|
when Symbol
|
|
45
67
|
value.to_s
|
|
46
68
|
when Array
|
|
@@ -125,7 +147,7 @@ module RailsTracepointStack
|
|
|
125
147
|
end
|
|
126
148
|
|
|
127
149
|
def self.safe_object_string(value)
|
|
128
|
-
value.inspect
|
|
150
|
+
utf8(value.inspect)
|
|
129
151
|
rescue SystemStackError, StandardError => error
|
|
130
152
|
inspect_fallback(value, error)
|
|
131
153
|
end
|
|
@@ -146,7 +168,7 @@ module RailsTracepointStack
|
|
|
146
168
|
when true, false, Numeric
|
|
147
169
|
value.to_s
|
|
148
170
|
when String
|
|
149
|
-
value.inspect
|
|
171
|
+
utf8(value).inspect
|
|
150
172
|
when Symbol
|
|
151
173
|
":#{value}"
|
|
152
174
|
when Array
|
|
@@ -32,12 +32,39 @@ module RailsTracepointStack
|
|
|
32
32
|
def self.line_for(record)
|
|
33
33
|
case record.kind
|
|
34
34
|
when :call then call_line(record)
|
|
35
|
-
when :
|
|
35
|
+
when :b_call then block_line(record)
|
|
36
|
+
when :return, :b_return then return_line(record)
|
|
36
37
|
when :raise then raise_line(record)
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
# A block has no name of its own. TracePoint reports the method it was
|
|
42
|
+
# written in, which is the useful label; a block written at class-body
|
|
43
|
+
# level - a scope, a lambda constant - has neither, so the location is
|
|
44
|
+
# all there is to show.
|
|
45
|
+
def self.block_line(record)
|
|
46
|
+
"#{indent(record.depth)}#{repeat_marker(record)}#{block_name(record)} { } " \
|
|
47
|
+
"(#{location(record)}) #{compact(record.params)}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.block_name(record)
|
|
51
|
+
return "block" if record.class_name.nil? || record.method_name.nil?
|
|
52
|
+
|
|
53
|
+
qualified_name(record)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.repeat_marker(record)
|
|
57
|
+
count = record.repeats.to_i
|
|
58
|
+
return "" if count <= 1
|
|
59
|
+
|
|
60
|
+
"↻ #{count}× "
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Stops at the first character that cannot be part of a constant name, so
|
|
64
|
+
# a singleton class that printed extra detail after its name still
|
|
65
|
+
# resolves. Requires a leading capital, which keeps anonymous singletons
|
|
66
|
+
# like #<Class:0x00007f1234> out.
|
|
67
|
+
SINGLETON_CLASS = /\A#<Class:([A-Z][\w:]*)[\s(>]/
|
|
41
68
|
TEMPLATE_FILE = /\.(erb|haml|slim|builder|jbuilder|rabl)\z/i
|
|
42
69
|
# Everything a template receives besides the locals belongs to the
|
|
43
70
|
# rendering machinery, not to the developer.
|
|
@@ -14,6 +14,8 @@ module RailsTracepointStack
|
|
|
14
14
|
def initialize(session: RailsTracepointStack::TraceSession.new, limits: RailsTracepointStack::Limits.new)
|
|
15
15
|
@session = session
|
|
16
16
|
@limits = limits
|
|
17
|
+
@open_block = nil
|
|
18
|
+
@suppressed_returns = Hash.new(0)
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def record(trace)
|
|
@@ -24,17 +26,54 @@ module RailsTracepointStack
|
|
|
24
26
|
return
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
built = build_record(trace)
|
|
30
|
+
return if collapsed?(built)
|
|
31
|
+
|
|
32
|
+
session.add(built)
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
private
|
|
31
36
|
|
|
37
|
+
# A block written inside a loop runs once per element and reports the
|
|
38
|
+
# same location every time, which buries the trace it belongs to. Runs of
|
|
39
|
+
# the same block at the same depth become one record carrying a count,
|
|
40
|
+
# and the matching returns are dropped with them.
|
|
41
|
+
def collapsed?(record)
|
|
42
|
+
return drop_suppressed_return(record) if record.block_return?
|
|
43
|
+
return break_run(record) unless record.block_call?
|
|
44
|
+
|
|
45
|
+
key = [record.file_path, record.line_number, record.depth]
|
|
46
|
+
|
|
47
|
+
if @open_block && @open_block.first == key
|
|
48
|
+
@open_block.last.repeats += 1
|
|
49
|
+
@suppressed_returns[record.depth] += 1
|
|
50
|
+
return true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@open_block = [key, record]
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def drop_suppressed_return(record)
|
|
58
|
+
return false unless @suppressed_returns[record.depth] > 0
|
|
59
|
+
|
|
60
|
+
@suppressed_returns[record.depth] -= 1
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Anything that is not part of the block's own call/return pair ends the
|
|
65
|
+
# run, so two separate loops over the same line stay separate.
|
|
66
|
+
def break_run(record)
|
|
67
|
+
@open_block = nil unless record.returned?
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
32
71
|
def build_record(trace)
|
|
33
72
|
exception = trace.exception
|
|
34
73
|
|
|
35
74
|
RailsTracepointStack::TraceRecord.new(
|
|
36
75
|
kind: trace.kind,
|
|
37
|
-
class_name:
|
|
76
|
+
class_name: class_name_for(trace),
|
|
38
77
|
method_name: trace.method_name,
|
|
39
78
|
file_path: trace.file_path,
|
|
40
79
|
line_number: trace.line_number,
|
|
@@ -42,10 +81,40 @@ module RailsTracepointStack
|
|
|
42
81
|
return_value: return_value_for(trace),
|
|
43
82
|
exception_class: exception && exception.class.to_s,
|
|
44
83
|
exception_message: exception && RailsTracepointStack::LogFormatter.stringify(exception.message),
|
|
45
|
-
depth: trace.depth || 0
|
|
84
|
+
depth: trace.depth || 0,
|
|
85
|
+
repeats: 1
|
|
46
86
|
)
|
|
47
87
|
end
|
|
48
88
|
|
|
89
|
+
# `to_s` on a class is whatever that class decided to print. ActiveRecord
|
|
90
|
+
# makes a model's singleton class print its entire schema, which turns one
|
|
91
|
+
# method call into hundreds of characters of column definitions. A module
|
|
92
|
+
# knows its own name, so ask for that instead and only fall back to `to_s`
|
|
93
|
+
# for the anonymous ones that have none.
|
|
94
|
+
def class_name_for(trace)
|
|
95
|
+
klass = trace.class_name
|
|
96
|
+
return RailsTracepointStack::LogFormatter.stringify(klass) unless klass.is_a?(Module)
|
|
97
|
+
|
|
98
|
+
name = klass.name
|
|
99
|
+
return name if name
|
|
100
|
+
|
|
101
|
+
attached = attached_module(klass)
|
|
102
|
+
return "#<Class:#{attached.name}>" if attached&.name
|
|
103
|
+
|
|
104
|
+
RailsTracepointStack::LogFormatter.stringify(klass)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Singleton classes have no name of their own. Ruby 3.2 can hand back the
|
|
108
|
+
# object they belong to; older versions leave only the printed form.
|
|
109
|
+
def attached_module(klass)
|
|
110
|
+
return nil unless klass.respond_to?(:attached_object)
|
|
111
|
+
|
|
112
|
+
attached = klass.attached_object
|
|
113
|
+
attached.is_a?(Module) ? attached : nil
|
|
114
|
+
rescue SystemStackError, StandardError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
49
118
|
def params_for(trace)
|
|
50
119
|
return {} unless limits.capture_params
|
|
51
120
|
|
|
@@ -59,7 +128,7 @@ module RailsTracepointStack
|
|
|
59
128
|
end
|
|
60
129
|
|
|
61
130
|
def snapshot(value)
|
|
62
|
-
RailsTracepointStack::Truncator.
|
|
131
|
+
RailsTracepointStack::Truncator.bounded(
|
|
63
132
|
RailsTracepointStack::LogFormatter.safe_value(value),
|
|
64
133
|
limits
|
|
65
134
|
)
|
|
@@ -75,9 +75,29 @@ unbounded capture of a real request is tens of thousands of lines.
|
|
|
75
75
|
| `max_traces:` | 5000 | Cap total lines |
|
|
76
76
|
| `max_string_length:` | 200 | Keep big payloads readable |
|
|
77
77
|
| `max_collection_size:` | 20 | Keep loaded associations readable |
|
|
78
|
+
| `max_value_length:` | 1000 | Cap any single value, whatever its shape |
|
|
78
79
|
| `capture_params: false` | on | Show only the flow |
|
|
79
80
|
| `capture_return: false` | on | Show only the calls |
|
|
80
81
|
| `threads: :all` | current only | Include background threads |
|
|
82
|
+
| `blocks: true` | off | Also trace blocks — see below |
|
|
83
|
+
|
|
84
|
+
### When a scope or lambda seems to do nothing
|
|
85
|
+
|
|
86
|
+
A Rails scope is a lambda, not a method, so by default nothing inside it shows
|
|
87
|
+
up — you see the call that used the scope and then its result, with no steps in
|
|
88
|
+
between. Same for any `yield`ed block. Add `blocks: true`:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
RailsTracepointStack.capture(blocks: true) { News.latest(User.current) }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Blocks render as `Class#method { }`, or as `block { }` plus a location when the
|
|
95
|
+
block has no enclosing method (which is what a scope looks like). A line like
|
|
96
|
+
`↻ 40× ... { }` means that block ran 40 times from the same place — a predicate
|
|
97
|
+
inside a loop — and only the first run's arguments were kept.
|
|
98
|
+
|
|
99
|
+
Leave it off unless you need it. A block inside a loop fires once per element,
|
|
100
|
+
so this costs far more traces than watching methods does.
|
|
81
101
|
|
|
82
102
|
To narrow by file instead, set patterns before capturing:
|
|
83
103
|
|
|
@@ -20,14 +20,17 @@ module RailsTracepointStack
|
|
|
20
20
|
trace_point.event
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
ENTERING = [:call, :b_call].freeze
|
|
24
|
+
LEAVING = [:return, :b_return].freeze
|
|
25
|
+
|
|
23
26
|
def params
|
|
24
|
-
return {} unless kind
|
|
27
|
+
return {} unless ENTERING.include?(kind)
|
|
25
28
|
|
|
26
29
|
@params ||= fetch_params(trace_point)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
def return_value
|
|
30
|
-
return nil unless kind
|
|
33
|
+
return nil unless LEAVING.include?(kind)
|
|
31
34
|
|
|
32
35
|
trace_point.return_value
|
|
33
36
|
end
|
|
@@ -13,6 +13,7 @@ module RailsTracepointStack
|
|
|
13
13
|
:exception_class,
|
|
14
14
|
:exception_message,
|
|
15
15
|
:depth,
|
|
16
|
+
:repeats,
|
|
16
17
|
keyword_init: true
|
|
17
18
|
) do
|
|
18
19
|
def call?
|
|
@@ -26,5 +27,19 @@ module RailsTracepointStack
|
|
|
26
27
|
def raise?
|
|
27
28
|
kind == :raise
|
|
28
29
|
end
|
|
30
|
+
|
|
31
|
+
def block_call?
|
|
32
|
+
kind == :b_call
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def block_return?
|
|
36
|
+
kind == :b_return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# True for anything that produced a value on the way out, so callers do not
|
|
40
|
+
# have to know whether a method or a block produced it.
|
|
41
|
+
def returned?
|
|
42
|
+
return? || block_return?
|
|
43
|
+
end
|
|
29
44
|
end
|
|
30
45
|
end
|
|
@@ -23,10 +23,22 @@ module RailsTracepointStack
|
|
|
23
23
|
# A nil thread watches every thread in the process, which is what the
|
|
24
24
|
# global tracer wants. Passing one confines tracing to it, so a capture
|
|
25
25
|
# running inside a threaded server does not pick up other requests.
|
|
26
|
-
|
|
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
|
+
)
|
|
27
36
|
@sink = sink
|
|
28
37
|
@events = events
|
|
29
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)
|
|
30
42
|
@filtered_count = 0
|
|
31
43
|
generate_tracer
|
|
32
44
|
end
|
|
@@ -39,6 +51,8 @@ module RailsTracepointStack
|
|
|
39
51
|
|
|
40
52
|
trace = RailsTracepointStack::Trace.new(trace_point: tracepoint)
|
|
41
53
|
|
|
54
|
+
next if own_block?(trace)
|
|
55
|
+
|
|
42
56
|
if ignore_trace?(trace: trace)
|
|
43
57
|
@filtered_count += 1
|
|
44
58
|
next
|
|
@@ -49,6 +63,35 @@ module RailsTracepointStack
|
|
|
49
63
|
end
|
|
50
64
|
end
|
|
51
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?
|
|
71
|
+
|
|
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
|
|
76
|
+
end
|
|
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
|
+
|
|
52
95
|
def out_of_scope_thread?
|
|
53
96
|
!@thread.nil? && Thread.current != @thread
|
|
54
97
|
end
|
|
@@ -61,8 +104,8 @@ module RailsTracepointStack
|
|
|
61
104
|
raw_position = raw_stack_position
|
|
62
105
|
|
|
63
106
|
case trace.kind
|
|
64
|
-
when :call then tracker.enter(raw_position)
|
|
65
|
-
when :return then tracker.leave(raw_position)
|
|
107
|
+
when :call, :b_call then tracker.enter(raw_position)
|
|
108
|
+
when :return, :b_return then tracker.leave(raw_position)
|
|
66
109
|
when :raise then tracker.raised(raw_position)
|
|
67
110
|
end
|
|
68
111
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module RailsTracepointStack
|
|
2
4
|
# Shrinks an already-serialized value so one fat argument (a big payload, a
|
|
3
5
|
# long SQL string, a loaded association) cannot dominate the output. Runs
|
|
@@ -6,6 +8,39 @@ module RailsTracepointStack
|
|
|
6
8
|
module Truncator
|
|
7
9
|
ELLIPSIS = "…".freeze
|
|
8
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
|
+
|
|
9
44
|
def self.call(value, limits)
|
|
10
45
|
case value
|
|
11
46
|
when String then truncate_string(value, limits)
|
|
@@ -22,11 +22,16 @@ module RailsTracepointStack
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
CAPTURE_EVENTS = [:call, :return, :raise].freeze
|
|
25
|
+
BLOCK_EVENTS = [:b_call, :b_return].freeze
|
|
25
26
|
|
|
26
27
|
# Traces a block and hands back everything that happened inside it, instead
|
|
27
28
|
# of writing to a log. Meant to be run as a one-off: capture, read, done.
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
30
35
|
|
|
31
36
|
collector = RailsTracepointStack::Sink::Collector.new(
|
|
32
37
|
limits: RailsTracepointStack::Limits.new(**limit_options)
|
|
@@ -34,13 +39,14 @@ module RailsTracepointStack
|
|
|
34
39
|
session = collector.session
|
|
35
40
|
tracer = RailsTracepointStack::Tracer.new(
|
|
36
41
|
sink: collector,
|
|
37
|
-
events: CAPTURE_EVENTS,
|
|
38
|
-
thread: (threads == :all) ? nil : Thread.current
|
|
42
|
+
events: blocks ? CAPTURE_EVENTS + BLOCK_EVENTS : CAPTURE_EVENTS,
|
|
43
|
+
thread: (threads == :all) ? nil : Thread.current,
|
|
44
|
+
own_block: traced
|
|
39
45
|
)
|
|
40
46
|
|
|
41
47
|
tracer.enable
|
|
42
48
|
begin
|
|
43
|
-
session.result =
|
|
49
|
+
session.result = traced.call(session)
|
|
44
50
|
rescue Exception => error
|
|
45
51
|
session.error = error
|
|
46
52
|
raise
|
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-07-
|
|
11
|
+
date: 2026-07-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|