ruby_method_tracer 0.3.3 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d467252ce5eedf8fe0111fe1afb3983d0d4a80ad7558940908543804e963b58
4
- data.tar.gz: e650becb6eee60e5bc118cf287a8dc05cbafbcb404536f1b2ab2aba2072f88f9
3
+ metadata.gz: 172daf6a55bb4350983cdf15aafbe4b954f418183401ef2be69c6b68c31a575d
4
+ data.tar.gz: '019cd6fd2053967bff554ae244eed994c614a585b16d16f1509e57a5ec5a444e'
5
5
  SHA512:
6
- metadata.gz: 890623622e3f183687caba11a65577d20a82f9fc0032da94f8471b2947a2a0c805df3492b686623c3384584e338d625710531b37ca35adf7a251451fb0907e9f
7
- data.tar.gz: 5cac3c1469df35c933e45a525e9384a7411553059633e98762fffdc1d56afe2eca1ab73b74d12c975a1d6dc2d6a7cd87720f3619aec9beffdf38d41a67961f0c
6
+ metadata.gz: 86454302178091a987db42dc9eff6e3fd320822ec38c29b27b35c7c0e92ffc16b4f2844131716202969929df8c90f3870e5b0a403ad290df7339d4fc986d2dc9
7
+ data.tar.gz: 3acee9f7dd4398d23ce23d34150dd88e258b9fe09cc420f364132d1fbdea4fb6bed07cdcef7a2710b2264763fb83b7dc8365534991c3057488aa3f41e3d45440
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-06-09
4
+
5
+ ### Added
6
+ - Global configuration via `RubyMethodTracer.configure { |c| ... }`, with `RubyMethodTracer.configuration` and `RubyMethodTracer.reset_configuration!`. Tracers created through the mixin now use these as defaults; explicit per-tracer options still take precedence.
7
+ - `Formatters::JsonFormatter` — serializes flat results or a call tree to JSON. Method arguments are never captured; exceptions are reduced to class + message; backtraces are opt-in (`include_backtrace:`) and length-bounded (`backtrace_limit:`). Uses `JSON.generate` only (no `Marshal`/`eval`/`YAML`).
8
+ - `Formatters::FlatFormatter` — renders an aggregated text table (method, calls, total, avg, errors) sorted by total time.
9
+ - `Exportable` mixin adding `render(format:)` and `export(path, format:)` to both tracers. Supported formats: `:json`, `:flat`, and `:tree` (EnhancedTracer only).
10
+
11
+ ### Security
12
+ - File export never invokes a shell and never interpolates the path into a command; it writes via `File.open` with `O_NOFOLLOW`, requires the destination directory to already exist (no recursive mkdir of attacker-influenced paths), and refuses to write through an existing symlink.
13
+ - Export format dispatch compares on the string form to avoid interning arbitrary symbols from potentially untrusted input.
14
+
3
15
  ## [0.3.3] - 2026-06-08
4
16
 
5
17
  ### Changed
