debug-agent 0.2.6 → 0.3.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: 282c047d0e86a70412110ecbd1a3579ee8a71af4b2743de37a4769242df6b634
4
- data.tar.gz: ba97cf4ae8a5757f70c5bf261e5b5b34fb5d752a5543e9b4c657db5fdfc299bf
3
+ metadata.gz: 94cb18c1904795fb713e2486d5f6d16e1bbce1a72d1820dd4122b2b7bc672a4d
4
+ data.tar.gz: 8cf474737f8b84aa5f2a20e5d07f13a02c7345df6805d6b075f72bbb5175fc4f
5
5
  SHA512:
6
- metadata.gz: 0b68399b6dd5fe9d83f9369a9ab896bde2c2d6941a3d8d531b6721427f21b96643e9e8b3793a7da31984b34da7ed79779ba43a30bcbf57e6c20b8451831482f0
7
- data.tar.gz: c4dc62c89f4bfd0100dae01b636130cc0316b5bc1f2981db85d52a8e360a997bf755851187719be8a88f4e05faf443bbc7c535d83b29011ba4c4b2fd8582cb89
6
+ metadata.gz: 48c72eaf044cb325c3d8cb9e5ddc5afabbf85f5830e675795e8afb34182db5eb381f90ce913af411b4cf4acb662f82bf23373f2c12785e3dcee0a2491624e2a6
7
+ data.tar.gz: 20e31311478fd6e5a54e0f693ce5a3cddc27952e5816a56082050bf17e77e4b19aba10d74bde0e95689765ec9e851228c0921724bb566f7609848ab76a844d79
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Ruby Debug Agent
2
2
 
