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 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Traceologist
4
+ VERSION = "0.1.0"
5
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module Traceologist
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []