debug-agent 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +45 -5
- data/lib/debug_agent/inspectors/build_info.rb +87 -0
- data/lib/debug_agent/inspectors/cpu_profile.rb +192 -0
- data/lib/debug_agent/inspectors/leak_detector.rb +161 -0
- data/lib/debug_agent/inspectors/service_registry.rb +88 -0
- data/lib/debug_agent/inspectors/snapshot.rb +206 -0
- data/lib/debug_agent/version.rb +1 -1
- data/lib/debug_agent.rb +14 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe4d325302d304db5821ef8bc5283ce3978e26d7721604efcbf00a646229346c
|
|
4
|
+
data.tar.gz: '058ae474cc6e35f83f59ee86da1ca807af84a51762c66a9452827805e28664c9'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f72cf134d401b2f055e5103eea6b6d35e06bbc75f7962a6f33d37f906f91e6899a5f562a5a032209448f469c5830b24d582b43b34730fbf949a1c47d4a8b25cb
|
|
7
|
+
data.tar.gz: dbb70e23e5bce9c717c8ecbdf708b2724811beeac9fa3672f496ac25dfedda108f0c051f71c7af5a1421cf181d120534aff1a94f6c511df3af04a56745a19098
|
data/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Ruby Debug Agent
|
|
2
2
|
|
|
3
3
|
[](https://github.com/topcheer/ruby-debug-agent)
|
|
4
|
-

|
|
5
|
+

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
An AI-powered runtime debugging agent that embeds directly into your Ruby application. Add one gem, configure an LLM key, and chat with your live app at `/agent` to inspect GC, ObjectSpace, threads, routes, Redis, Rails models/routes, Sidekiq queues, Puma stats, fibers/signals, process info, HTTP requests, and more — **
|
|
9
|
+
An AI-powered runtime debugging agent that embeds directly into your Ruby application. Add one gem, configure an LLM key, and chat with your live app at `/agent` to inspect GC, ObjectSpace, threads, routes, Redis, Rails models/routes, Sidekiq queues, Puma stats, fibers/signals, process info, HTTP requests, and more — **98 diagnostic tools across 36 inspectors**.
|
|
10
10
|
|
|
11
11
|
## Version Support
|
|
12
12
|
|
|
@@ -71,10 +71,10 @@ http://localhost:4567/agent
|
|
|
71
71
|
- **Context compression** — automatically summarizes old conversation when token limit is approached
|
|
72
72
|
- **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
|
|
73
73
|
- **Max tool rounds** (25) with forced final summary when limit is reached
|
|
74
|
-
- **
|
|
74
|
+
- **98 diagnostic tools** across **36 inspectors**
|
|
75
75
|
- Zero external dependencies (no Datadog, no Grafana, no APM)
|
|
76
76
|
|
|
77
|
-
## Inspectors & Tools (
|
|
77
|
+
## Inspectors & Tools (98)
|
|
78
78
|
|
|
79
79
|
### GC Inspector
|
|
80
80
|
| Tool | Description |
|
|
@@ -250,6 +250,40 @@ http://localhost:4567/agent
|
|
|
250
250
|
| `detect_pool_leaks` | Heuristic leak detection (growing pool, high wait ratio, saturation) |
|
|
251
251
|
| `get_pool_wait_stats` | Connection acquire wait stats (avg, P95, max wait, timeout count) |
|
|
252
252
|
|
|
253
|
+
### CPU Profiler Inspector (v0.7.0)
|
|
254
|
+
| Tool | Description |
|
|
255
|
+
|------|-------------|
|
|
256
|
+
| `start_cpu_profile` | Start a CPU profiling session (stackprof/stackprof-native) |
|
|
257
|
+
| `stop_cpu_profile` | Stop CPU profiling and return collected profile data |
|
|
258
|
+
| `get_top_functions` | Get top CPU-consuming functions from the current profile |
|
|
259
|
+
|
|
260
|
+
### Memory Leak Detector Inspector (v0.7.0)
|
|
261
|
+
| Tool | Description |
|
|
262
|
+
|------|-------------|
|
|
263
|
+
| `take_heap_snapshot` | Capture an ObjectSpace heap snapshot for leak analysis |
|
|
264
|
+
| `compare_heap_snapshots` | Compare two heap snapshots to identify object growth |
|
|
265
|
+
| `get_leak_candidates` | Identify objects likely to be memory leaks |
|
|
266
|
+
|
|
267
|
+
### Deployment/Build Info Inspector (v0.7.0)
|
|
268
|
+
| Tool | Description |
|
|
269
|
+
|------|-------------|
|
|
270
|
+
| `get_build_info` | Build version, commit hash, and gem metadata |
|
|
271
|
+
| `get_deployment_info` | Deployment environment, container, and orchestration metadata |
|
|
272
|
+
| `get_runtime_version` | Ruby interpreter version, engine, and platform details |
|
|
273
|
+
|
|
274
|
+
### Snapshot & Diff Inspector (v0.7.0)
|
|
275
|
+
| Tool | Description |
|
|
276
|
+
|------|-------------|
|
|
277
|
+
| `take_snapshot` | Capture a runtime state snapshot |
|
|
278
|
+
| `compare_snapshots` | Compare two snapshots to identify state changes |
|
|
279
|
+
| `list_snapshots` | List all saved snapshots with timestamps |
|
|
280
|
+
|
|
281
|
+
### Service Registry Inspector (v0.7.0)
|
|
282
|
+
| Tool | Description |
|
|
283
|
+
|------|-------------|
|
|
284
|
+
| `get_registered_services` | List all registered application services |
|
|
285
|
+
| `get_service_dependencies` | Map service-to-service dependency graph |
|
|
286
|
+
|
|
253
287
|
## Custom Tools
|
|
254
288
|
|
|
255
289
|
```ruby
|
|
@@ -302,6 +336,12 @@ cd demo && ruby -I../lib app.rb
|
|
|
302
336
|
|
|
303
337
|
[](https://github.com/topcheer/ruby-debug-agent)
|
|
304
338
|
|
|
339
|
+
## Built With
|
|
340
|
+
|
|
341
|
+
[](https://github.com/topcheer/ggcode)
|
|
342
|
+
|
|
343
|
+
This project was built using [ggcode](https://github.com/topcheer/ggcode) — an AI coding assistant for terminal-based development.
|
|
344
|
+
|
|
305
345
|
## License
|
|
306
346
|
|
|
307
347
|
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'etc'
|
|
3
|
+
|
|
4
|
+
module DebugAgent
|
|
5
|
+
register_tool('get_build_info',
|
|
6
|
+
'Get Ruby build info: version, engine (MRI/JRuby/TruffleRuby), ' \
|
|
7
|
+
'platform, build date, RUBY_DESCRIPTION') do
|
|
8
|
+
{
|
|
9
|
+
ruby_version: RUBY_VERSION,
|
|
10
|
+
ruby_engine: RUBY_ENGINE,
|
|
11
|
+
ruby_engine_version: defined?(RUBY_ENGINE_VERSION) ? RUBY_ENGINE_VERSION : RUBY_VERSION,
|
|
12
|
+
platform: RUBY_PLATFORM,
|
|
13
|
+
ruby_description: RUBY_DESCRIPTION,
|
|
14
|
+
ruby_patchlevel: defined?(RUBY_PATCHLEVEL) ? RUBY_PATCHLEVEL : nil,
|
|
15
|
+
ruby_revision: defined?(RUBY_REVISION) ? RUBY_REVISION.to_s : nil,
|
|
16
|
+
ruby_release_date: defined?(RUBY_RELEASE_DATE) ? RUBY_RELEASE_DATE : nil,
|
|
17
|
+
build_date: defined?(RUBY_RELEASE_DATE) ? RUBY_RELEASE_DATE : nil,
|
|
18
|
+
host_os: RbConfig::CONFIG['host_os'],
|
|
19
|
+
host_cpu: RbConfig::CONFIG['host_cpu'],
|
|
20
|
+
configure_args: RbConfig::CONFIG['configure_args']
|
|
21
|
+
}
|
|
22
|
+
rescue => e
|
|
23
|
+
{ error: e.message }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
register_tool('get_deployment_info',
|
|
27
|
+
'Get deployment info: hostname, PID, uptime, container detection, ' \
|
|
28
|
+
'APP_ENV, Rails env') do
|
|
29
|
+
rss = `ps -o rss= -p #{Process.pid}`.to_i
|
|
30
|
+
uptime_seconds = Time.now - DebugAgent::PROCESS_START_TIME
|
|
31
|
+
|
|
32
|
+
container_detected = File.exist?('/.dockerenv')
|
|
33
|
+
in_cgroup = false
|
|
34
|
+
begin
|
|
35
|
+
in_cgroup = File.read('/proc/1/cgroup').include?('docker') ||
|
|
36
|
+
File.read('/proc/1/cgroup').include?('containerd')
|
|
37
|
+
rescue
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
hostname: Socket.gethostname,
|
|
42
|
+
pid: Process.pid,
|
|
43
|
+
ppid: Process.ppid,
|
|
44
|
+
process_name: $0,
|
|
45
|
+
uptime_seconds: uptime_seconds.round(0),
|
|
46
|
+
rss_mb: (rss / 1024.0).round(2),
|
|
47
|
+
container_detected: container_detected || in_cgroup,
|
|
48
|
+
docker_detected: File.exist?('/.dockerenv'),
|
|
49
|
+
app_env: ENV['APP_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
|
|
50
|
+
rails_env: defined?(::Rails) ? ::Rails.env.to_s : nil,
|
|
51
|
+
user: Etc.getpwuid(Process.uid)&.name,
|
|
52
|
+
uid: Process.uid,
|
|
53
|
+
gid: Process.gid,
|
|
54
|
+
cpu_count: Etc.nprocessors
|
|
55
|
+
}
|
|
56
|
+
rescue => e
|
|
57
|
+
{ error: e.message }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
register_tool('get_runtime_versions',
|
|
61
|
+
'Get versions of key gems: rails, sinatra, sidekiq, redis, puma ' \
|
|
62
|
+
'if loaded') do
|
|
63
|
+
key_gems = %w[rails sinatra sidekiq redis puma rack rack-attack \
|
|
64
|
+
activerecord activesupport postgresql pg mysql2 bunny \
|
|
65
|
+
faraday httplog oj msgpack json]
|
|
66
|
+
|
|
67
|
+
versions = {}
|
|
68
|
+
key_gems.each do |gem_name|
|
|
69
|
+
spec = Gem.loaded_specs[gem_name] || Gem.loaded_specs.values.find { |s| s.name == gem_name }
|
|
70
|
+
versions[gem_name] = spec&.version&.to_s if spec
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Remove nils
|
|
74
|
+
versions.compact!
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
ruby_version: RUBY_VERSION,
|
|
78
|
+
ruby_engine: RUBY_ENGINE,
|
|
79
|
+
rubygems_version: Gem::VERSION,
|
|
80
|
+
bundler_version: defined?(Bundler) ? Bundler::VERSION : nil,
|
|
81
|
+
loaded_gem_count: Gem.loaded_specs.size,
|
|
82
|
+
key_gem_versions: versions
|
|
83
|
+
}
|
|
84
|
+
rescue => e
|
|
85
|
+
{ error: e.message }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require 'timeout'
|
|
2
|
+
|
|
3
|
+
module DebugAgent
|
|
4
|
+
# CPU profiler inspector. Uses stackprof if available, otherwise falls back
|
|
5
|
+
# to a sampling profiler that periodically captures caller stacks.
|
|
6
|
+
@cpu_profile_data = nil
|
|
7
|
+
@cpu_profile_running = false
|
|
8
|
+
@cpu_profile_lock = Mutex.new
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :cpu_profile_data, :cpu_profile_running
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
register_tool('start_cpu_profile',
|
|
15
|
+
'Start CPU profiling for a given duration. Uses stackprof if available, ' \
|
|
16
|
+
'otherwise falls back to a sampling profiler. Auto-stops after duration.',
|
|
17
|
+
duration_seconds: { type: 'integer', description: 'Profile duration in seconds', required: false }) do |duration_seconds: 10|
|
|
18
|
+
duration = duration_seconds.to_i
|
|
19
|
+
duration = 10 if duration <= 0
|
|
20
|
+
|
|
21
|
+
@cpu_profile_lock.synchronize do
|
|
22
|
+
next { error: 'CPU profile already running' } if @cpu_profile_running
|
|
23
|
+
@cpu_profile_running = true
|
|
24
|
+
@cpu_profile_data = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if defined?(::StackProf)
|
|
28
|
+
# StackProf path: block-based profiling
|
|
29
|
+
StackProf.run(mode: :cpu, interval: 1000) do
|
|
30
|
+
sleep(duration)
|
|
31
|
+
end
|
|
32
|
+
@cpu_profile_lock.synchronize { @cpu_profile_running = false }
|
|
33
|
+
raw = StackProf.results
|
|
34
|
+
@cpu_profile_data = parse_stackprof(raw) if raw
|
|
35
|
+
{
|
|
36
|
+
status: 'completed',
|
|
37
|
+
backend: 'stackprof',
|
|
38
|
+
duration_seconds: duration,
|
|
39
|
+
samples: raw ? raw[:samples] : 0
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
# Fallback: sampling profiler using caller stacks
|
|
43
|
+
samples = Hash.new(0)
|
|
44
|
+
total_samples = 0
|
|
45
|
+
sample_lock = Mutex.new
|
|
46
|
+
end_time = Time.now + duration
|
|
47
|
+
|
|
48
|
+
sampler = Thread.new do
|
|
49
|
+
while Time.now < end_time && @cpu_profile_running
|
|
50
|
+
stack = caller(2)&.join("\n")
|
|
51
|
+
sample_lock.synchronize do
|
|
52
|
+
samples[stack] += 1
|
|
53
|
+
total_samples += 1
|
|
54
|
+
end if stack
|
|
55
|
+
sleep(0.001)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
sampler.join
|
|
60
|
+
@cpu_profile_lock.synchronize { @cpu_profile_running = false }
|
|
61
|
+
@cpu_profile_data = parse_call_samples(samples, total_samples)
|
|
62
|
+
{
|
|
63
|
+
status: 'completed',
|
|
64
|
+
backend: 'sampling',
|
|
65
|
+
duration_seconds: duration,
|
|
66
|
+
total_samples: total_samples,
|
|
67
|
+
unique_stacks: samples.size
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
rescue => e
|
|
71
|
+
@cpu_profile_running = false
|
|
72
|
+
{ error: e.message }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
register_tool('stop_cpu_profile',
|
|
76
|
+
'Stop CPU profiling and return top 20 methods by self time. ' \
|
|
77
|
+
'Each entry includes method name, file, line, self_ms, total_ms, calls.') do
|
|
78
|
+
next { error: 'No profile data available. Run start_cpu_profile first.' } unless @cpu_profile_data
|
|
79
|
+
|
|
80
|
+
@cpu_profile_running = false
|
|
81
|
+
top = (@cpu_profile_data[:functions] || []).first(20)
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
status: 'stopped',
|
|
85
|
+
backend: @cpu_profile_data[:backend],
|
|
86
|
+
total_samples: @cpu_profile_data[:total_samples],
|
|
87
|
+
top_functions: top
|
|
88
|
+
}
|
|
89
|
+
rescue => e
|
|
90
|
+
{ error: e.message }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
register_tool('get_top_functions',
|
|
94
|
+
'Return top methods from last CPU profile. Sort by self_time, total_time, or calls.',
|
|
95
|
+
limit: { type: 'integer', description: 'Number of functions to return (default 20)', required: false },
|
|
96
|
+
sort_by: { type: 'string', description: 'Sort key: self_time, total_time, or calls (default self_time)', required: false }) do |limit: 20, sort_by: 'self_time'|
|
|
97
|
+
next { error: 'No profile data available. Run start_cpu_profile first.' } unless @cpu_profile_data
|
|
98
|
+
|
|
99
|
+
funcs = (@cpu_profile_data[:functions] || []).dup
|
|
100
|
+
sort_key = %w[self_time total_time calls].include?(sort_by.to_s) ? sort_by.to_sym : :self_time
|
|
101
|
+
funcs.sort_by! { |f| -f[sort_key].to_f }
|
|
102
|
+
top = funcs.first(limit.to_i > 0 ? limit.to_i : 20)
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
sort_by: sort_key,
|
|
106
|
+
total_functions: @cpu_profile_data[:functions]&.size || 0,
|
|
107
|
+
top_functions: top
|
|
108
|
+
}
|
|
109
|
+
rescue => e
|
|
110
|
+
{ error: e.message }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# --- Helpers ---
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def parse_stackprof(raw)
|
|
119
|
+
return nil unless raw && raw[:frames]
|
|
120
|
+
|
|
121
|
+
funcs = raw[:frames].map do |_frame_key, frame|
|
|
122
|
+
{
|
|
123
|
+
method: frame[:name] || 'unknown',
|
|
124
|
+
file: frame[:file],
|
|
125
|
+
line: frame[:line],
|
|
126
|
+
self_ms: ((frame[:samples].to_f / raw[:samples].to_f) * (raw[:gc_profile_time] || raw[:walltime] || 0) * 1000).round(2),
|
|
127
|
+
total_ms: ((frame[:total_samples].to_f / raw[:samples].to_f) * (raw[:gc_profile_time] || raw[:walltime] || 0) * 1000).round(2),
|
|
128
|
+
calls: frame[:samples].to_i
|
|
129
|
+
}
|
|
130
|
+
end.sort_by { |f| -f[:self_ms] }
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
backend: 'stackprof',
|
|
134
|
+
total_samples: raw[:samples],
|
|
135
|
+
functions: funcs
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def parse_call_samples(samples, total_samples)
|
|
140
|
+
method_stats = Hash.new { |h, k| h[k] = { self_time: 0, total_time: 0, calls: 0 } }
|
|
141
|
+
|
|
142
|
+
samples.each do |stack_str, count|
|
|
143
|
+
lines = stack_str.split("\n")
|
|
144
|
+
next if lines.empty?
|
|
145
|
+
|
|
146
|
+
# First line of caller(2) is the most recently called method
|
|
147
|
+
top_line = lines.first
|
|
148
|
+
parsed = parse_backtrace_line(top_line)
|
|
149
|
+
|
|
150
|
+
key = "#{parsed[:file]}:#{parsed[:method]}"
|
|
151
|
+
method_stats[key][:self_time] += count
|
|
152
|
+
method_stats[key][:total_time] += count
|
|
153
|
+
method_stats[key][:calls] += 1
|
|
154
|
+
|
|
155
|
+
# All lines contribute to total_time of their respective methods
|
|
156
|
+
lines.each do |line|
|
|
157
|
+
p = parse_backtrace_line(line)
|
|
158
|
+
k = "#{p[:file]}:#{p[:method]}"
|
|
159
|
+
method_stats[k][:total_time] += count
|
|
160
|
+
method_stats[k][:calls] += count unless k == key
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
funcs = method_stats.map do |_k, s|
|
|
165
|
+
parsed = parse_backtrace_line(samples.keys.find { |stk| stk.include?(_k) || true }&.split("\n")&.first || '')
|
|
166
|
+
{
|
|
167
|
+
method: _k.split(':').last,
|
|
168
|
+
file: _k.split(':')[0...-1].join(':'),
|
|
169
|
+
line: parsed[:line],
|
|
170
|
+
self_ms: (s[:self_time].to_f / total_samples.to_f * 10000).round(2),
|
|
171
|
+
total_ms: (s[:total_time].to_f / total_samples.to_f * 10000).round(2),
|
|
172
|
+
calls: s[:calls]
|
|
173
|
+
}
|
|
174
|
+
end.sort_by { |f| -f[:self_ms] }
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
backend: 'sampling',
|
|
178
|
+
total_samples: total_samples,
|
|
179
|
+
functions: funcs
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_backtrace_line(line)
|
|
184
|
+
# Format: "/path/to/file.rb:42:in `method_name'"
|
|
185
|
+
if line =~ /^(.+):(\d+):in `(.+)'$/
|
|
186
|
+
{ file: $1, line: $2.to_i, method: $3 }
|
|
187
|
+
else
|
|
188
|
+
{ file: 'unknown', line: 0, method: 'unknown' }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require 'objspace'
|
|
2
|
+
|
|
3
|
+
module DebugAgent
|
|
4
|
+
# Memory leak detector inspector. Records heap snapshots and compares them
|
|
5
|
+
# to identify object types with consistent growth.
|
|
6
|
+
@heap_snapshots = {}
|
|
7
|
+
@heap_snapshot_counter = 0
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :heap_snapshots
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
register_tool('take_heap_snapshot',
|
|
14
|
+
'Take a heap snapshot recording object counts by type, heap slots, ' \
|
|
15
|
+
'and GC counts. Returns a snapshot ID for later comparison.') do
|
|
16
|
+
counts = Hash.new(0)
|
|
17
|
+
ObjectSpace.each_object do |obj|
|
|
18
|
+
begin
|
|
19
|
+
name = obj.class.name || obj.class.to_s
|
|
20
|
+
counts[name] += 1
|
|
21
|
+
rescue
|
|
22
|
+
counts['<unknown>'] += 1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
gc_stats = GC.stat
|
|
27
|
+
total_objects = counts.values.sum
|
|
28
|
+
|
|
29
|
+
@heap_snapshot_counter += 1
|
|
30
|
+
snapshot_id = "heap-#{@heap_snapshot_counter}"
|
|
31
|
+
|
|
32
|
+
snapshot = {
|
|
33
|
+
id: snapshot_id,
|
|
34
|
+
taken_at: Time.now,
|
|
35
|
+
object_counts: counts,
|
|
36
|
+
total_objects: total_objects,
|
|
37
|
+
live_slots: gc_stats[:heap_live_slots],
|
|
38
|
+
free_slots: gc_stats[:heap_free_slots],
|
|
39
|
+
total_slots: gc_stats[:heap_live_slots].to_i + gc_stats[:heap_free_slots].to_i,
|
|
40
|
+
gc_count: gc_stats[:count],
|
|
41
|
+
major_gc_count: gc_stats[:major_gc_count],
|
|
42
|
+
minor_gc_count: gc_stats[:minor_gc_count],
|
|
43
|
+
total_allocated_objects: gc_stats[:total_allocated_objects],
|
|
44
|
+
total_freed_objects: gc_stats[:total_freed_objects],
|
|
45
|
+
heap_pages: gc_stats[:heap_length]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@heap_snapshots[snapshot_id] = snapshot
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
snapshot_id: snapshot_id,
|
|
52
|
+
taken_at: snapshot[:taken_at].iso8601,
|
|
53
|
+
summary: {
|
|
54
|
+
total_objects: total_objects,
|
|
55
|
+
total_classes: counts.size,
|
|
56
|
+
live_slots: snapshot[:live_slots],
|
|
57
|
+
free_slots: snapshot[:free_slots],
|
|
58
|
+
gc_count: snapshot[:gc_count],
|
|
59
|
+
top_types: counts.sort_by { |_, v| -v }.first(10).to_h
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
rescue => e
|
|
63
|
+
{ error: e.message }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
register_tool('compare_heap_snapshots',
|
|
67
|
+
'Compare two heap snapshots. Returns per-type count delta and growth ' \
|
|
68
|
+
'percentage, sorted by absolute growth.',
|
|
69
|
+
snapshot_a: { type: 'string', description: 'First snapshot ID', required: true },
|
|
70
|
+
snapshot_b: { type: 'string', description: 'Second snapshot ID', required: true }) do |snapshot_a:, snapshot_b:|
|
|
71
|
+
snap_a = @heap_snapshots[snapshot_a.to_s]
|
|
72
|
+
snap_b = @heap_snapshots[snapshot_b.to_s]
|
|
73
|
+
|
|
74
|
+
next { error: "Snapshot '#{snapshot_a}' not found. Available: #{@heap_snapshots.keys.join(', ')}" } unless snap_a
|
|
75
|
+
next { error: "Snapshot '#{snapshot_b}' not found. Available: #{@heap_snapshots.keys.join(', ')}" } unless snap_b
|
|
76
|
+
|
|
77
|
+
counts_a = snap_a[:object_counts]
|
|
78
|
+
counts_b = snap_b[:object_counts]
|
|
79
|
+
|
|
80
|
+
all_types = (counts_a.keys | counts_b.keys).uniq
|
|
81
|
+
deltas = all_types.map do |type|
|
|
82
|
+
ca = counts_a[type] || 0
|
|
83
|
+
cb = counts_b[type] || 0
|
|
84
|
+
delta = cb - ca
|
|
85
|
+
growth_pct = ca > 0 ? (delta.to_f / ca * 100).round(2) : (delta > 0 ? Float::INFINITY : 0.0)
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
type: type,
|
|
89
|
+
count_before: ca,
|
|
90
|
+
count_after: cb,
|
|
91
|
+
count_delta: delta,
|
|
92
|
+
growth_percentage: growth_pct
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Sort by absolute growth descending
|
|
97
|
+
deltas.sort_by! { |d| -d[:count_delta].abs }
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
snapshot_a: snapshot_a,
|
|
101
|
+
snapshot_b: snapshot_b,
|
|
102
|
+
time_between: (snap_b[:taken_at] - snap_a[:taken_at]).round(2),
|
|
103
|
+
summary: {
|
|
104
|
+
total_objects_before: snap_a[:total_objects],
|
|
105
|
+
total_objects_after: snap_b[:total_objects],
|
|
106
|
+
net_delta: snap_b[:total_objects] - snap_a[:total_objects],
|
|
107
|
+
gc_runs_between: snap_b[:gc_count] - snap_a[:gc_count],
|
|
108
|
+
live_slots_delta: snap_b[:live_slots] - snap_a[:live_slots]
|
|
109
|
+
},
|
|
110
|
+
type_changes: deltas.first(50)
|
|
111
|
+
}
|
|
112
|
+
rescue => e
|
|
113
|
+
{ error: e.message }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
register_tool('get_leak_candidates',
|
|
117
|
+
'Identify object types with consistent growth across stored snapshots. ' \
|
|
118
|
+
'Shows top growing object types.') do |snapshot_limit: 10|
|
|
119
|
+
next { error: 'Need at least 2 snapshots to detect trends' } if @heap_snapshots.size < 2
|
|
120
|
+
|
|
121
|
+
snapshots = @heap_snapshots.values.sort_by { |s| s[:taken_at] }
|
|
122
|
+
snapshots = snapshots.last([snapshot_limit.to_i, 2].max)
|
|
123
|
+
|
|
124
|
+
# For each type, compute growth across consecutive snapshot pairs
|
|
125
|
+
type_growth = Hash.new { |h, k| h[k] = { deltas: [], total_growth: 0 } }
|
|
126
|
+
|
|
127
|
+
snapshots.each_cons(2) do |s1, s2|
|
|
128
|
+
all_types = (s1[:object_counts].keys | s2[:object_counts].keys)
|
|
129
|
+
all_types.each do |type|
|
|
130
|
+
c1 = s1[:object_counts][type] || 0
|
|
131
|
+
c2 = s2[:object_counts][type] || 0
|
|
132
|
+
delta = c2 - c1
|
|
133
|
+
type_growth[type][:deltas] << delta
|
|
134
|
+
type_growth[type][:total_growth] += delta
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
candidates = type_growth
|
|
139
|
+
.select { |_, g| g[:total_growth] > 0 && g[:deltas].all? { |d| d >= 0 } }
|
|
140
|
+
.map do |type, g|
|
|
141
|
+
{
|
|
142
|
+
type: type,
|
|
143
|
+
total_growth: g[:total_growth],
|
|
144
|
+
snapshots_growing: g[:deltas].count(&:positive?),
|
|
145
|
+
total_snapshots_compared: g[:deltas].size,
|
|
146
|
+
consistency: g[:deltas].all?(&:positive?) ? 'consistent' : 'partial',
|
|
147
|
+
per_period_deltas: g[:deltas]
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
.sort_by { |c| -c[:total_growth] }
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
snapshots_analyzed: snapshots.size,
|
|
154
|
+
first_snapshot: snapshots.first[:id],
|
|
155
|
+
last_snapshot: snapshots.last[:id],
|
|
156
|
+
leak_candidates: candidates.first(20)
|
|
157
|
+
}
|
|
158
|
+
rescue => e
|
|
159
|
+
{ error: e.message }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module DebugAgent
|
|
2
|
+
register_tool('get_registered_services',
|
|
3
|
+
'List all registered debug agent tools grouped by inspector category. ' \
|
|
4
|
+
'Shows tool count and names per group.') do
|
|
5
|
+
all_tools = registry.names
|
|
6
|
+
|
|
7
|
+
# Categorize tools by known inspector groups
|
|
8
|
+
categories = {
|
|
9
|
+
'runtime' => %w[get_gc_stats get_memory_summary trigger_gc get_thread_summary get_runtime_info get_object_allocations],
|
|
10
|
+
'gc' => %w[],
|
|
11
|
+
'object_space' => %w[],
|
|
12
|
+
'threads' => %w[],
|
|
13
|
+
'process' => %w[],
|
|
14
|
+
'system' => %w[],
|
|
15
|
+
'http_tracker' => %w[],
|
|
16
|
+
'routes' => %w[],
|
|
17
|
+
'redis' => %w[],
|
|
18
|
+
'rails' => %w[],
|
|
19
|
+
'sidekiq' => %w[],
|
|
20
|
+
'puma' => %w[],
|
|
21
|
+
'logging' => %w[],
|
|
22
|
+
'cache' => %w[],
|
|
23
|
+
'http_client' => %w[],
|
|
24
|
+
'metrics' => %w[],
|
|
25
|
+
'active_record_stats' => %w[],
|
|
26
|
+
'faraday' => %w[],
|
|
27
|
+
'concurrent' => %w[],
|
|
28
|
+
'security' => %w[],
|
|
29
|
+
'health' => %w[],
|
|
30
|
+
'scheduler' => %w[],
|
|
31
|
+
'error_tracking' => %w[],
|
|
32
|
+
'websocket' => %w[],
|
|
33
|
+
'locks' => %w[],
|
|
34
|
+
'migration' => %w[],
|
|
35
|
+
'config' => %w[],
|
|
36
|
+
'feature_flags' => %w[],
|
|
37
|
+
'endpoint_test' => %w[],
|
|
38
|
+
'pool_inspector' => %w[],
|
|
39
|
+
'cpu_profile' => %w[],
|
|
40
|
+
'leak_detector' => %w[],
|
|
41
|
+
'build_info' => %w[],
|
|
42
|
+
'snapshot' => %w[],
|
|
43
|
+
'service_registry' => %w[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Since we cannot reliably categorize at runtime without a lookup table,
|
|
47
|
+
# we list all tools alphabetically with their descriptions
|
|
48
|
+
tools_with_desc = all_tools.sort.map do |name|
|
|
49
|
+
tool = registry.get(name)
|
|
50
|
+
{
|
|
51
|
+
name: name,
|
|
52
|
+
description: tool&.respond_to?(:description) ? tool.description : nil
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
total_tools: all_tools.size,
|
|
58
|
+
tools: tools_with_desc
|
|
59
|
+
}
|
|
60
|
+
rescue => e
|
|
61
|
+
{ error: e.message }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
register_tool('get_service_dependencies',
|
|
65
|
+
'Show all loaded gems and their versions from Gem.loaded_specs') do
|
|
66
|
+
specs = Gem.loaded_specs.values.sort_by(&:name)
|
|
67
|
+
|
|
68
|
+
gems = specs.map do |spec|
|
|
69
|
+
{
|
|
70
|
+
name: spec.name,
|
|
71
|
+
version: spec.version.to_s,
|
|
72
|
+
loaded_from: spec.loaded_from,
|
|
73
|
+
dependencies: spec.dependencies.map { |d| "#{d.name} (#{d.requirement})" }
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
total_gems: gems.size,
|
|
79
|
+
ruby_version: RUBY_VERSION,
|
|
80
|
+
ruby_engine: RUBY_ENGINE,
|
|
81
|
+
rubygems_version: Gem::VERSION,
|
|
82
|
+
bundler_version: defined?(Bundler) ? Bundler::VERSION : nil,
|
|
83
|
+
gems: gems
|
|
84
|
+
}
|
|
85
|
+
rescue => e
|
|
86
|
+
{ error: e.message }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'objspace'
|
|
2
|
+
|
|
3
|
+
module DebugAgent
|
|
4
|
+
# Snapshot & Diff inspector. Collects metrics across all inspectors at a
|
|
5
|
+
# point in time and allows comparing snapshots to detect changes.
|
|
6
|
+
@metric_snapshots = {}
|
|
7
|
+
@metric_snapshot_counter = 0
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :metric_snapshots
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
register_tool('take_snapshot',
|
|
14
|
+
'Take a cross-inspector snapshot: thread count, fiber count, memory (RSS), ' \
|
|
15
|
+
'GC stats, object count, DB pool stats, cache stats, error count. Returns snapshot ID.') do
|
|
16
|
+
gc = GC.stat
|
|
17
|
+
obj_counts = ObjectSpace.count_objects
|
|
18
|
+
rss = `ps -o rss= -p #{Process.pid}`.to_i
|
|
19
|
+
|
|
20
|
+
metrics = {
|
|
21
|
+
taken_at: Time.now,
|
|
22
|
+
threads: Thread.list.size,
|
|
23
|
+
alive_threads: Thread.list.count(&:alive?),
|
|
24
|
+
fiber_count: Fiber.respond_to?(:list) ? Fiber.list.size : count_fibers,
|
|
25
|
+
rss_mb: (rss / 1024.0).round(2),
|
|
26
|
+
gc: {
|
|
27
|
+
count: gc[:count],
|
|
28
|
+
major_gc_count: gc[:major_gc_count],
|
|
29
|
+
minor_gc_count: gc[:minor_gc_count],
|
|
30
|
+
heap_live_slots: gc[:heap_live_slots],
|
|
31
|
+
heap_free_slots: gc[:heap_free_slots],
|
|
32
|
+
total_allocated_objects: gc[:total_allocated_objects],
|
|
33
|
+
total_freed_objects: gc[:total_freed_objects],
|
|
34
|
+
old_objects: gc[:old_objects]
|
|
35
|
+
},
|
|
36
|
+
objects: {
|
|
37
|
+
total: obj_counts[:TOTAL] || obj_counts.values.sum,
|
|
38
|
+
free_slots: obj_counts[:FREE] || 0,
|
|
39
|
+
t_string: obj_counts[:T_STRING] || 0,
|
|
40
|
+
t_array: obj_counts[:T_ARRAY] || 0,
|
|
41
|
+
t_hash: obj_counts[:T_HASH] || 0,
|
|
42
|
+
t_object: obj_counts[:T_OBJECT] || 0,
|
|
43
|
+
t_data: obj_counts[:T_DATA] || 0
|
|
44
|
+
},
|
|
45
|
+
db_pool: gather_db_pool_stats,
|
|
46
|
+
cache: gather_cache_stats,
|
|
47
|
+
error_count: gather_error_count
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@metric_snapshot_counter += 1
|
|
51
|
+
snapshot_id = "snap-#{@metric_snapshot_counter}"
|
|
52
|
+
@metric_snapshots[snapshot_id] = metrics
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
snapshot_id: snapshot_id,
|
|
56
|
+
taken_at: metrics[:taken_at].iso8601,
|
|
57
|
+
summary: {
|
|
58
|
+
threads: metrics[:threads],
|
|
59
|
+
fibers: metrics[:fiber_count],
|
|
60
|
+
rss_mb: metrics[:rss_mb],
|
|
61
|
+
total_objects: metrics[:objects][:total],
|
|
62
|
+
gc_count: metrics[:gc][:count],
|
|
63
|
+
live_slots: metrics[:gc][:heap_live_slots]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
rescue => e
|
|
67
|
+
{ error: e.message }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
register_tool('compare_snapshots',
|
|
71
|
+
'Compare two cross-inspector snapshots. Returns all changed values ' \
|
|
72
|
+
'with deltas.',
|
|
73
|
+
snapshot_a: { type: 'string', description: 'First snapshot ID', required: true },
|
|
74
|
+
snapshot_b: { type: 'string', description: 'Second snapshot ID', required: true }) do |snapshot_a:, snapshot_b:|
|
|
75
|
+
snap_a = @metric_snapshots[snapshot_a.to_s]
|
|
76
|
+
snap_b = @metric_snapshots[snapshot_b.to_s]
|
|
77
|
+
|
|
78
|
+
next { error: "Snapshot '#{snapshot_a}' not found. Available: #{@metric_snapshots.keys.join(', ')}" } unless snap_a
|
|
79
|
+
next { error: "Snapshot '#{snapshot_b}' not found. Available: #{@metric_snapshots.keys.join(', ')}" } unless snap_b
|
|
80
|
+
|
|
81
|
+
changes = compute_snapshot_diff(snap_a, snap_b, '')
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
snapshot_a: snapshot_a,
|
|
85
|
+
snapshot_b: snapshot_b,
|
|
86
|
+
time_between_seconds: (snap_b[:taken_at] - snap_a[:taken_at]).round(2),
|
|
87
|
+
changes: changes
|
|
88
|
+
}
|
|
89
|
+
rescue => e
|
|
90
|
+
{ error: e.message }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
register_tool('list_snapshots',
|
|
94
|
+
'List all stored cross-inspector snapshots') do
|
|
95
|
+
if @metric_snapshots.empty?
|
|
96
|
+
next { message: 'No snapshots taken yet. Call take_snapshot first.', count: 0, snapshots: [] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
count: @metric_snapshots.size,
|
|
101
|
+
snapshots: @metric_snapshots.map do |id, snap|
|
|
102
|
+
{
|
|
103
|
+
snapshot_id: id,
|
|
104
|
+
taken_at: snap[:taken_at].iso8601,
|
|
105
|
+
threads: snap[:threads],
|
|
106
|
+
rss_mb: snap[:rss_mb],
|
|
107
|
+
total_objects: snap[:objects][:total],
|
|
108
|
+
gc_count: snap[:gc][:count]
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
}
|
|
112
|
+
rescue => e
|
|
113
|
+
{ error: e.message }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# --- Helpers ---
|
|
117
|
+
|
|
118
|
+
class << self
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def count_fibers
|
|
122
|
+
# Best-effort fiber count via ObjectSpace
|
|
123
|
+
count = 0
|
|
124
|
+
ObjectSpace.each_object(Fiber) { count += 1 }
|
|
125
|
+
count
|
|
126
|
+
rescue
|
|
127
|
+
0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def gather_db_pool_stats
|
|
131
|
+
return nil unless defined?(::ActiveRecord::Base)
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
pool = ::ActiveRecord::Base.connection_pool
|
|
135
|
+
stats = pool.respond_to?(:stats) ? pool.stats : {}
|
|
136
|
+
{
|
|
137
|
+
size: pool.respond_to?(:size) ? pool.size : nil,
|
|
138
|
+
stats: stats
|
|
139
|
+
}
|
|
140
|
+
rescue
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def gather_cache_stats
|
|
146
|
+
return nil unless defined?(::Rails)
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
cache = ::Rails.cache
|
|
150
|
+
return nil unless cache
|
|
151
|
+
stats = if cache.respond_to?(:stats)
|
|
152
|
+
cache.stats
|
|
153
|
+
elsif cache.respond_to?(:info)
|
|
154
|
+
cache.info
|
|
155
|
+
else
|
|
156
|
+
{}
|
|
157
|
+
end
|
|
158
|
+
{ backend: cache.class.name, stats: stats }
|
|
159
|
+
rescue
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def gather_error_count
|
|
165
|
+
# ErrorTracking inspector stores error counts if available
|
|
166
|
+
if defined?(@error_log) && @error_log.is_a?(Array)
|
|
167
|
+
@error_log.size
|
|
168
|
+
else
|
|
169
|
+
0
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def compute_snapshot_diff(a, b, prefix)
|
|
174
|
+
changes = []
|
|
175
|
+
|
|
176
|
+
if a.is_a?(Hash) && b.is_a?(Hash)
|
|
177
|
+
all_keys = (a.keys | b.keys)
|
|
178
|
+
all_keys.each do |key|
|
|
179
|
+
va = a[key]
|
|
180
|
+
vb = b[key]
|
|
181
|
+
path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
182
|
+
|
|
183
|
+
if va.is_a?(Hash) && vb.is_a?(Hash)
|
|
184
|
+
changes.concat(compute_snapshot_diff(va, vb, path))
|
|
185
|
+
elsif va != vb
|
|
186
|
+
changes << {
|
|
187
|
+
metric: path,
|
|
188
|
+
before: va,
|
|
189
|
+
after: vb,
|
|
190
|
+
delta: (va.is_a?(Numeric) && vb.is_a?(Numeric)) ? (vb - va) : nil
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
elsif a != b
|
|
195
|
+
changes << {
|
|
196
|
+
metric: prefix,
|
|
197
|
+
before: a,
|
|
198
|
+
after: b,
|
|
199
|
+
delta: (a.is_a?(Numeric) && b.is_a?(Numeric)) ? (b - a) : nil
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
changes
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/debug_agent/version.rb
CHANGED
data/lib/debug_agent.rb
CHANGED
|
@@ -41,10 +41,24 @@ require_relative 'debug_agent/inspectors/config_inspector'
|
|
|
41
41
|
require_relative 'debug_agent/inspectors/feature_flags'
|
|
42
42
|
require_relative 'debug_agent/inspectors/endpoint_test'
|
|
43
43
|
require_relative 'debug_agent/inspectors/pool_inspector'
|
|
44
|
+
require_relative 'debug_agent/inspectors/cpu_profile'
|
|
45
|
+
require_relative 'debug_agent/inspectors/leak_detector'
|
|
46
|
+
require_relative 'debug_agent/inspectors/build_info'
|
|
47
|
+
require_relative 'debug_agent/inspectors/snapshot'
|
|
48
|
+
require_relative 'debug_agent/inspectors/service_registry'
|
|
44
49
|
|
|
45
50
|
module DebugAgent
|
|
46
51
|
class Error < StandardError; end
|
|
47
52
|
|
|
53
|
+
# ggcode promotional message — shown once on first require
|
|
54
|
+
unless defined?(@_ggcode_banner_shown)
|
|
55
|
+
@_ggcode_banner_shown = true
|
|
56
|
+
puts ""
|
|
57
|
+
puts " This package was built with ggcode — an AI coding assistant."
|
|
58
|
+
puts " https://github.com/topcheer/ggcode"
|
|
59
|
+
puts ""
|
|
60
|
+
end
|
|
61
|
+
|
|
48
62
|
# Process start time for uptime tracking
|
|
49
63
|
PROCESS_START_TIME = Time.now
|
|
50
64
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: debug-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ggcode
|
|
@@ -165,10 +165,12 @@ files:
|
|
|
165
165
|
- lib/debug_agent/context_compressor.rb
|
|
166
166
|
- lib/debug_agent/engine.rb
|
|
167
167
|
- lib/debug_agent/inspectors/active_record_stats.rb
|
|
168
|
+
- lib/debug_agent/inspectors/build_info.rb
|
|
168
169
|
- lib/debug_agent/inspectors/cache.rb
|
|
169
170
|
- lib/debug_agent/inspectors/concurrent.rb
|
|
170
171
|
- lib/debug_agent/inspectors/config_inspector.rb
|
|
171
172
|
- lib/debug_agent/inspectors/core_ext.rb
|
|
173
|
+
- lib/debug_agent/inspectors/cpu_profile.rb
|
|
172
174
|
- lib/debug_agent/inspectors/endpoint_test.rb
|
|
173
175
|
- lib/debug_agent/inspectors/error_tracking.rb
|
|
174
176
|
- lib/debug_agent/inspectors/faraday.rb
|
|
@@ -177,6 +179,7 @@ files:
|
|
|
177
179
|
- lib/debug_agent/inspectors/health.rb
|
|
178
180
|
- lib/debug_agent/inspectors/http_client.rb
|
|
179
181
|
- lib/debug_agent/inspectors/http_tracker.rb
|
|
182
|
+
- lib/debug_agent/inspectors/leak_detector.rb
|
|
180
183
|
- lib/debug_agent/inspectors/locks.rb
|
|
181
184
|
- lib/debug_agent/inspectors/logging.rb
|
|
182
185
|
- lib/debug_agent/inspectors/metrics.rb
|
|
@@ -191,7 +194,9 @@ files:
|
|
|
191
194
|
- lib/debug_agent/inspectors/runtime.rb
|
|
192
195
|
- lib/debug_agent/inspectors/scheduler.rb
|
|
193
196
|
- lib/debug_agent/inspectors/security.rb
|
|
197
|
+
- lib/debug_agent/inspectors/service_registry.rb
|
|
194
198
|
- lib/debug_agent/inspectors/sidekiq.rb
|
|
199
|
+
- lib/debug_agent/inspectors/snapshot.rb
|
|
195
200
|
- lib/debug_agent/inspectors/system.rb
|
|
196
201
|
- lib/debug_agent/inspectors/threads.rb
|
|
197
202
|
- lib/debug_agent/inspectors/websocket.rb
|