traceologist 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +12 -0
- data/lib/traceologist/version.rb +5 -0
- data/lib/traceologist.rb +131 -0
- data/sig/traceologist.rbs +4 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c261c58ea4419254a9430d2ebabfda66ebf2ad5e8d9db9f95f3ec31d7563a26e
|
|
4
|
+
data.tar.gz: cd77fd9073de348dcec9306a3f51d1c84465e98c33dd192c30ff8bc06373a949
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3a5e166470e5df3db44bb7429d53fecc306a2058be77659129df6693108ef230ab5527cd4c2488fc9f9fa596afdc000550ea0552530bf3eeda46f0f0698653b0
|
|
7
|
+
data.tar.gz: 0ab1eeb6feede252012c4b5eb8c606b3f688f5e33d8f474881cd9cb57ae44b7970bf8631007e842edebe551ef8ea59c9591250e1adcc11a797a21d665742832d
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Koji NAKAMURA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Traceologist
|
|
2
|
+
|
|
3
|
+
Traceologist is a Ruby gem that traces method call sequences using `TracePoint`, returning a structured, human-readable log of calls, arguments, and return values.
|
|
4
|
+
|
|
5
|
+
It is designed for debugging and understanding runtime behavior — drop it into any block of code and get a clear picture of what methods were called, with what arguments, and what they returned.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "traceologist"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install traceologist
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require "traceologist"
|
|
27
|
+
|
|
28
|
+
class Order
|
|
29
|
+
def initialize(items)
|
|
30
|
+
@items = items
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def total
|
|
34
|
+
@items.sum { |item| item[:price] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = Traceologist.trace_sequence(filter: "Order") do
|
|
39
|
+
order = Order.new([{ price: 100 }, { price: 250 }])
|
|
40
|
+
order.total
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts result
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Output:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
-> Order(#1)#initialize
|
|
50
|
+
items: [{:price=>100}, {:price=>250}]
|
|
51
|
+
<- Order(#1)#initialize
|
|
52
|
+
=> Order(#1)
|
|
53
|
+
-> Order(#1)#total
|
|
54
|
+
<- Order(#1)#total
|
|
55
|
+
=> 350
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Options
|
|
59
|
+
|
|
60
|
+
#### `filter:` — Limit tracing to specific classes
|
|
61
|
+
|
|
62
|
+
Pass a class name prefix (or an array of prefixes) to exclude noise from the trace:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Traceologist.trace_sequence(filter: "MyApp") { ... }
|
|
66
|
+
Traceologist.trace_sequence(filter: ["MyApp::Order", "MyApp::Item"]) { ... }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Without a filter, **all** Ruby method calls within the block are traced.
|
|
70
|
+
|
|
71
|
+
#### `depth_limit:` — Cap recursion depth (default: `20`)
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
Traceologist.trace_sequence(depth_limit: 5, filter: "MyClass") { ... }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `show_location:` — Include source file and line number
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
result = Traceologist.trace_sequence(filter: "MyClass", show_location: true) do
|
|
81
|
+
MyClass.new.run
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Output includes a comment on each call line:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
-> MyClass(#1)#run # /path/to/my_class.rb:12
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Reading the output
|
|
92
|
+
|
|
93
|
+
Each element of the returned array is a string. The lines follow this format:
|
|
94
|
+
|
|
95
|
+
| Pattern | Meaning |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `-> ClassName(#N)#method` | Method call (indented by depth) |
|
|
98
|
+
| ` arg: value` | Argument name and value |
|
|
99
|
+
| `<- ClassName(#N)#method` | Method return (indented by depth) |
|
|
100
|
+
| ` => value` | Return value |
|
|
101
|
+
|
|
102
|
+
`#N` is a stable sequence number assigned to each unique object instance within the traced block. The same object always gets the same number, making it easy to follow a single instance across many calls.
|
|
103
|
+
|
|
104
|
+
Primitive values (`Integer`, `Float`, `String`, `Symbol`, `nil`, `true`, `false`) are shown with `inspect`. All other objects are shown as `ClassName(#N)`.
|
|
105
|
+
|
|
106
|
+
### Writing to a file
|
|
107
|
+
|
|
108
|
+
`trace_sequence` returns a `Traceologist::String` — a plain string that also supports `>>` for writing to a file:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
result = Traceologist.trace_sequence(filter: "MyClass") { MyClass.new.run }
|
|
112
|
+
|
|
113
|
+
result >> "trace.txt" # writes to file; raises if it already exists
|
|
114
|
+
puts result # prints to stdout like any string
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If the file already exists, `>>` raises a `RuntimeError` to avoid accidental overwrites.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
bin/setup # install dependencies
|
|
123
|
+
bundle exec rake test # run tests
|
|
124
|
+
bundle exec rubocop # lint
|
|
125
|
+
bin/console # interactive prompt
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kozy4324/traceologist.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/traceologist.rb
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "traceologist/version"
|
|
4
|
+
|
|
5
|
+
# Traceologist traces Ruby method call sequences using TracePoint,
|
|
6
|
+
# returning a structured log of calls, arguments, and return values.
|
|
7
|
+
module Traceologist
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# A String subclass returned by {trace_sequence} that supports writing to a file with `>>`.
|
|
11
|
+
class String < ::String
|
|
12
|
+
# Writes the trace to a file. Raises if the file already exists.
|
|
13
|
+
# @param path [String] destination file path
|
|
14
|
+
def >>(other)
|
|
15
|
+
raise "A file already exists!" if File.exist?(other)
|
|
16
|
+
|
|
17
|
+
File.write(other, to_s)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Traces method call sequences within the given block using TracePoint.
|
|
22
|
+
#
|
|
23
|
+
# @param depth_limit [Integer] Maximum call depth to trace (default: 20)
|
|
24
|
+
# @param filter [String, Symbol, Array<String, Symbol>, nil] Only trace classes whose name
|
|
25
|
+
# starts with one of these prefixes. When nil, all calls are traced.
|
|
26
|
+
# @param show_location [Boolean] Whether to include source file and line number (default: false)
|
|
27
|
+
# @yield The block of code to trace
|
|
28
|
+
# @return [Traceologist::String] The call sequence as a newline-joined string
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# result = Traceologist.trace_sequence(filter: "MyClass") { MyClass.new.run }
|
|
32
|
+
# puts result
|
|
33
|
+
def self.trace_sequence(depth_limit: 20, filter: nil, show_location: false, &)
|
|
34
|
+
Tracer.new(depth_limit: depth_limit, filter: filter, show_location: show_location).run(&)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @api private
|
|
38
|
+
class Tracer
|
|
39
|
+
def initialize(depth_limit:, filter:, show_location:)
|
|
40
|
+
@depth_limit = depth_limit
|
|
41
|
+
@filter = filter
|
|
42
|
+
@show_location = show_location
|
|
43
|
+
@depth = 0
|
|
44
|
+
@call_stack = []
|
|
45
|
+
@calls = []
|
|
46
|
+
@object_registry = {}.compare_by_identity
|
|
47
|
+
@next_id = 0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run(&)
|
|
51
|
+
trace_point = TracePoint.new(:call, :return) { |event| handle(event) }
|
|
52
|
+
trace_point.enable(&)
|
|
53
|
+
Traceologist::String.new(@calls.join("\n"))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def handle(event)
|
|
59
|
+
assign_seq(event.self)
|
|
60
|
+
case event.event
|
|
61
|
+
when :call then on_call(event)
|
|
62
|
+
when :return then on_return(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_call(event)
|
|
67
|
+
unless matches?(event.defined_class.to_s) && @depth <= @depth_limit
|
|
68
|
+
@call_stack << false
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@call_stack << true
|
|
73
|
+
@calls << call_line(event)
|
|
74
|
+
record_args(event)
|
|
75
|
+
@depth += 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_return(event)
|
|
79
|
+
return unless @call_stack.pop
|
|
80
|
+
|
|
81
|
+
@depth -= 1
|
|
82
|
+
indent = " " * @depth
|
|
83
|
+
@calls << "#{indent}<- #{label(event)}"
|
|
84
|
+
@calls << "#{indent} => #{format_value(event.return_value)}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def call_line(event)
|
|
88
|
+
indent = " " * @depth
|
|
89
|
+
location = @show_location ? " # #{event.path}:#{event.lineno}" : ""
|
|
90
|
+
"#{indent}-> #{label(event)}#{location}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def record_args(event)
|
|
94
|
+
indent = " " * @depth
|
|
95
|
+
args = event.parameters.filter_map do |(type, name)|
|
|
96
|
+
next if name.nil? || type == :block
|
|
97
|
+
|
|
98
|
+
val = event.binding.local_variable_get(name)
|
|
99
|
+
"#{name}: #{format_value(val)}"
|
|
100
|
+
end
|
|
101
|
+
@calls << args.map { |arg| "#{indent} #{arg}" }.join("\n") unless args.empty?
|
|
102
|
+
rescue StandardError
|
|
103
|
+
# ignore binding errors
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def label(event)
|
|
107
|
+
"#{event.defined_class}(##{@object_registry[event.self]})##{event.method_id}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def matches?(class_name)
|
|
111
|
+
@filter.nil? || Array(@filter).any? { |prefix| class_name.start_with?(prefix.to_s) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def assign_seq(obj)
|
|
115
|
+
@object_registry[obj] ||= (@next_id += 1)
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def format_value(val)
|
|
121
|
+
case val
|
|
122
|
+
when Numeric, ::String, Symbol, NilClass, TrueClass, FalseClass
|
|
123
|
+
val.inspect
|
|
124
|
+
else
|
|
125
|
+
"#{val.class}(##{assign_seq(val)})"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private_constant :Tracer
|
|
131
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: traceologist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Koji NAKAMURA
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Traceologist wraps a block of Ruby code with TracePoint and returns a
|
|
14
|
+
structured, human-readable log of every method call, its arguments, and
|
|
15
|
+
its return value. Useful for debugging and understanding runtime behavior.
|
|
16
|
+
email:
|
|
17
|
+
- kozy4324@gmail.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- Rakefile
|
|
25
|
+
- lib/traceologist.rb
|
|
26
|
+
- lib/traceologist/version.rb
|
|
27
|
+
- sig/traceologist.rbs
|
|
28
|
+
homepage: https://github.com/kozy4324/traceologist
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://github.com/kozy4324/traceologist
|
|
33
|
+
source_code_uri: https://github.com/kozy4324/traceologist
|
|
34
|
+
changelog_uri: https://github.com/kozy4324/traceologist/releases
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 3.2.0
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 4.0.3
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Trace Ruby method call sequences with arguments and return values.
|
|
53
|
+
test_files: []
|