debug-agent 0.2.6 → 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.
@@ -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.4.0'.freeze
3
3
  end
data/lib/debug_agent.rb CHANGED
@@ -18,6 +18,18 @@ 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'
26
+ require_relative 'debug_agent/inspectors/logging'
27
+ require_relative 'debug_agent/inspectors/cache'
28
+ require_relative 'debug_agent/inspectors/http_client'
29
+ require_relative 'debug_agent/inspectors/metrics'
30
+ require_relative 'debug_agent/inspectors/active_record_stats'
31
+ require_relative 'debug_agent/inspectors/faraday'
32
+ require_relative 'debug_agent/inspectors/concurrent'
21
33
 
22
34
  module DebugAgent
23
35
  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.4.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,24 @@ 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/active_record_stats.rb
168
+ - lib/debug_agent/inspectors/cache.rb
169
+ - lib/debug_agent/inspectors/concurrent.rb
170
+ - lib/debug_agent/inspectors/core_ext.rb
171
+ - lib/debug_agent/inspectors/faraday.rb
69
172
  - lib/debug_agent/inspectors/gc.rb
173
+ - lib/debug_agent/inspectors/http_client.rb
70
174
  - lib/debug_agent/inspectors/http_tracker.rb
175
+ - lib/debug_agent/inspectors/logging.rb
176
+ - lib/debug_agent/inspectors/metrics.rb
71
177
  - lib/debug_agent/inspectors/object_space.rb
72
178
  - lib/debug_agent/inspectors/process_info.rb
179
+ - lib/debug_agent/inspectors/puma.rb
180
+ - lib/debug_agent/inspectors/rails.rb
181
+ - lib/debug_agent/inspectors/redis.rb
73
182
  - lib/debug_agent/inspectors/routes.rb
74
183
  - lib/debug_agent/inspectors/runtime.rb
184
+ - lib/debug_agent/inspectors/sidekiq.rb
75
185
  - lib/debug_agent/inspectors/system.rb
76
186
  - lib/debug_agent/inspectors/threads.rb
77
187
  - lib/debug_agent/llm_client.rb