data/CLAUDE.md ADDED
@@ -0,0 +1,61 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bundle exec rspec # Run all tests (also generates coverage report in coverage/)
9
+ bundle exec rubocop # Lint
10
+ bundle exec rake # Run both spec and rubocop (default task)
11
+
12
+ # Run a single spec file
13
+ bundle exec rspec spec/ruby_method_tracer/simple_tracer_spec.rb
14
+
15
+ # Run a specific example by description
16
+ bundle exec rspec spec/ruby_method_tracer/simple_tracer_spec.rb -e "records call details"
17
+
18
+ # Install gem locally for manual testing
19
+ bundle exec rake install
20
+
21
+ # Release: bump version in lib/ruby_method_tracer/version.rb, then:
22
+ bundle exec rake release
23
+ ```
24
+
25
+ ## Commit conventions
26
+
27
+ - Do NOT add a `Co-authored-by:` trailer (or any "Generated with"/agent attribution) to commit messages.
28
+
29
+ ## Architecture
30
+
31
+ The gem provides two ways to trace methods:
32
+
33
+ 1. **Mixin API** — `include RubyMethodTracer` in a class, then call `trace_methods(:method_name, **opts)` at the class level. This creates a `SimpleTracer` internally and is the simplest entry point.
34
+
35
+ 2. **Direct tracer API** — instantiate `SimpleTracer` or `EnhancedTracer` directly for programmatic access to results.
36
+
37
+ ### Tracing mechanism (SimpleTracer)
38
+
39
+ `SimpleTracer#trace_method` wraps a method by:
40
+ 1. Detecting visibility (public/protected/private) via `method_defined?` etc.
41
+ 2. Aliasing the original to `__ruby_method_tracer_original_<name>__`
42
+ 3. Redefining the method with a proc that times the call and delegates to the alias
43
+ 4. Restoring the original visibility
44
+
45
+ The wrapper uses `Thread.current[:__ruby_method_tracer_in_trace]` as a reentrancy flag to avoid recursive double-recording. Kwargs are forwarded conditionally (`kwargs.empty?` check) to stay compatible with Ruby 3.x.
46
+
47
+ Results are stored in `@calls` (an Array) guarded by a `Mutex`. The `max_calls` option enforces a sliding window by `shift`-ing the oldest entry.
48
+
49
+ ### EnhancedTracer
50
+
51
+ Inherits from `SimpleTracer` and adds call-tree tracking via `CallTree`. It overrides `trace_method` to use a per-method reentrancy key (`__ruby_method_tracer_in_trace_<method_name>`) so that *different* wrapped methods can nest inside each other (unlike `SimpleTracer` which blocks all nesting).
52
+
53
+ ### CallTree
54
+
55
+ Stack-based hierarchy tracker. `start_call` pushes a record (with a `parent` pointer and `children` array) onto `@call_stack`; `end_call` pops it, fills timing/status, and appends to the flat `@calls` list. Root-level calls (depth 0) are also collected in `@root_calls` for tree rendering.
56
+
57
+ ### Formatters
58
+
59
+ `Formatters::BaseFormatter` — abstract base providing `format_time` and `colorize` (ANSI).
60
+
61
+ `Formatters::TreeFormatter < BaseFormatter` — renders a `CallTree` as an ASCII tree with `└──`/`├──` connectors, then appends a statistics block (slowest methods, most-called methods).
data/README.md CHANGED
@@ -8,7 +8,9 @@ RubyMethodTracer is a lightweight Ruby mixin for targeted method tracing. It wra
8
8
  ## Highlights
9
9
  - Wrap only the methods you care about; public, protected, and private methods are supported.
10
10
  - Records duration, success/error state, and timestamps with thread-safe storage.
11
- - **NEW: Hierarchical call tree visualization** to understand nested method calls and dependencies.
11
+ - **Hierarchical call tree visualization** to understand nested method calls and dependencies.
12
+ - **NEW: JSON and flat-table formatters plus file export**, alongside the existing tree output.
13
+ - **NEW: Global configuration** via `RubyMethodTracer.configure` for process-wide defaults.
12
14
  - Configurable threshold to ignore fast calls and optional log streaming via `Logger`.
13
15
  - Zero dependencies beyond the Ruby standard library, keeping overhead minimal.
14
16
 
@@ -214,6 +216,51 @@ Most Called Methods:
214
216
  - Color-coded output for better readability
215
217
 
216
218
 
