ruby_method_tracer 0.3.2 → 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 +4 -4
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +61 -0
- data/README.md +48 -1
- data/lib/ruby_method_tracer/call_tree.rb +51 -39
- data/lib/ruby_method_tracer/configuration.rb +57 -0
- data/lib/ruby_method_tracer/enhanced_tracer.rb +55 -36
- data/lib/ruby_method_tracer/exportable.rb +73 -0
- data/lib/ruby_method_tracer/formatters/flat_formatter.rb +119 -0
- data/lib/ruby_method_tracer/formatters/json_formatter.rb +105 -0
- data/lib/ruby_method_tracer/simple_tracer.rb +24 -24
- data/lib/ruby_method_tracer/version.rb +1 -1
- data/lib/ruby_method_tracer.rb +37 -1
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 172daf6a55bb4350983cdf15aafbe4b954f418183401ef2be69c6b68c31a575d
|
|
4
|
+
data.tar.gz: '019cd6fd2053967bff554ae244eed994c614a585b16d16f1509e57a5ec5a444e'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86454302178091a987db42dc9eff6e3fd320822ec38c29b27b35c7c0e92ffc16b4f2844131716202969929df8c90f3870e5b0a403ad290df7339d4fc986d2dc9
|
|
7
|
+
data.tar.gz: 3acee9f7dd4398d23ce23d34150dd88e258b9fe09cc420f364132d1fbdea4fb6bed07cdcef7a2710b2264763fb83b7dc8365534991c3057488aa3f41e3d45440
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
15
|
+
## [0.3.3] - 2026-06-08
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `CallTree` now stores its call stack in per-thread storage (keyed per instance) instead of a single shared `@call_stack`, so concurrent callers each track their own nesting depth. `@calls` and `@root_calls` remain shared and `Mutex`-guarded.
|
|
19
|
+
- `SimpleTracer` and `EnhancedTracer` now use a per-instance reentrancy key (`__ruby_method_tracer_in_trace_<object_id>`) so separate tracer instances no longer interfere with each other's re-entry guards.
|
|
20
|
+
- `EnhancedTracer` hierarchy tracking extracted into a dedicated `run_with_hierarchy` method; the call-tree entry is now always closed in an `ensure` block (even for non-`StandardError` exceptions) to prevent the per-thread call stack from becoming corrupted.
|
|
21
|
+
- `SimpleTracer` now delegates `format_time` and `colorize` to a `Formatters::BaseFormatter` instance instead of duplicating the formatting logic inline.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Keyword-argument forwarding in `EnhancedTracer`'s wrapper now avoids passing `**{}`, preventing `SystemStackError` on Ruby 3.4+ (mirrors the `SimpleTracer` fix from 0.3.2).
|
|
25
|
+
|
|
3
26
|
## [0.3.2] - 2025-11-22
|
|
4
27
|
|
|
5
28
|
### Fixed
|
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
|
-
- **
|
|
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.
|
|
@@ -4,45 +4,48 @@ module RubyMethodTracer
|
|
|
4
4
|
# CallTree manages the hierarchical structure of method calls,
|
|
5
5
|
# tracking parent-child relationships and call depths.
|
|
6
6
|
#
|
|
7
|
-
# It uses a
|
|
7
|
+
# It uses a per-thread stack to manage nested calls and builds
|
|
8
8
|
# a tree structure showing the complete call hierarchy.
|
|
9
|
+
#
|
|
10
|
+
# Note: @calls and @root_calls are shared across threads and protected
|
|
11
|
+
# by a Mutex. The call stack is stored in thread-local storage so that
|
|
12
|
+
# concurrent callers each maintain their own independent call depth.
|
|
9
13
|
class CallTree
|
|
10
14
|
attr_reader :calls, :root_calls
|
|
11
15
|
|
|
12
16
|
def initialize
|
|
13
|
-
@calls = [] # All recorded calls (flat list)
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
+
@calls = [] # All recorded calls (flat list, shared)
|
|
18
|
+
@root_calls = [] # Top-level calls (depth 0, shared)
|
|
19
|
+
@lock = Mutex.new # Protects @calls and @root_calls
|
|
20
|
+
@thread_key = :"__ruby_method_tracer_call_stack_#{object_id}" # per-instance thread-local key
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# Start tracking a method call
|
|
20
24
|
#
|
|
21
25
|
# @param method_name [String] The name of the method being called
|
|
22
26
|
# @return [Hash] The call record that was pushed to the stack
|
|
23
|
-
def start_call(method_name)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
def start_call(method_name)
|
|
28
|
+
stack = thread_call_stack
|
|
29
|
+
|
|
30
|
+
call_record = {
|
|
31
|
+
method_name: method_name,
|
|
32
|
+
start_time: monotonic_time,
|
|
33
|
+
depth: stack.size,
|
|
34
|
+
children: [],
|
|
35
|
+
status: nil,
|
|
36
|
+
error: nil,
|
|
37
|
+
execution_time: nil,
|
|
38
|
+
timestamp: Time.now
|
|
39
|
+
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
# Add as child to parent if we're nested
|
|
42
|
+
stack.last[:children] << call_record if stack.any?
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
# Track root-level calls (lock required since @root_calls is shared)
|
|
45
|
+
@lock.synchronize { @root_calls << call_record } if stack.empty?
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
47
|
+
stack.push(call_record)
|
|
48
|
+
call_record
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
# End tracking a method call
|
|
@@ -51,24 +54,23 @@ module RubyMethodTracer
|
|
|
51
54
|
# @param error [Exception, nil] The exception if status is :error
|
|
52
55
|
# @return [Hash, nil] The completed call record
|
|
53
56
|
def end_call(status = :success, error = nil)
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
stack = thread_call_stack
|
|
58
|
+
return nil if stack.empty?
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
call_record = stack.pop
|
|
61
|
+
call_record[:status] = status
|
|
62
|
+
call_record[:error] = error
|
|
63
|
+
call_record[:execution_time] = monotonic_time - call_record[:start_time]
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
+
@lock.synchronize { @calls << call_record }
|
|
66
|
+
call_record
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
# Get the current call depth
|
|
69
|
+
# Get the current call depth for the calling thread
|
|
68
70
|
#
|
|
69
71
|
# @return [Integer] The current nesting level
|
|
70
72
|
def current_depth
|
|
71
|
-
|
|
73
|
+
thread_call_stack.size
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Get call hierarchy as nested structure
|
|
@@ -100,23 +102,33 @@ module RubyMethodTracer
|
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
# Clear all recorded calls and reset state
|
|
105
|
+
#
|
|
106
|
+
# Note: only the current thread's call stack is cleared; other threads
|
|
107
|
+
# that are mid-trace retain their stacks.
|
|
103
108
|
def clear
|
|
104
109
|
@lock.synchronize do
|
|
105
110
|
@calls.clear
|
|
106
|
-
@call_stack.clear
|
|
107
111
|
@root_calls.clear
|
|
108
112
|
end
|
|
113
|
+
thread_call_stack.clear
|
|
109
114
|
end
|
|
110
115
|
|
|
111
|
-
# Check if
|
|
116
|
+
# Check if the current thread has no active calls
|
|
112
117
|
#
|
|
113
118
|
# @return [Boolean]
|
|
114
119
|
def empty?
|
|
115
|
-
|
|
120
|
+
thread_call_stack.empty?
|
|
116
121
|
end
|
|
117
122
|
|
|
118
123
|
private
|
|
119
124
|
|
|
125
|
+
# Returns the call stack for the current thread, creating it if needed.
|
|
126
|
+
# Using a per-instance key prevents interference between multiple CallTree
|
|
127
|
+
# instances running in the same thread.
|
|
128
|
+
def thread_call_stack
|
|
129
|
+
Thread.current[@thread_key] ||= []
|
|
130
|
+
end
|
|
131
|
+
|
|
120
132
|
def monotonic_time
|
|
121
133
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
134
|
end
|
|
@@ -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
|
|
@@ -39,7 +39,7 @@ module RubyMethodTracer
|
|
|
39
39
|
@target_class.send(:alias_method, aliased, method_name)
|
|
40
40
|
|
|
41
41
|
tracer = self
|
|
42
|
-
key =
|
|
42
|
+
key = @tracer_key # unique per tracer instance; prevents cross-tracer interference
|
|
43
43
|
|
|
44
44
|
# Build wrapper that tracks hierarchy
|
|
45
45
|
@target_class.define_method(method_name, &build_enhanced_wrapper(aliased, method_name, key, tracer))
|
|
@@ -83,55 +83,74 @@ module RubyMethodTracer
|
|
|
83
83
|
|
|
84
84
|
private
|
|
85
85
|
|
|
86
|
-
#
|
|
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
|
+
|
|
87
97
|
def build_enhanced_wrapper(aliased, method_name, key, tracer)
|
|
88
98
|
track_hierarchy = tracer.instance_variable_get(:@track_hierarchy)
|
|
89
99
|
# Use method-specific key to prevent only SELF-recursion, not all nested calls
|
|
90
100
|
method_key = :"#{key}_#{method_name}"
|
|
91
101
|
|
|
92
102
|
proc do |*args, **kwargs, &block|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
full_method_name = "#{tracer.instance_variable_get(:@target_class)}##{method_name}"
|
|
99
|
-
|
|
100
|
-
# Start tracking in call tree
|
|
101
|
-
tracer.call_tree.start_call(full_method_name)
|
|
102
|
-
|
|
103
|
-
start = tracer.__send__(:monotonic_time)
|
|
104
|
-
begin
|
|
105
|
-
result = __send__(aliased, *args, **kwargs, &block)
|
|
106
|
-
execution_time = tracer.__send__(:monotonic_time) - start
|
|
107
|
-
|
|
108
|
-
# Record in both places
|
|
109
|
-
tracer.__send__(:record_call, method_name, execution_time, :success)
|
|
110
|
-
tracer.call_tree.end_call(:success)
|
|
111
|
-
|
|
112
|
-
result
|
|
113
|
-
rescue StandardError => e
|
|
114
|
-
execution_time = tracer.__send__(:monotonic_time) - start
|
|
115
|
-
|
|
116
|
-
# Record in both places
|
|
117
|
-
tracer.__send__(:record_call, method_name, execution_time, :error, e)
|
|
118
|
-
tracer.call_tree.end_call(:error, e)
|
|
103
|
+
# Ruby 3+ compatible forwarding helper (avoids passing **{} which caused
|
|
104
|
+
# SystemStackError with Ruby 3.4+ keyword argument forwarding)
|
|
105
|
+
call_aliased = lambda do
|
|
106
|
+
kwargs.empty? ? __send__(aliased, *args, &block) : __send__(aliased, *args, **kwargs, &block)
|
|
107
|
+
end
|
|
119
108
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Thread.current[method_key] = false
|
|
123
|
-
end
|
|
109
|
+
if track_hierarchy
|
|
110
|
+
tracer.__send__(:run_with_hierarchy, method_name, method_key, call_aliased)
|
|
124
111
|
else
|
|
125
|
-
tracer.__send__(:wrap_call, method_name, key)
|
|
126
|
-
__send__(aliased, *args, **kwargs, &block)
|
|
127
|
-
end
|
|
112
|
+
tracer.__send__(:wrap_call, method_name, key) { call_aliased.call }
|
|
128
113
|
end
|
|
129
114
|
end
|
|
130
115
|
end
|
|
116
|
+
|
|
117
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
118
|
+
def run_with_hierarchy(method_name, method_key, call_aliased)
|
|
119
|
+
# Prevent only recursive calls to the SAME method
|
|
120
|
+
return call_aliased.call if Thread.current[method_key]
|
|
121
|
+
|
|
122
|
+
Thread.current[method_key] = true
|
|
123
|
+
full_method_name = "#{@target_class}##{method_name}"
|
|
124
|
+
|
|
125
|
+
# Start tracking in call tree before entering the timed section
|
|
126
|
+
@call_tree.start_call(full_method_name)
|
|
127
|
+
|
|
128
|
+
start = monotonic_time
|
|
129
|
+
call_status = :success
|
|
130
|
+
call_error = nil
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
result = call_aliased.call
|
|
134
|
+
execution_time = monotonic_time - start
|
|
135
|
+
record_call(method_name, execution_time, :success)
|
|
136
|
+
result
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
call_status = :error
|
|
139
|
+
call_error = e
|
|
140
|
+
execution_time = monotonic_time - start
|
|
141
|
+
record_call(method_name, execution_time, :error, e)
|
|
142
|
+
raise
|
|
143
|
+
ensure
|
|
144
|
+
Thread.current[method_key] = false
|
|
145
|
+
# Always end the call tree entry, even for non-StandardError exceptions,
|
|
146
|
+
# to prevent the per-thread call stack from becoming corrupted.
|
|
147
|
+
@call_tree.end_call(call_status, call_error)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
131
150
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
132
151
|
|
|
133
152
|
def default_options
|
|
134
|
-
super.merge(track_hierarchy:
|
|
153
|
+
super.merge(track_hierarchy: RubyMethodTracer.configuration.track_hierarchy)
|
|
135
154
|
end
|
|
136
155
|
end
|
|
137
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
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "set"
|
|
4
4
|
require "logger"
|
|
5
|
+
require_relative "formatters/base_formatter"
|
|
6
|
+
require_relative "exportable"
|
|
5
7
|
|
|
6
8
|
module RubyMethodTracer
|
|
7
9
|
# SimpleTracer wraps instance methods on a target class and records
|
|
@@ -19,7 +21,10 @@ module RubyMethodTracer
|
|
|
19
21
|
# tracer = RubyMethodTracer::SimpleTracer.new(MyClass, threshold: 0.005)
|
|
20
22
|
# tracer.trace_method(:expensive_call)
|
|
21
23
|
# results = tracer.fetch_results
|
|
22
|
-
|
|
24
|
+
# rubocop:disable Metrics/ClassLength
|
|
25
|
+
class SimpleTracer
|
|
26
|
+
include Exportable
|
|
27
|
+
|
|
23
28
|
def initialize(target_class, **options)
|
|
24
29
|
@target_class = target_class
|
|
25
30
|
@options = default_options.merge(options)
|
|
@@ -27,6 +32,9 @@ module RubyMethodTracer
|
|
|
27
32
|
@lock = Mutex.new # Mutex to make writes to @calls thread safe.
|
|
28
33
|
@wrapped_methods = Set.new
|
|
29
34
|
@logger = @options[:logger] || Logger.new($stdout)
|
|
35
|
+
# Unique per instance so separate tracers don't interfere with each other.
|
|
36
|
+
@tracer_key = :"__ruby_method_tracer_in_trace_#{object_id}"
|
|
37
|
+
@formatter = Formatters::BaseFormatter.new
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
def trace_method(name)
|
|
@@ -39,7 +47,7 @@ module RubyMethodTracer
|
|
|
39
47
|
@target_class.send(:alias_method, aliased, method_name) # Aliases original implementation to our private name.
|
|
40
48
|
|
|
41
49
|
tracer = self
|
|
42
|
-
key =
|
|
50
|
+
key = @tracer_key # unique per tracer instance; prevents cross-tracer interference
|
|
43
51
|
|
|
44
52
|
# Defines a new method with the original name that delegates to our wrapper.
|
|
45
53
|
@target_class.define_method(method_name, &build_wrapper(aliased, method_name, key, tracer))
|
|
@@ -84,12 +92,19 @@ module RubyMethodTracer
|
|
|
84
92
|
|
|
85
93
|
private
|
|
86
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
|
+
|
|
87
101
|
def default_options
|
|
102
|
+
config = RubyMethodTracer.configuration
|
|
88
103
|
{
|
|
89
|
-
threshold:
|
|
90
|
-
auto_output:
|
|
91
|
-
max_calls:
|
|
92
|
-
logger:
|
|
104
|
+
threshold: config.threshold,
|
|
105
|
+
auto_output: config.auto_output,
|
|
106
|
+
max_calls: config.max_calls,
|
|
107
|
+
logger: config.logger
|
|
93
108
|
}
|
|
94
109
|
end
|
|
95
110
|
|
|
@@ -161,27 +176,12 @@ module RubyMethodTracer
|
|
|
161
176
|
end
|
|
162
177
|
|
|
163
178
|
def format_time(seconds)
|
|
164
|
-
|
|
165
|
-
"#{seconds.round(3)}s"
|
|
166
|
-
elsif seconds >= 0.001
|
|
167
|
-
"#{(seconds * 1000).round(1)}ms"
|
|
168
|
-
else
|
|
169
|
-
"#{(seconds * 1_000_000).round(0)}µs"
|
|
170
|
-
end
|
|
179
|
+
@formatter.format_time(seconds)
|
|
171
180
|
end
|
|
172
181
|
|
|
173
182
|
def colorize(text, color)
|
|
174
|
-
|
|
175
|
-
red: "31",
|
|
176
|
-
green: "32",
|
|
177
|
-
yellow: "33",
|
|
178
|
-
blue: "34",
|
|
179
|
-
magenta: "35",
|
|
180
|
-
cyan: "36",
|
|
181
|
-
white: "37",
|
|
182
|
-
reset: "0"
|
|
183
|
-
}
|
|
184
|
-
"\e[#{colors[color]}m#{text}\e[#{colors[:reset]}m"
|
|
183
|
+
@formatter.colorize(text, color)
|
|
185
184
|
end
|
|
186
185
|
end
|
|
186
|
+
# rubocop:enable Metrics/ClassLength
|
|
187
187
|
end
|
data/lib/ruby_method_tracer.rb
CHANGED
|
@@ -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"
|
|
5
|
+
require_relative "ruby_method_tracer/formatters/base_formatter"
|
|
4
6
|
require_relative "ruby_method_tracer/simple_tracer"
|
|
5
7
|
require_relative "ruby_method_tracer/call_tree"
|
|
6
8
|
require_relative "ruby_method_tracer/enhanced_tracer"
|
|
7
|
-
require_relative "ruby_method_tracer/formatters/base_formatter"
|
|
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,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_method_tracer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seun Adekunle
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: A developer-friendly gem for tracing method calls, execution times, with
|
|
14
13
|
minimal overhead.
|
|
@@ -21,14 +20,19 @@ files:
|
|
|
21
20
|
- ".rspec"
|
|
22
21
|
- ".rubocop.yml"
|
|
23
22
|
- CHANGELOG.md
|
|
23
|
+
- CLAUDE.md
|
|
24
24
|
- CODE_OF_CONDUCT.md
|
|
25
25
|
- LICENSE.txt
|
|
26
26
|
- README.md
|
|
27
27
|
- Rakefile
|
|
28
28
|
- lib/ruby_method_tracer.rb
|
|
29
29
|
- lib/ruby_method_tracer/call_tree.rb
|
|
30
|
+
- lib/ruby_method_tracer/configuration.rb
|
|
30
31
|
- lib/ruby_method_tracer/enhanced_tracer.rb
|
|
32
|
+
- lib/ruby_method_tracer/exportable.rb
|
|
31
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
|
|
32
36
|
- lib/ruby_method_tracer/formatters/tree_formatter.rb
|
|
33
37
|
- lib/ruby_method_tracer/simple_tracer.rb
|
|
34
38
|
- lib/ruby_method_tracer/version.rb
|
|
@@ -42,7 +46,6 @@ metadata:
|
|
|
42
46
|
source_code_uri: https://github.com/Seunadex/ruby_method_tracer
|
|
43
47
|
changelog_uri: https://github.com/Seunadex/ruby_method_tracer/blob/main/CHANGELOG.md
|
|
44
48
|
rubygems_mfa_required: 'true'
|
|
45
|
-
post_install_message:
|
|
46
49
|
rdoc_options: []
|
|
47
50
|
require_paths:
|
|
48
51
|
- lib
|
|
@@ -57,8 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
60
|
- !ruby/object:Gem::Version
|
|
58
61
|
version: '0'
|
|
59
62
|
requirements: []
|
|
60
|
-
rubygems_version: 3.
|
|
61
|
-
signing_key:
|
|
63
|
+
rubygems_version: 3.6.7
|
|
62
64
|
specification_version: 4
|
|
63
65
|
summary: Lightweight method tracing for Ruby applications
|
|
64
66
|
test_files: []
|