3
- 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, process info, HTTP requests, and more.
3
+ [![Gem Version](https://img.shields.io/badge/gem-debug--agent-red)](https://github.com/topcheer/ruby-debug-agent)
4
+ ![Tools](https://img.shields.io/badge/tools-40-blue)
5
+ ![Inspectors](https://img.shields.io/badge/inspectors-13-green)
6
+
7
+ 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 — **40 diagnostic tools across 13 inspectors**.
4
8
 
5
9
  ## Quick Start
6
10
 
@@ -51,9 +55,10 @@ http://localhost:4567/agent
51
55
  - **Context compression** — automatically summarizes old conversation when token limit is approached
52
56
  - **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
53
57
  - **Max tool rounds** (25) with forced final summary when limit is reached
54
- - **26 diagnostic tools** across 8 inspectors
58
+ - **40 diagnostic tools** across **13 inspectors**
59
+ - Zero external dependencies (no Datadog, no Grafana, no APM)
55
60
 
56
- ## Inspectors & Tools (26)
61
+ ## Inspectors & Tools (40)
57
62
 
58
63
  ### GC Inspector
59
64
  | Tool | Description |
@@ -75,6 +80,7 @@ http://localhost:4567/agent
75
80
  | `get_thread_list` | List all threads with status and backtrace summary |
76
81
  | `get_thread_count` | Thread count |
77
82
  | `get_main_thread_info` | Main thread priority, status |
83
+ | `get_thread_backtrace` | Full backtrace for a specific thread |
78
84
 
79
85
  ### Route Inspector
80
86
  | Tool | Description |
@@ -88,6 +94,7 @@ http://localhost:4567/agent
88
94
  | `get_process_info` | PID, ppid, platform, Ruby version, uptime |
89
95
  | `get_cpu_time` | Process.times() user/sys CPU time |
90
96
  | `get_environment_variables` | Environment variables (masked secrets) |
97
+ | `get_process_memory` | Process RSS, VMS, and memory growth trend |
91
98
 
92
99
  ### Runtime Inspector
93
100
  | Tool | Description |
@@ -108,9 +115,43 @@ http://localhost:4567/agent
108
115
  | Tool | Description |
109
116
  |------|-------------|
110
117
  | `get_system_info` | Hostname, CPU cores, disk |
111
- | `get_disk_usage` | Disk usage for working directory |
118
+ | `get_disk_usage` | Disk usage for the working directory |
112
119
  | `get_file_descriptors` | Open file descriptor count |
113
120
 
121
+ ### Redis Inspector
122
+ | Tool | Description |
123
+ |------|-------------|
124
+ | `get_redis_info` | Redis server info: memory, clients, persistence |
125
+ | `get_redis_keys` | Scan Redis keyspace with pattern matching |
126
+ | `get_redis_slowlog` | Redis slow query log entries |
127
+ | `get_redis_stats` | Per-command call count, hit/miss ratio, keyspace stats |
128
+
129
+ ### Rails Inspector
130
+ | Tool | Description |
131
+ |------|-------------|
132
+ | `get_rails_models` | List ActiveRecord models with table names and associations |
133
+ | `get_rails_routes` | List Rails routes with helper names and HTTP verbs |
134
+ | `get_rails_db_schema` | Database schema version and pending migrations |
135
+
136
+ ### Sidekiq Inspector
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `get_sidekiq_queues` | Queue list with depth, latency, and size |
140
+ | `get_sidekiq_workers` | Active Sidekiq workers with job and host info |
141
+ | `get_sidekiq_jobs` | Inspect jobs in a queue/retry set with payload |
142
+
143
+ ### Puma Inspector
144
+ | Tool | Description |
145
+ |------|-------------|
146
+ | `get_puma_stats` | Puma cluster stats: workers, threads, running/backlog, boot time |
147
+
148
+ ### Fibers/Signals Inspector
149
+ | Tool | Description |
150
+ |------|-------------|
151
+ | `get_fiber_list` | List active Ruby Fibers with state and backtrace |
152
+ | `get_signal_handlers` | List registered signal handlers (Signal.trap) |
153
+ | `get_trap_handlers` | Inspect trap handlers for SIGINT, SIGTERM, etc. |
154
+
114
155
  ## Custom Tools
115
156
 
116
157
  ```ruby
@@ -133,12 +174,36 @@ end
133
174
 
134
175
  ## Run the Demo
135
176
 
177
+ The demo uses **Sinatra** + **redis-rb** + **SQLite** + **Sidekiq**. Start Redis with Docker Compose first:
178
+
179
+ ### Docker Compose
180
+
181
+ ```yaml
182
+ # docker-compose.yml
183
+ services:
184
+ redis:
185
+ image: redis:7-alpine
186
+ ports:
187
+ - "6379:6379"
188
+ command: redis-server --save 60 1 --loglevel warning
189
+ ```
190
+
191
+ ```bash
192
+ docker compose up -d
193
+ ```
194
+
195
+ ### Start the app
196
+
136
197
  ```bash
137
198
  export LLM_API_KEY=your-key
138
199
  cd demo && ruby -I../lib app.rb
139
200
  # Open http://localhost:4567/agent
140
201
  ```
141
202
 
203
+ ## RubyGems
204
+
205
+ [![Gem](https://img.shields.io/badge/rubygems-debug--agent-red)](https://github.com/topcheer/ruby-debug-agent)
206
+
142
207
  ## License
143
208
 
144
209
  MIT
@@ -0,0 +1,105 @@
1
+ module DebugAgent
2
+ register_tool('get_fiber_info',
3
+ 'List alive fibers (Ruby 3.0+ via Fiber.list) with status and backtrace') do
4
+ unless Fiber.respond_to?(:list)
5
+ return {
6
+ supported: false,
7
+ message: 'Fiber.list requires Ruby 3.0+. ' \
8
+ "Current Ruby: #{RUBY_VERSION} (#{RUBY_ENGINE})"
9
+ }
10
+ end
11
+
12
+ fibers = Fiber.list.map do |fiber|
13
+ backtrace =
14
+ begin
15
+ fiber.backtrace || []
16
+ rescue => e
17
+ ["<unable to get backtrace: #{e.message}>"]
18
+ end
19
+
20
+ {
21
+ object_id: fiber.object_id,
22
+ to_s: fiber.to_s,
23
+ alive: fiber.alive?,
24
+ resizable: fiber.respond_to?(:resizable?) ? fiber.resizable? : nil,
25
+ storage: fiber.respond_to?(:storage) ? (fiber.storage&.keys&.map(&:to_s) rescue nil) : nil,
26
+ backtrace_summary: backtrace.first(5),
27
+ backtrace_length: backtrace.size
28
+ }
29
+ end
30
+
31
+ {
32
+ supported: true,
33
+ total_fibers: fibers.size,
34
+ alive_fibers: fibers.count { |f| f[:alive] },
35
+ fibers: fibers
36
+ }
37
+ rescue => e
38
+ { error: e.message }
39
+ end
40
+
41
+ register_tool('get_signal_handlers',
42
+ 'List registered signal handlers (Signal.trap) and default handlers') do
43
+ handlers = []
44
+
45
+ Signal.list.each do |name, number|
46
+ begin
47
+ current = Signal.trap(name)
48
+ rescue => e
49
+ current = "<error: #{e.message}>"
50
+ end
51
+
52
+ handlers << {
53
+ signal: name,
54
+ number: number,
55
+ handler:
56
+ case current
57
+ when 'DEFAULT' then 'DEFAULT (system default)'
58
+ when 'IGNORE' then 'IGNORE (ignored)'
59
+ when 'EXIT' then 'EXIT (terminate process)'
60
+ when 'SYSTEM_DEFAULT' then 'SYSTEM_DEFAULT'
61
+ when String then current
62
+ when Proc
63
+ begin
64
+ src = current.source_location
65
+ src ? "Proc at #{src.join(':')}" : 'Proc (unknown source)'
66
+ rescue
67
+ 'Proc'
68
+ end
69
+ else
70
+ current.inspect
71
+ end
72
+ }
73
+ end
74
+
75
+ {
76
+ total_signals: handlers.size,
77
+ signals: handlers.sort_by { |h| h[:number] }
78
+ }
79
+ rescue => e
80
+ { error: e.message }
81
+ end
82
+
83
+ register_tool('get_encoding_info',
84
+ 'Get Ruby encoding info: Encoding.list, default external/internal/locale encodings') do
85
+ list = Encoding.list.map do |enc|
86
+ {
87
+ name: enc.name,
88
+ aliases: Encoding.aliases.select { |_, n| n == enc.name }.keys,
89
+ dummy: enc.dummy?,
90
+ ascii_compatible: enc.ascii_compatible?
91
+ }
92
+ end
93
+
94
+ {
95
+ total_encodings: list.size,
96
+ default_external: Encoding.default_external.to_s,
97
+ default_internal: Encoding.default_internal&.to_s,
98
+ locale: Encoding.find('locale').to_s,
99
+ filesystem: Encoding.find('filesystem').to_s,
100
+ encodings: list
101
+ }
102
+ rescue => e
103
+ { error: e.message }
104
+ end
105
+ end
@@ -0,0 +1,73 @@
1
+ module DebugAgent
2
+ register_tool('get_puma_stats',
3
+ 'Get Puma worker stats: running workers, threads, backlog, requests ' \
4
+ '(uses Puma.stats if Puma is loaded)') do
5
+ unless defined?(::Puma)
6
+ return { error: 'Puma is not loaded (puma gem not installed)' }
7
+ end
8
+
9
+ raw =
10
+ if ::Puma.respond_to?(:stats_hash)
11
+ ::Puma.stats_hash
12
+ elsif ::Puma.respond_to?(:stats)
13
+ # Puma.stats returns a JSON string in older versions; parse it.
14
+ s = ::Puma.stats
15
+ s.is_a?(String) ? JSON.parse(s, symbolize_names: true) : s
16
+ else
17
+ return { error: 'Puma is loaded but does not expose Puma.stats' }
18
+ end
19
+
20
+ raw = raw.respond_to?(:transform_keys) ? raw : raw
21
+
22
+ # Normalize into a structured summary.
23
+ workers = []
24
+
25
+ # Clustered mode: raw is { workers: N, booted_workers: N, old_workers: N,
26
+ # phase: N, worker_status: [...] }
27
+ # Single mode: raw is { backed_up: N, running: N, pool_capacity: N,
28
+ # max_threads: N, requests_count: N }
29
+ if raw.is_a?(Hash) && raw.key?(:worker_status)
30
+ raw[:worker_status].each_with_index do |w, i|
31
+ last_stats = w[:last_status] || {}
32
+ workers << {
33
+ index: i,
34
+ pid: w[:pid],
35
+ index_field: w[:index],
36
+ booted: w[:booted],
37
+ last_checkin: w[:last_checkin],
38
+ running_threads: last_stats[:running],
39
+ pool_capacity: last_stats[:pool_capacity],
40
+ max_threads: last_stats[:max_threads],
41
+ backlog: last_stats[:backed_up],
42
+ requests: last_stats[:requests_count]
43
+ }
44
+ end
45
+
46
+ {
47
+ mode: 'cluster',
48
+ configured_workers: raw[:workers],
49
+ booted_workers: raw[:booted_workers],
50
+ old_workers: raw[:old_workers],
51
+ phase: raw[:phase],
52
+ workers: workers,
53
+ total_running_threads: workers.sum { |w| w[:running_threads].to_i },
54
+ total_backlog: workers.sum { |w| w[:backlog].to_i },
55
+ total_requests: workers.sum { |w| w[:requests].to_i }
56
+ }
57
+ elsif raw.is_a?(Hash)
58
+ {
59
+ mode: 'single',
60
+ running_threads: raw[:running],
61
+ pool_capacity: raw[:pool_capacity],
62
+ max_threads: raw[:max_threads],
63
+ backlog: raw[:backed_up],
64
+ requests: raw[:requests_count],
65
+ raw: raw
66
+ }
67
+ else
68
+ { raw: raw }
69
+ end
70
+ rescue => e
71
+ { error: e.message }
72
+ end
73
+ end
@@ -0,0 +1,127 @@
1
+ module DebugAgent
2
+ register_tool('get_rails_routes',
3
+ 'List all Rails routes: verb, path, controller#action ' \
4
+ '(requires Rails.application.routes)') do
5
+ unless defined?(::Rails) && defined?(::ActionDispatch)
6
+ return { error: 'Rails is not loaded (Rails::Application not found)' }
7
+ end
8
+
9
+ routes_set = ::Rails.application.routes.routes
10
+
11
+ routes = routes_set.map do |route|
12
+ {
13
+ name: route.name,
14
+ verb: route.verb.source.gsub(/[$^]/, ''),
15
+ path: route.path.spec.to_s,
16
+ controller: route.defaults[:controller]&.to_s,
17
+ action: route.defaults[:action]&.to_s,
18
+ internal: route.internal?
19
+ }
20
+ end
21
+
22
+ {
23
+ total: routes.size,
24
+ routes: routes
25
+ }
26
+ rescue => e
27
+ { error: e.message }
28
+ end
29
+
30
+ register_tool('get_rails_models',
31
+ 'List ActiveRecord models: class name, table name, columns ' \
32
+ '(iterates ActiveRecord::Base.descendants)') do
33
+ unless defined?(::ActiveRecord)
34
+ return { error: 'ActiveRecord is not loaded (ActiveRecord::Base not found)' }
35
+ end
36
+
37
+ models = ::ActiveRecord::Base.descendants.map do |model|
38
+ columns =
39
+ begin
40
+ if model.table_exists?
41
+ model.columns.map do |col|
42
+ {
43
+ name: col.name,
44
+ type: col.sql_type_metadata&.type.to_s,
45
+ sql_type: col.sql_type_metadata&.sql_type.to_s,
46
+ null: col.null,
47
+ default: col.default,
48
+ primary: col.name == model.primary_key
49
+ }
50
+ end
51
+ else
52
+ []
53
+ end
54
+ rescue => e
55
+ [{ error: e.message }]
56
+ end
57
+
58
+ table_name = begin
59
+ model.table_name
60
+ rescue
61
+ nil
62
+ end
63
+
64
+ table_exists = begin
65
+ model.table_exists?
66
+ rescue
67
+ false
68
+ end
69
+
70
+ {
71
+ class_name: model.name,
72
+ table_name: table_name,
73
+ table_exists: table_exists,
74
+ column_count: columns.size,
75
+ columns: columns
76
+ }
77
+ end
78
+
79
+ {
80
+ total: models.size,
81
+ models: models.sort_by { |m| m[:class_name].to_s }
82
+ }
83
+ rescue => e
84
+ { error: e.message }
85
+ end
86
+
87
+ register_tool('get_rails_schema',
88
+ 'Get ActiveRecord schema cache: table names and column definitions ' \
89
+ '(uses ActiveRecord::Base.connection.tables)') do
90
+ unless defined?(::ActiveRecord)
91
+ return { error: 'ActiveRecord is not loaded (ActiveRecord::Base not found)' }
92
+ end
93
+
94
+ connection = ::ActiveRecord::Base.connection
95
+ tables = connection.tables
96
+
97
+ schema = tables.map do |table|
98
+ columns =
99
+ begin
100
+ connection.columns(table).map do |col|
101
+ {
102
+ name: col.name,
103
+ type: col.sql_type_metadata&.type.to_s,
104
+ sql_type: col.sql_type_metadata&.sql_type.to_s,
105
+ null: col.null,
106
+ default: col.default
107
+ }
108
+ end
109
+ rescue => e
110
+ [{ error: e.message }]
111
+ end
112
+
113
+ {
114
+ table: table,
115
+ column_count: columns.size,
116
+ columns: columns
117
+ }
118
+ end
119
+
120
+ {
121
+ total_tables: schema.size,
122
+ tables: schema
123
+ }
124
+ rescue => e
125
+ { error: e.message }
126
+ end
127
+ end
@@ -0,0 +1,205 @@
1
+ module DebugAgent
2
+ # Registry for Redis clients (redis-rb). Applications register their
3
+ # Redis / connection-pool objects so the inspector can introspect them.
4
+ #
5
+ # DebugAgent.register_redis_client(:cache, Redis.new(url: ENV['REDIS_URL']))
6
+ @redis_clients = {}
7
+
8
+ class << self
9
+ attr_reader :redis_clients
10
+
11
+ def register_redis_client(name, client)
12
+ @redis_clients[name.to_s] = client
13
+ end
14
+ end
15
+
16
+ # Resolve a registered Redis client. Accepts a bare Redis object or a
17
+ # ConnectionPool (redis-rb ships ConnectionPool support). We yield a
18
+ # usable connection object to the block.
19
+ def self.with_redis(name = nil)
20
+ name, client = if name
21
+ [name.to_s, redis_clients[name.to_s]]
22
+ else
23
+ redis_clients.first
24
+ end
25
+
26
+ return [nil, nil] unless client
27
+
28
+ [name, client]
29
+ end
30
+
31
+ register_tool('get_redis_pool_stats',
32
+ 'Get Redis connection pool stats: registered clients, pool size, ' \
33
+ 'available/in-use connections, host, port, db') do |name: nil|
34
+ return { error: 'Redis is not loaded (redis gem not installed)' } unless defined?(::Redis)
35
+ return { error: 'No Redis clients registered. Call DebugAgent.register_redis_client(:name, client).' } if redis_clients.empty?
36
+
37
+ targets = name ? { name.to_s => redis_clients[name.to_s] } : redis_clients
38
+ targets = targets.reject { |_, c| c.nil? }
39
+ return { error: "No Redis client registered under '#{name}'" } if targets.empty?
40
+
41
+ stats = targets.map do |client_name, client|
42
+ begin
43
+ info = {}
44
+
45
+ # ConnectionPool vs bare Redis
46
+ pool = nil
47
+ redis_conn = nil
48
+
49
+ if defined?(::ConnectionPool) && client.is_a?(::ConnectionPool)
50
+ pool = client
51
+ client.with { |c| redis_conn = c }
52
+ else
53
+ redis_conn = client
54
+ end
55
+
56
+ # Basic server connection details
57
+ info[:client_name] = client_name
58
+ info[:type] = pool ? 'connection_pool' : 'redis'
59
+
60
+ if redis_conn.respond_to?(:connection)
61
+ conn = redis_conn.connection rescue {}
62
+ info[:host] = conn[:host]
63
+ info[:port] = conn[:port]
64
+ info[:db] = conn[:db]
65
+ end
66
+
67
+ if pool
68
+ # ConnectionPool does not expose live counters publicly; report
69
+ # configured size. Available/in-use are best-effort via instance vars.
70
+ info[:pool_configured_size] = pool.instance_variable_get(:@size)
71
+ info[:pool_available] = pool.instance_variable_get(:@available)&.length
72
+ info[:pool_in_use] = info[:pool_configured_size].to_i - info[:pool_available].to_i
73
+ else
74
+ info[:pool_configured_size] = 1
75
+ info[:pool_available] = 1
76
+ info[:pool_in_use] = 0
77
+ end
78
+
79
+ # Ping to confirm reachability
80
+ info[:connected] = begin
81
+ redis_conn.ping == 'PONG'
82
+ rescue => e
83
+ info[:ping_error] = e.message
84
+ false
85
+ end
86
+
87
+ info
88
+ rescue => e
89
+ { client_name: client_name, error: e.message }
90
+ end
91
+ end
92
+
93
+ { clients: stats }
94
+ rescue => e
95
+ { error: e.message }
96
+ end
97
+
98
+ register_tool('get_redis_info',
99
+ 'Execute Redis INFO command and parse key sections ' \
100
+ '(Server, Clients, Memory, Stats, Keyspace)') do |name: nil|
101
+ return { error: 'Redis is not loaded (redis gem not installed)' } unless defined?(::Redis)
102
+
103
+ _name, client = DebugAgent.with_redis(name)
104
+ return { error: 'No Redis clients registered. Call DebugAgent.register_redis_client(:name, client).' } unless client
105
+
106
+ redis_conn =
107
+ if defined?(::ConnectionPool) && client.is_a?(::ConnectionPool)
108
+ client.with { |c| c }
109
+ else
110
+ client
111
+ end
112
+
113
+ raw = redis_conn.info
114
+ sections = {}
115
+
116
+ # Group known INFO keys into sections
117
+ section_keys = {
118
+ 'Server' => %w[redis_version redis_mode os arch_bits tcp_port uptime_in_seconds uptime_in_days],
119
+ 'Clients' => %w[connected_clients blocked_clients tracking_clients],
120
+ 'Memory' => %w[used_memory used_memory_human used_memory_peak used_memory_peak_human used_memory_rss mem_fragmentation_ratio maxmemory maxmemory_human],
121
+ 'Stats' => %w[total_connections_received total_commands_processed instantaneous_ops_per_sec keyspace_hits keyspace_misses expired_keys evicted_keys pubsub_channels pubsub_patterns],
122
+ 'Persistence' => %w[rdb_last_bgsave_status rdb_changes_since_last_save aof_enabled]
123
+ }
124
+
125
+ section_keys.each do |section, keys|
126
+ sections[section] = keys.each_with_object({}) do |k, h|
127
+ h[k] = raw[k] if raw.key?(k)
128
+ end
129
+ end
130
+
131
+ # Keyspace section looks like "db0:keys=10,expires=0,avg_ttl=0"
132
+ keyspace = {}
133
+ raw.each do |k, v|
134
+ next unless k =~ /^db\d+$/
135
+ parsed = v.split(',').each_with_object({}) do |pair, h|
136
+ key, val = pair.split('=')
137
+ h[key] = val
138
+ end
139
+ keyspace[k] = parsed
140
+ end
141
+ sections['Keyspace'] = keyspace unless keyspace.empty?
142
+
143
+ { sections: sections, raw_keys: raw.size }
144
+ rescue => e
145
+ { error: e.message }
146
+ end
147
+
148
+ register_tool('get_redis_latency',
149
+ 'Measure Redis PING latency over 10 samples (min/avg/max in ms)') do |name: nil, samples: 10|
150
+ return { error: 'Redis is not loaded (redis gem not installed)' } unless defined?(::Redis)
151
+
152
+ _name, client = DebugAgent.with_redis(name)
153
+ return { error: 'No Redis clients registered. Call DebugAgent.register_redis_client(:name, client).' } unless client
154
+
155
+ samples = samples.to_i
156
+ samples = 10 if samples <= 0
157
+
158
+ redis_conn =
159
+ if defined?(::ConnectionPool) && client.is_a?(::ConnectionPool)
160
+ client.with { |c| c }
161
+ else
162
+ client
163
+ end
164
+
165
+ latencies = []
166
+ samples.times do
167
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
168
+ redis_conn.ping
169
+ finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
170
+ latencies << ((finish - start) * 1000.0)
171
+ end
172
+
173
+ {
174
+ samples: latencies.size,
175
+ min_ms: latencies.min.round(3),
176
+ avg_ms: (latencies.sum / latencies.size).round(3),
177
+ max_ms: latencies.max.round(3),
178
+ all_ms: latencies.map { |l| l.round(3) }
179
+ }
180
+ rescue => e
181
+ { error: e.message }
182
+ end
183
+
184
+ register_tool('get_redis_db_size',
185
+ 'Execute Redis DBSIZE command (number of keys in current db)') do |name: nil|
186
+ return { error: 'Redis is not loaded (redis gem not installed)' } unless defined?(::Redis)
187
+
188
+ _name, client = DebugAgent.with_redis(name)
189
+ return { error: 'No Redis clients registered. Call DebugAgent.register_redis_client(:name, client).' } unless client
190
+
191
+ redis_conn =
192
+ if defined?(::ConnectionPool) && client.is_a?(::ConnectionPool)
193
+ client.with { |c| c }
194
+ else
195
+ client
196
+ end
197
+
198
+ {
199
+ db_size: redis_conn.dbsize,
200
+ client: _name
201
+ }
202
+ rescue => e
203
+ { error: e.message }
204
+ end
205
+ end
@@ -0,0 +1,121 @@
1
+ module DebugAgent
2
+ # Registry for Sidekiq queue objects (Sidekiq::Queue instances). Applications
3
+ # register named queues so the inspector can read live stats.
4
+ #
5
+ # DebugAgent.register_sidekiq_queue(:default, Sidekiq::Queue.new('default'))
6
+ @sidekiq_queues = {}
7
+
8
+ class << self
9
+ attr_reader :sidekiq_queues
10
+
11
+ def register_sidekiq_queue(name, queue)
12
+ @sidekiq_queues[name.to_s] = queue
13
+ end
14
+ end
15
+
16
+ register_tool('get_sidekiq_queues',
17
+ 'Get Sidekiq queue stats: processed, failed, enqueued totals and ' \
18
+ 'per-queue sizes (requires sidekiq)') do
19
+ unless defined?(::Sidekiq)
20
+ return { error: 'Sidekiq is not loaded (sidekiq gem not installed)' }
21
+ end
22
+
23
+ stats = ::Sidekiq::Stats.new
24
+
25
+ queues =
26
+ if sidekiq_queues.any?
27
+ sidekiq_queues.map do |name, queue|
28
+ {
29
+ name: name,
30
+ size: queue.size,
31
+ latency_seconds: queue.latency.to_f.round(3)
32
+ }
33
+ end
34
+ else
35
+ ::Sidekiq::Queue.all.map do |queue|
36
+ {
37
+ name: queue.name,
38
+ size: queue.size,
39
+ latency_seconds: queue.latency.to_f.round(3)
40
+ }
41
+ end
42
+ end
43
+
44
+ {
45
+ processed: stats.processed,
46
+ failed: stats.failed,
47
+ enqueued: stats.enqueued,
48
+ queues: queues
49
+ }
50
+ rescue => e
51
+ { error: e.message }
52
+ end
53
+
54
+ register_tool('get_sidekiq_workers',
55
+ 'Get Sidekiq worker stats: busy workers, processes, total concurrency ' \
56
+ '(requires sidekiq)') do
57
+ unless defined?(::Sidekiq)
58
+ return { error: 'Sidekiq is not loaded (sidekiq gem not installed)' }
59
+ end
60
+
61
+ processes = ::Sidekiq::ProcessSet.new.to_a
62
+
63
+ total_busy = processes.sum(&:busy)
64
+ total_concurrency = processes.sum(&:concurrency)
65
+
66
+ process_list = processes.map do |p|
67
+ {
68
+ identity: p.identity,
69
+ hostname: p['hostname'],
70
+ pid: p['pid'],
71
+ started_at: p['started_at'],
72
+ concurrency: p.concurrency,
73
+ busy: p.busy,
74
+ queues: p['queues'] || []
75
+ }
76
+ end
77
+
78
+ {
79
+ busy: total_busy,
80
+ processes: process_list.size,
81
+ total_concurrency: total_concurrency,
82
+ process_list: process_list
83
+ }
84
+ rescue => e
85
+ { error: e.message }
86
+ end
87
+
88
+ register_tool('get_sidekiq_retries',
89
+ 'Get Sidekiq retry set: count and sample jobs (requires sidekiq)') do |sample_size: 10|
90
+ unless defined?(::Sidekiq)
91
+ return { error: 'Sidekiq is not loaded (sidekiq gem not installed)' }
92
+ end
93
+
94
+ retry_set = ::Sidekiq::RetrySet.new
95
+ sample_size = sample_size.to_i
96
+ sample_size = 10 if sample_size <= 0
97
+
98
+ samples = []
99
+ retry_set.first(sample_size).each do |job|
100
+ samples << {
101
+ class: job.klass,
102
+ queue: job.queue,
103
+ args: job.args,
104
+ retry_count: job['retry_count'],
105
+ failed_at: job['failed_at'],
106
+ next_retry: job['next_at'] || job.at,
107
+ jid: job.jid,
108
+ error_message: job['error_message'],
109
+ error_class: job['error_class']
110
+ }
111
+ end
112
+
113
+ {
114
+ retry_count: retry_set.size,
115
+ sample_size: samples.size,
116
+ sample_jobs: samples
117
+ }
118
+ rescue => e
119
+ { error: e.message }
120
+ end
121
+ end
@@ -1,3 +1,3 @@
1
1
  module DebugAgent
2
- VERSION = '0.2.6'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
data/lib/debug_agent.rb CHANGED
@@ -18,6 +18,11 @@ require_relative 'debug_agent/inspectors/object_space'
18
18
  require_relative 'debug_agent/inspectors/threads'
19
19
  require_relative 'debug_agent/inspectors/routes'
20
20
  require_relative 'debug_agent/inspectors/process_info'
21
+ require_relative 'debug_agent/inspectors/core_ext'
22
+ require_relative 'debug_agent/inspectors/redis'
23
+ require_relative 'debug_agent/inspectors/rails'
24
+ require_relative 'debug_agent/inspectors/sidekiq'
25
+ require_relative 'debug_agent/inspectors/puma'
21
26
 
22
27
  module DebugAgent
23
28
  class Error < StandardError; end
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.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ggcode
@@ -51,8 +51,106 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: sinatra
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '4.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '4.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: redis
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: connection_pool
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.4'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.4'
96
+ - !ruby/object:Gem::Dependency
97
+ name: sidekiq
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '7.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '7.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: puma
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '6.0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '6.0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rackup
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '2.0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '2.0'
54
152
  description: Embed an AI debugging assistant into your Ruby web app. Inspect GC, threads,
55
- memory, ObjectSpace, routes, HTTP requests, and more.
153
+ memory, ObjectSpace, routes, HTTP requests, Redis, Sidekiq, Puma, and more.
56
154
  email:
57
155
  - noreply@ggcode.dev
58
156
  executables: []
@@ -66,12 +164,17 @@ files:
66
164
  - lib/debug_agent/config.rb
67
165
  - lib/debug_agent/context_compressor.rb
68
166
  - lib/debug_agent/engine.rb
167
+ - lib/debug_agent/inspectors/core_ext.rb
69
168
  - lib/debug_agent/inspectors/gc.rb
70
169
  - lib/debug_agent/inspectors/http_tracker.rb
71
170
  - lib/debug_agent/inspectors/object_space.rb
72
171
  - lib/debug_agent/inspectors/process_info.rb
172
+ - lib/debug_agent/inspectors/puma.rb
173
+ - lib/debug_agent/inspectors/rails.rb
174
+ - lib/debug_agent/inspectors/redis.rb
73
175
  - lib/debug_agent/inspectors/routes.rb
74
176
  - lib/debug_agent/inspectors/runtime.rb
177
+ - lib/debug_agent/inspectors/sidekiq.rb
75
178
  - lib/debug_agent/inspectors/system.rb
76
179
  - lib/debug_agent/inspectors/threads.rb
77
180
  - lib/debug_agent/llm_client.rb