219
+ ### Global Configuration
220
+
221
+ Set process-wide defaults once at boot. Per-tracer options always override these globals.
222
+
223
+ ```ruby
224
+ RubyMethodTracer.configure do |config|
225
+ config.threshold = 0.005 # record calls slower than 5ms
226
+ config.auto_output = false
227
+ config.max_calls = 1000
228
+ config.logger = Rails.logger if defined?(Rails)
229
+ config.track_hierarchy = true # EnhancedTracer call-tree tracking
230
+ end
231
+ ```
232
+
233
+ `RubyMethodTracer.reset_configuration!` restores the built-in defaults (useful in tests).
234
+
235
+ ### Reporting & Export
236
+
237
+ Both tracers can render their results to a string or write them to a file via the formatter layer.
238
+
239
+ ```ruby
240
+ tracer = RubyMethodTracer::EnhancedTracer.new(OrderProcessor, threshold: 0.0)
241
+ tracer.trace_method(:process_order)
242
+ OrderProcessor.new.process_order(order)
243
+
244
+ # Render to a string
245
+ puts tracer.render(format: :flat) # aggregated table
246
+ json = tracer.render(format: :json, pretty: true)
247
+ tree = tracer.render(format: :tree) # EnhancedTracer only
248
+
249
+ # Write to a file (returns the absolute path written)
250
+ tracer.export("tmp/trace.json", format: :json)
251
+ tracer.export("tmp/trace.txt", format: :flat, colorize: false)
252
+ ```
253
+
254
+ Supported formats: `:json`, `:flat`, and `:tree` (EnhancedTracer only).
255
+
256
+ **JSON options**
257
+
258
+ - `pretty` (Boolean, default `false`): pretty-print the JSON.
259
+ - `include_backtrace` (Boolean, default `false`): include exception backtraces. Off by default to avoid leaking internal paths.
260
+ - `backtrace_limit` (Integer, default `10`): maximum backtrace lines when `include_backtrace` is enabled.
261
+
262
+ > Security/privacy: method **arguments are never captured**, so secrets passed as parameters are not recorded. Exceptions are reduced to class + message (backtrace is opt-in). Export never invokes a shell, requires the target directory to already exist, and refuses to write through a symlink. Call `render`/`export` when tracing is quiescent (after the traced work completes).
263
+
217
264
  ### Options (SimpleTracer)
218
265
 
219
266
  - `threshold` (Float, default `0.001`): minimum duration (in seconds) to record.
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMethodTracer
4
+ # Configuration holds global defaults for tracers created through the
5
+ # mixin API. Per-tracer options passed to `trace_methods` or to a tracer's
6
+ # constructor always take precedence over these globals.
7
+ #
8
+ # Configure once during application boot:
9
+ #
10
+ # RubyMethodTracer.configure do |config|
11
+ # config.threshold = 0.005
12
+ # config.max_calls = 500
13
+ # config.auto_output = true
14
+ # end
15
+ #
16
+ # The object is intended to be set at boot and then read concurrently.
17
+ # Mutating it after threads are tracing is not recommended.
18
+ class Configuration
19
+ # Minimum duration (seconds) a call must take to be recorded.
20
+ attr_accessor :threshold
21
+ # When true, each recorded call is emitted to the logger.
22
+ attr_accessor :auto_output
23
+ # Maximum number of calls retained in memory (sliding window).
24
+ attr_accessor :max_calls
25
+ # Logger instance used for auto output. Nil means each tracer builds its own.
26
+ attr_accessor :logger
27
+ # Whether EnhancedTracer builds a hierarchical call tree.
28
+ attr_accessor :track_hierarchy
29
+
30
+ def initialize
31
+ reset!
32
+ end
33
+
34
+ # Restore all settings to their built-in defaults.
35
+ def reset!
36
+ @threshold = 0.001
37
+ @auto_output = false
38
+ @max_calls = 1000
39
+ @logger = nil
40
+ @track_hierarchy = true
41
+ self
42
+ end
43
+
44
+ # Snapshot of the option keys consumed by the tracers.
45
+ #
46
+ # @return [Hash]
47
+ def to_h
48
+ {
49
+ threshold: @threshold,
50
+ auto_output: @auto_output,
51
+ max_calls: @max_calls,
52
+ logger: @logger,
53
+ track_hierarchy: @track_hierarchy
54
+ }
55
+ end
56
+ end
57
+ end
@@ -83,6 +83,17 @@ module RubyMethodTracer
83
83
 
84
84
  private
85
85
 
86
+ # Expose the call tree so JSON/flat exports include hierarchy + statistics.
87
+ def report_source
88
+ @call_tree
89
+ end
90
+
91
+ def build_formatter(format)
92
+ return Formatters::TreeFormatter.new if format.to_sym == :tree
93
+
94
+ super
95
+ end
96
+
86
97
  def build_enhanced_wrapper(aliased, method_name, key, tracer)
87
98
  track_hierarchy = tracer.instance_variable_get(:@track_hierarchy)
88
99
  # Use method-specific key to prevent only SELF-recursion, not all nested calls
@@ -139,7 +150,7 @@ module RubyMethodTracer
139
150
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
140
151
 
141
152
  def default_options
142
- super.merge(track_hierarchy: true)
153
+ super.merge(track_hierarchy: RubyMethodTracer.configuration.track_hierarchy)
143
154
  end
144
155
  end
145
156
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMethodTracer
4
+ # Exportable provides render/export helpers shared by the tracers.
5
+ #
6
+ # Host classes must implement a private `#report_source` returning the data
7
+ # handed to formatters (a results hash or a CallTree).
8
+ #
9
+ # Security notes:
10
+ # - `export` never invokes a shell and never interpolates the path into a
11
+ # command; it writes via `File.open` with `O_NOFOLLOW`.
12
+ # - The destination directory must already exist (no recursive mkdir of
13
+ # attacker-influenced paths) and the tracer refuses to write through an
14
+ # existing symlink, guarding against symlink-redirection.
15
+ #
16
+ # Concurrency: `render`/`export` read the tracer's in-memory buffers and are
17
+ # intended to be called when tracing is quiescent (e.g. after the traced work
18
+ # has finished). The flat results path is snapshotted under a lock; the call
19
+ # tree is read via its locked hierarchy/statistics accessors.
20
+ module Exportable
21
+ # Render the current results to a string.
22
+ #
23
+ # @param format [Symbol] :json, :flat (and :tree for EnhancedTracer)
24
+ # @return [String]
25
+ def render(format: :json, **opts)
26
+ build_formatter(format).format(report_source, opts)
27
+ end
28
+
29
+ # Render results and write them to a file.
30
+ #
31
+ # @param path [String] Destination file path
32
+ # @param format [Symbol] :json, :flat (and :tree for EnhancedTracer)
33
+ # @return [String] The absolute path written
34
+ def export(path, format: :json, **opts)
35
+ write_export(path, render(format: format, **opts))
36
+ end
37
+
38
+ private
39
+
40
+ # Compare on the string form so an unknown format never interns an
41
+ # arbitrary symbol (avoids unbounded symbol-table growth if a caller
42
+ # forwards untrusted input as the format).
43
+ def build_formatter(format)
44
+ case format.to_s
45
+ when "json" then Formatters::JsonFormatter.new
46
+ when "flat" then Formatters::FlatFormatter.new
47
+ else raise ArgumentError, "unknown export format: #{format.inspect}"
48
+ end
49
+ end
50
+
51
+ def write_export(path, content)
52
+ safe_path = validate_export_path(path)
53
+ # O_NOFOLLOW makes the open fail if the final component is a symlink,
54
+ # closing the check-then-write race left by the stat-based guard below.
55
+ flags = File::WRONLY | File::CREAT | File::TRUNC | File::NOFOLLOW
56
+ File.open(safe_path, flags) { |file| file.write(content) }
57
+ safe_path
58
+ rescue Errno::ELOOP
59
+ raise ArgumentError, "refusing to write through symlink: #{path}"
60
+ end
61
+
62
+ def validate_export_path(path)
63
+ raise ArgumentError, "export path must be provided" if path.nil? || path.to_s.strip.empty?
64
+
65
+ expanded = File.expand_path(path.to_s)
66
+ dir = File.dirname(expanded)
67
+ raise ArgumentError, "export directory does not exist: #{dir}" unless File.directory?(dir)
68
+ raise ArgumentError, "refusing to overwrite symlink: #{expanded}" if File.symlink?(expanded)
69
+
70
+ expanded
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_formatter"
4
+
5
+ module RubyMethodTracer
6
+ module Formatters
7
+ # FlatFormatter renders trace data as a flat, aggregated text table:
8
+ # one row per unique method with call count, total time, average time,
9
+ # and error count, sorted by total time descending.
10
+ #
11
+ # Accepts either a CallTree or a flat results hash from
12
+ # `SimpleTracer#fetch_results`.
13
+ class FlatFormatter < BaseFormatter
14
+ HEADERS = %w[Method Calls Total Avg Errors].freeze
15
+
16
+ def format(data, options = {})
17
+ opts = default_options.merge(options)
18
+ calls = extract_calls(data)
19
+ return "No method calls recorded.\n" if calls.empty?
20
+
21
+ rows = build_rows(calls)
22
+ render(rows, opts)
23
+ end
24
+
25
+ private
26
+
27
+ def default_options
28
+ { colorize: true }
29
+ end
30
+
31
+ def extract_calls(data)
32
+ if data.respond_to?(:call_hierarchy)
33
+ # Read the call tree through its lock-protected accessor, then flatten
34
+ # the hierarchy into a single list of calls.
35
+ data.call_hierarchy.flat_map { |node| flatten_node(node) }
36
+ elsif data.is_a?(Hash)
37
+ data[:calls] || []
38
+ else
39
+ []
40
+ end
41
+ end
42
+
43
+ def flatten_node(node)
44
+ [node, *(node[:children] || []).flat_map { |child| flatten_node(child) }]
45
+ end
46
+
47
+ def build_rows(calls)
48
+ rows = aggregate(calls).map { |name, agg| build_row(name, agg) }
49
+ rows.sort_by { |row| -row[:total] }
50
+ end
51
+
52
+ def build_row(name, agg)
53
+ {
54
+ method: name,
55
+ calls: agg[:count],
56
+ total: agg[:total_time],
57
+ avg: agg[:total_time] / agg[:count],
58
+ errors: agg[:errors]
59
+ }
60
+ end
61
+
62
+ def aggregate(calls)
63
+ stats = Hash.new { |h, k| h[k] = { count: 0, total_time: 0.0, errors: 0 } }
64
+ calls.each do |call|
65
+ agg = stats[call[:method_name]]
66
+ agg[:count] += 1
67
+ agg[:total_time] += call[:execution_time].to_f
68
+ agg[:errors] += 1 if call[:status] == :error
69
+ end
70
+ stats
71
+ end
72
+
73
+ def render(rows, opts)
74
+ cell_rows = rows.map { |row| cells_for(row) }
75
+ widths = column_widths(cell_rows)
76
+ lines = [align(HEADERS, widths), separator(widths)]
77
+ rows.zip(cell_rows).each { |row, cells| lines << data_line(row, cells, widths, opts) }
78
+ "#{lines.join("\n")}\n"
79
+ end
80
+
81
+ # Plain (uncolored) cell strings, used for both width calc and rendering.
82
+ def cells_for(row)
83
+ [row[:method], row[:calls].to_s, format_time(row[:total]), format_time(row[:avg]), row[:errors].to_s]
84
+ end
85
+
86
+ def column_widths(cell_rows)
87
+ HEADERS.each_index.map do |i|
88
+ (cell_rows.map { |cells| cells[i].length } + [HEADERS[i].length]).max
89
+ end
90
+ end
91
+
92
+ def align(cells, widths)
93
+ cells.each_index.map { |i| pad(cells[i], widths[i], i.zero?) }.join(" ")
94
+ end
95
+
96
+ # Justify a plain cell, then wrap it in color so ANSI codes never affect
97
+ # the computed column width.
98
+ def data_line(row, cells, widths, opts)
99
+ cells.each_index.map { |i| color_cell(pad(cells[i], widths[i], i.zero?), i, row, opts) }.join(" ")
100
+ end
101
+
102
+ def pad(text, width, left)
103
+ left ? text.ljust(width) : text.rjust(width)
104
+ end
105
+
106
+ def color_cell(padded, index, row, opts)
107
+ return padded unless opts[:colorize]
108
+ return colorize(padded, :cyan) if index.zero?
109
+ return colorize(padded, :red) if index == 4 && row[:errors].positive?
110
+
111
+ padded
112
+ end
113
+
114
+ def separator(widths)
115
+ widths.map { |w| "-" * w }.join(" ")
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "base_formatter"
6
+
7
+ module RubyMethodTracer
8
+ module Formatters
9
+ # JsonFormatter serializes trace data to JSON.
10
+ #
11
+ # Security/privacy notes:
12
+ # - Serialization uses `JSON.generate` only; no `Marshal`/`eval`/`YAML` is
13
+ # involved, so the output cannot be used as a deserialization gadget.
14
+ # - Method arguments are never captured or emitted, avoiding accidental
15
+ # leakage of secrets passed as parameters.
16
+ # - Exceptions are reduced to their class name and message. Backtraces are
17
+ # opt-in (`include_backtrace: true`) and truncated to `backtrace_limit`
18
+ # lines to avoid leaking large amounts of internal path information.
19
+ #
20
+ # Accepts either a CallTree (serializes hierarchy + statistics) or a flat
21
+ # results hash as produced by `SimpleTracer#fetch_results`.
22
+ class JsonFormatter < BaseFormatter
23
+ def format(data, options = {})
24
+ opts = default_options.merge(options)
25
+ payload = build_payload(data, opts)
26
+ opts[:pretty] ? JSON.pretty_generate(payload) : JSON.generate(payload)
27
+ end
28
+
29
+ private
30
+
31
+ def default_options
32
+ {
33
+ pretty: false,
34
+ include_backtrace: false,
35
+ backtrace_limit: 10
36
+ }
37
+ end
38
+
39
+ def build_payload(data, opts)
40
+ if data.respond_to?(:call_hierarchy) && data.respond_to?(:statistics)
41
+ {
42
+ generated_at: Time.now.utc.iso8601,
43
+ call_hierarchy: data.call_hierarchy.map { |node| serialize_node(node, opts) },
44
+ statistics: serialize_statistics(data.statistics)
45
+ }
46
+ else
47
+ serialize_flat(data, opts)
48
+ end
49
+ end
50
+
51
+ def serialize_flat(results, opts)
52
+ results = {} unless results.is_a?(Hash)
53
+ calls = results[:calls] || []
54
+ {
55
+ generated_at: Time.now.utc.iso8601,
56
+ total_calls: results[:total_calls] || calls.size,
57
+ total_time: results[:total_time] || 0.0,
58
+ calls: calls.map { |call| serialize_call(call, opts) }
59
+ }
60
+ end
61
+
62
+ def serialize_call(call, opts)
63
+ {
64
+ method_name: call[:method_name],
65
+ execution_time: call[:execution_time],
66
+ status: call[:status],
67
+ error: serialize_error(call[:error], opts),
68
+ timestamp: iso8601(call[:timestamp])
69
+ }
70
+ end
71
+
72
+ def serialize_node(node, opts)
73
+ {
74
+ method_name: node[:method_name],
75
+ execution_time: node[:execution_time],
76
+ status: node[:status],
77
+ depth: node[:depth],
78
+ error: serialize_error(node[:error], opts),
79
+ timestamp: iso8601(node[:timestamp]),
80
+ children: (node[:children] || []).map { |child| serialize_node(child, opts) }
81
+ }
82
+ end
83
+
84
+ def serialize_statistics(stats)
85
+ stats
86
+ end
87
+
88
+ # Reduce an exception to a safe, bounded representation.
89
+ def serialize_error(error, opts)
90
+ return nil unless error
91
+
92
+ result = { class: error.class.name, message: error.message.to_s }
93
+ if opts[:include_backtrace] && error.backtrace
94
+ limit = opts[:backtrace_limit].to_i
95
+ result[:backtrace] = limit.positive? ? error.backtrace.first(limit) : []
96
+ end
97
+ result
98
+ end
99
+
100
+ def iso8601(time)
101
+ time.respond_to?(:iso8601) ? time.iso8601(6) : time
102
+ end
103
+ end
104
+ end
105
+ end
@@ -3,6 +3,7 @@
3
3
  require "set"
4
4
  require "logger"
5
5
  require_relative "formatters/base_formatter"
6
+ require_relative "exportable"
6
7
 
7
8
  module RubyMethodTracer
8
9
  # SimpleTracer wraps instance methods on a target class and records
@@ -20,7 +21,10 @@ module RubyMethodTracer
20
21
  # tracer = RubyMethodTracer::SimpleTracer.new(MyClass, threshold: 0.005)
21
22
  # tracer.trace_method(:expensive_call)
22
23
  # results = tracer.fetch_results
24
+ # rubocop:disable Metrics/ClassLength
23
25
  class SimpleTracer
26
+ include Exportable
27
+
24
28
  def initialize(target_class, **options)
25
29
  @target_class = target_class
26
30
  @options = default_options.merge(options)
@@ -88,12 +92,19 @@ module RubyMethodTracer
88
92
 
89
93
  private
90
94
 
95
+ # Data passed to formatters by Exportable. Overridden by EnhancedTracer to
96
+ # expose the call tree.
97
+ def report_source
98
+ fetch_results
99
+ end
100
+
91
101
  def default_options
102
+ config = RubyMethodTracer.configuration
92
103
  {
93
- threshold: 0.001,
94
- auto_output: false,
95
- max_calls: 1000,
96
- logger: nil
104
+ threshold: config.threshold,
105
+ auto_output: config.auto_output,
106
+ max_calls: config.max_calls,
107
+ logger: config.logger
97
108
  }
98
109
  end
99
110
 
@@ -172,4 +183,5 @@ module RubyMethodTracer
172
183
  @formatter.colorize(text, color)
173
184
  end
174
185
  end
186
+ # rubocop:enable Metrics/ClassLength
175
187
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyMethodTracer
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ruby_method_tracer/version"
4
+ require_relative "ruby_method_tracer/configuration"
4
5
  require_relative "ruby_method_tracer/formatters/base_formatter"
5
6
  require_relative "ruby_method_tracer/simple_tracer"
6
7
  require_relative "ruby_method_tracer/call_tree"
7
8
  require_relative "ruby_method_tracer/enhanced_tracer"
8
9
  require_relative "ruby_method_tracer/formatters/tree_formatter"
10
+ require_relative "ruby_method_tracer/formatters/json_formatter"
11
+ require_relative "ruby_method_tracer/formatters/flat_formatter"
9
12
 
10
13
  # Public: Mixin that adds lightweight method tracing to classes.
11
14
  #
@@ -26,6 +29,39 @@ require_relative "ruby_method_tracer/formatters/tree_formatter"
26
29
  module RubyMethodTracer
27
30
  class Error < StandardError; end
28
31
 
32
+ @configuration = Configuration.new
33
+ @configuration_mutex = Mutex.new
34
+
35
+ class << self
36
+ # Global configuration shared as defaults by tracers created via the mixin.
37
+ #
38
+ # @return [RubyMethodTracer::Configuration]
39
+ attr_reader :configuration
40
+
41
+ # Yield the global configuration for mutation. Intended to be called once
42
+ # at application boot.
43
+ #
44
+ # RubyMethodTracer.configure do |config|
45
+ # config.threshold = 0.005
46
+ # end
47
+ #
48
+ # @yieldparam config [RubyMethodTracer::Configuration]
49
+ # @return [RubyMethodTracer::Configuration]
50
+ def configure
51
+ @configuration_mutex.synchronize do
52
+ yield(@configuration) if block_given?
53
+ end
54
+ @configuration
55
+ end
56
+
57
+ # Reset the global configuration back to built-in defaults.
58
+ #
59
+ # @return [RubyMethodTracer::Configuration]
60
+ def reset_configuration!
61
+ @configuration_mutex.synchronize { @configuration.reset! }
62
+ end
63
+ end
64
+
29
65
  def self.included(base)
30
66
  base.extend(ClassMethods)
31
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_method_tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seun Adekunle
@@ -20,14 +20,19 @@ files:
20
20
  - ".rspec"
21
21
  - ".rubocop.yml"
22
22
  - CHANGELOG.md
23
+ - CLAUDE.md
23
24
  - CODE_OF_CONDUCT.md
24
25
  - LICENSE.txt
25
26
  - README.md
26
27
  - Rakefile
27
28
  - lib/ruby_method_tracer.rb
28
29
  - lib/ruby_method_tracer/call_tree.rb
30
+ - lib/ruby_method_tracer/configuration.rb
29
31
  - lib/ruby_method_tracer/enhanced_tracer.rb
32
+ - lib/ruby_method_tracer/exportable.rb
30
33
  - lib/ruby_method_tracer/formatters/base_formatter.rb
34
+ - lib/ruby_method_tracer/formatters/flat_formatter.rb
35
+ - lib/ruby_method_tracer/formatters/json_formatter.rb
31
36
  - lib/ruby_method_tracer/formatters/tree_formatter.rb
32
37
  - lib/ruby_method_tracer/simple_tracer.rb
33
38
  - lib/ruby_method_tracer/version.rb