debug-agent 0.5.0 → 0.6.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 +62 -5
- data/lib/debug_agent/inspectors/config_inspector.rb +137 -0
- data/lib/debug_agent/inspectors/endpoint_test.rb +284 -0
- data/lib/debug_agent/inspectors/feature_flags.rb +215 -0
- data/lib/debug_agent/inspectors/locks.rb +343 -0
- data/lib/debug_agent/inspectors/migration.rb +150 -0
- data/lib/debug_agent/inspectors/pool_inspector.rb +320 -0
- data/lib/debug_agent/version.rb +1 -1
- data/lib/debug_agent.rb +6 -0
- metadata +7 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
module DebugAgent
|
|
4
|
+
# Register connection pools for deep-dive inspection.
|
|
5
|
+
#
|
|
6
|
+
# DebugAgent.register_pool(:primary, ActiveRecord::Base.connection_pool)
|
|
7
|
+
@registered_pools = {}
|
|
8
|
+
@pool_stats = {}
|
|
9
|
+
@pool_meta_lock = Mutex.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_reader :registered_pools, :pool_stats
|
|
13
|
+
|
|
14
|
+
def register_pool(name, pool)
|
|
15
|
+
pool_name = name.to_s
|
|
16
|
+
@registered_pools[pool_name] = pool
|
|
17
|
+
@pool_meta_lock.synchronize do
|
|
18
|
+
@pool_stats[pool_name] ||= {
|
|
19
|
+
acquire_times: [],
|
|
20
|
+
last_checked_at: nil
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
pool
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def ar_pool_stats
|
|
31
|
+
return nil unless defined?(::ActiveRecord::Base)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
pool = ::ActiveRecord::Base.connection_pool
|
|
35
|
+
stats = {}
|
|
36
|
+
|
|
37
|
+
# ActiveRecord::ConnectionPool#stats (Rails 5+)
|
|
38
|
+
if pool.respond_to?(:stats)
|
|
39
|
+
stats = pool.stats
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
result = {
|
|
43
|
+
size: pool.respond_to?(:size) ? pool.size : nil,
|
|
44
|
+
class: pool.class.name,
|
|
45
|
+
stats: stats
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Try to get more detail via instance variables if available
|
|
49
|
+
if pool.instance_variable_defined?(:@available)
|
|
50
|
+
available = pool.instance_variable_get(:@available)
|
|
51
|
+
if available.respond_to?(:size)
|
|
52
|
+
result[:available_connections] = available.size
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if pool.instance_variable_defined?(:@connections)
|
|
57
|
+
connections = pool.instance_variable_get(:@connections)
|
|
58
|
+
if connections.is_a?(Array)
|
|
59
|
+
result[:total_connections] = connections.size
|
|
60
|
+
result[:connections] = connections.map do |conn|
|
|
61
|
+
conn_info(conn)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
result
|
|
67
|
+
rescue => e
|
|
68
|
+
{ error: "ActiveRecord pool inspection failed: #{e.message}" }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def conn_info(conn)
|
|
73
|
+
info = { object_id: conn.object_id, class: conn.class.name }
|
|
74
|
+
if conn.respond_to?(:active?)
|
|
75
|
+
begin
|
|
76
|
+
info[:active] = conn.active?
|
|
77
|
+
rescue
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
if conn.respond_to?(:in_use?)
|
|
82
|
+
begin
|
|
83
|
+
info[:in_use] = conn.in_use?
|
|
84
|
+
rescue
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
if conn.respond_to?(:seconds_idle)
|
|
89
|
+
begin
|
|
90
|
+
info[:seconds_idle] = conn.seconds_idle
|
|
91
|
+
rescue
|
|
92
|
+
nil
|
|
93
|
+
end end
|
|
94
|
+
info
|
|
95
|
+
rescue
|
|
96
|
+
{ object_id: conn&.object_id, class: conn&.class&.name }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pool_details_for(name, pool)
|
|
100
|
+
if defined?(::ActiveRecord::ConnectionAdapters::AbstractAdapter) &&
|
|
101
|
+
pool.is_a?(::ActiveRecord::ConnectionAdapters::AbstractAdapter)
|
|
102
|
+
# Direct adapter, not a pool
|
|
103
|
+
return {
|
|
104
|
+
name: name,
|
|
105
|
+
class: pool.class.name,
|
|
106
|
+
type: 'adapter',
|
|
107
|
+
active: (pool.active? if pool.respond_to?(:active?)),
|
|
108
|
+
in_use: (pool.in_use? if pool.respond_to?(:in_use?)),
|
|
109
|
+
seconds_idle: (pool.seconds_idle if pool.respond_to?(:seconds_idle))
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ActiveRecord ConnectionPool
|
|
114
|
+
if pool.respond_to?(:stats) || pool.respond_to?(:size)
|
|
115
|
+
stats = {}
|
|
116
|
+
stats[:size] = pool.size if pool.respond_to?(:size)
|
|
117
|
+
stats[:stats] = pool.stats if pool.respond_to?(:stats)
|
|
118
|
+
|
|
119
|
+
if pool.instance_variable_defined?(:@connections)
|
|
120
|
+
conns = pool.instance_variable_get(:@connections)
|
|
121
|
+
if conns.is_a?(Array)
|
|
122
|
+
stats[:total_connections] = conns.size
|
|
123
|
+
stats[:connections] = conns.map { |c| conn_info(c) }
|
|
124
|
+
stats[:busy_connections] = conns.count { |c| c.respond_to?(:in_use?) ? c.in_use? : false }
|
|
125
|
+
stats[:idle_connections] = conns.count { |c| c.respond_to?(:in_use?) ? !c.in_use? : true }
|
|
126
|
+
stats[:dead_connections] = conns.count do |c|
|
|
127
|
+
c.respond_to?(:active?) ? !c.active? : false
|
|
128
|
+
rescue
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return { name: name, class: pool.class.name, type: 'active_record_pool' }.merge(stats)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Custom pool — best-effort introspection
|
|
138
|
+
info = { name: name, class: pool.class.name, type: 'custom' }
|
|
139
|
+
%i[size available busy idle dead count total active checkout checkin].each do |m|
|
|
140
|
+
if pool.respond_to?(m)
|
|
141
|
+
begin
|
|
142
|
+
val = pool.public_send(m)
|
|
143
|
+
info[m] = val unless val.nil?
|
|
144
|
+
rescue
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
info
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
register_tool('get_pool_details',
|
|
154
|
+
'Connection pool deep dive: size, busy, dead, idle connections. ' \
|
|
155
|
+
'Auto-detects ActiveRecord::Base.connection_pool or uses registered pools') do
|
|
156
|
+
pools = []
|
|
157
|
+
|
|
158
|
+
# Registered pools
|
|
159
|
+
registered_pools.each do |name, pool|
|
|
160
|
+
pools << pool_details_for(name, pool)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Auto-detect ActiveRecord pool if not already registered
|
|
164
|
+
if defined?(::ActiveRecord::Base) && registered_pools.none? { |_, p| p == ::ActiveRecord::Base.connection_pool rescue false }
|
|
165
|
+
begin
|
|
166
|
+
ar = ar_pool_stats
|
|
167
|
+
if ar && !ar[:error]
|
|
168
|
+
pools << ar.merge(name: 'active_record_default', source: 'auto_detected')
|
|
169
|
+
end
|
|
170
|
+
rescue
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if pools.empty?
|
|
176
|
+
next {
|
|
177
|
+
error: 'No connection pools available. Load ActiveRecord or register a pool ' \
|
|
178
|
+
'with DebugAgent.register_pool(:name, pool).'
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
total_pools: pools.size,
|
|
184
|
+
pools: pools
|
|
185
|
+
}
|
|
186
|
+
rescue => e
|
|
187
|
+
{ error: e.message }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
register_tool('detect_pool_leaks',
|
|
191
|
+
'Heuristic connection pool leak detection: flags connections checked out ' \
|
|
192
|
+
'for more than 30 seconds (configurable threshold)') do
|
|
193
|
+
threshold_seconds = 30
|
|
194
|
+
leaks = []
|
|
195
|
+
checked_out = []
|
|
196
|
+
|
|
197
|
+
all_pools = registered_pools.dup
|
|
198
|
+
|
|
199
|
+
# Auto-detect ActiveRecord pool
|
|
200
|
+
if defined?(::ActiveRecord::Base)
|
|
201
|
+
begin
|
|
202
|
+
all_pools['active_record_default'] = ::ActiveRecord::Base.connection_pool
|
|
203
|
+
rescue
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
all_pools.each do |name, pool|
|
|
209
|
+
pool_info = { name: name, connections: [], leak_count: 0 }
|
|
210
|
+
|
|
211
|
+
# ActiveRecord ConnectionPool — access connections via instance variable
|
|
212
|
+
if pool.instance_variable_defined?(:@connections)
|
|
213
|
+
conns = pool.instance_variable_get(:@connections)
|
|
214
|
+
if conns.is_a?(Array)
|
|
215
|
+
conns.each do |conn|
|
|
216
|
+
conn_data = conn_info(conn)
|
|
217
|
+
|
|
218
|
+
# Check if connection has been checked out for too long
|
|
219
|
+
if conn.respond_to?(:in_use?) && conn.in_use?
|
|
220
|
+
idle_secs = (conn.respond_to?(:seconds_idle) ? conn.seconds_idle : nil)
|
|
221
|
+
conn_data[:status] = 'checked_out'
|
|
222
|
+
conn_data[:idle_seconds] = idle_secs
|
|
223
|
+
checked_out << { pool: name }.merge(conn_data)
|
|
224
|
+
|
|
225
|
+
if idle_secs && idle_secs > threshold_seconds
|
|
226
|
+
conn_data[:leak_suspect] = true
|
|
227
|
+
conn_data[:leak_reason] = "Checked out for #{idle_secs}s (threshold: #{threshold_seconds}s)"
|
|
228
|
+
pool_info[:leak_count] += 1
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
pool_info[:connections] << conn_data
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
leaks << pool_info if pool_info[:connections].any?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
total_leaks = leaks.sum { |p| p[:leak_count] }
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
threshold_seconds: threshold_seconds,
|
|
244
|
+
total_leak_suspects: total_leaks,
|
|
245
|
+
pools_inspected: leaks.size,
|
|
246
|
+
checked_out_connections: checked_out.size,
|
|
247
|
+
pools: leaks,
|
|
248
|
+
recommendation: if total_leaks > 0
|
|
249
|
+
"#{total_leaks} potential connection leak(s) detected. " \
|
|
250
|
+
"Check for missing connection.release or unclosed transactions."
|
|
251
|
+
elsif checked_out.any?
|
|
252
|
+
"#{checked_out.size} connection(s) checked out but within normal threshold."
|
|
253
|
+
else
|
|
254
|
+
'No connection leaks detected.'
|
|
255
|
+
end
|
|
256
|
+
}
|
|
257
|
+
rescue => e
|
|
258
|
+
{ error: e.message }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
register_tool('get_pool_wait_stats',
|
|
262
|
+
'Connection pool acquisition wait time statistics: how long threads wait ' \
|
|
263
|
+
'to acquire a database connection from the pool') do
|
|
264
|
+
all_pools = registered_pools.dup
|
|
265
|
+
|
|
266
|
+
# Auto-detect ActiveRecord pool
|
|
267
|
+
if defined?(::ActiveRecord::Base)
|
|
268
|
+
begin
|
|
269
|
+
all_pools['active_record_default'] = ::ActiveRecord::Base.connection_pool
|
|
270
|
+
rescue
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
pool_stats = all_pools.map do |name, pool|
|
|
276
|
+
stats = { name: name, class: pool.class.name }
|
|
277
|
+
|
|
278
|
+
# ActiveRecord connection pool metrics
|
|
279
|
+
if pool.respond_to?(:stats)
|
|
280
|
+
begin
|
|
281
|
+
raw = pool.stats
|
|
282
|
+
stats[:raw_stats] = raw
|
|
283
|
+
stats[:size] = raw[:size] if raw.is_a?(Hash) && raw[:size]
|
|
284
|
+
stats[:busy] = raw[:busy] if raw.is_a?(Hash) && raw[:busy]
|
|
285
|
+
stats[:dead] = raw[:dead] if raw.is_a?(Hash) && raw[:dead]
|
|
286
|
+
stats[:idle] = raw[:idle] if raw.is_a?(Hash) && raw[:idle]
|
|
287
|
+
stats[:waiting] = raw[:waiting] if raw.is_a?(Hash) && raw[:waiting]
|
|
288
|
+
rescue
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Size info
|
|
294
|
+
stats[:pool_size] = pool.size if pool.respond_to?(:size)
|
|
295
|
+
|
|
296
|
+
# Count threads waiting (approximate: scan for threads blocked on connection checkout)
|
|
297
|
+
waiting_threads = Thread.list.select do |t|
|
|
298
|
+
next false unless t.alive? && t.status == 'sleep'
|
|
299
|
+
bt = begin
|
|
300
|
+
t.backtrace || []
|
|
301
|
+
rescue
|
|
302
|
+
[]
|
|
303
|
+
end
|
|
304
|
+
bt.any? { |line| line =~ /connection_pool|with_connection|checkout|acquire/i }
|
|
305
|
+
end
|
|
306
|
+
stats[:threads_waiting_for_connection] = waiting_threads.size
|
|
307
|
+
stats[:waiting_thread_ids] = waiting_threads.map(&:object_id)
|
|
308
|
+
|
|
309
|
+
stats
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
{
|
|
313
|
+
total_pools: pool_stats.size,
|
|
314
|
+
total_waiting_threads: pool_stats.sum { |p| p[:threads_waiting_for_connection] || 0 },
|
|
315
|
+
pools: pool_stats
|
|
316
|
+
}
|
|
317
|
+
rescue => e
|
|
318
|
+
{ error: e.message }
|
|
319
|
+
end
|
|
320
|
+
end
|
data/lib/debug_agent/version.rb
CHANGED
data/lib/debug_agent.rb
CHANGED
|
@@ -35,6 +35,12 @@ require_relative 'debug_agent/inspectors/health'
|
|
|
35
35
|
require_relative 'debug_agent/inspectors/scheduler'
|
|
36
36
|
require_relative 'debug_agent/inspectors/error_tracking'
|
|
37
37
|
require_relative 'debug_agent/inspectors/websocket'
|
|
38
|
+
require_relative 'debug_agent/inspectors/locks'
|
|
39
|
+
require_relative 'debug_agent/inspectors/migration'
|
|
40
|
+
require_relative 'debug_agent/inspectors/config_inspector'
|
|
41
|
+
require_relative 'debug_agent/inspectors/feature_flags'
|
|
42
|
+
require_relative 'debug_agent/inspectors/endpoint_test'
|
|
43
|
+
require_relative 'debug_agent/inspectors/pool_inspector'
|
|
38
44
|
|
|
39
45
|
module DebugAgent
|
|
40
46
|
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ggcode
|
|
@@ -167,16 +167,22 @@ files:
|
|
|
167
167
|
- lib/debug_agent/inspectors/active_record_stats.rb
|
|
168
168
|
- lib/debug_agent/inspectors/cache.rb
|
|
169
169
|
- lib/debug_agent/inspectors/concurrent.rb
|
|
170
|
+
- lib/debug_agent/inspectors/config_inspector.rb
|
|
170
171
|
- lib/debug_agent/inspectors/core_ext.rb
|
|
172
|
+
- lib/debug_agent/inspectors/endpoint_test.rb
|
|
171
173
|
- lib/debug_agent/inspectors/error_tracking.rb
|
|
172
174
|
- lib/debug_agent/inspectors/faraday.rb
|
|
175
|
+
- lib/debug_agent/inspectors/feature_flags.rb
|
|
173
176
|
- lib/debug_agent/inspectors/gc.rb
|
|
174
177
|
- lib/debug_agent/inspectors/health.rb
|
|
175
178
|
- lib/debug_agent/inspectors/http_client.rb
|
|
176
179
|
- lib/debug_agent/inspectors/http_tracker.rb
|
|
180
|
+
- lib/debug_agent/inspectors/locks.rb
|
|
177
181
|
- lib/debug_agent/inspectors/logging.rb
|
|
178
182
|
- lib/debug_agent/inspectors/metrics.rb
|
|
183
|
+
- lib/debug_agent/inspectors/migration.rb
|
|
179
184
|
- lib/debug_agent/inspectors/object_space.rb
|
|
185
|
+
- lib/debug_agent/inspectors/pool_inspector.rb
|
|
180
186
|
- lib/debug_agent/inspectors/process_info.rb
|
|
181
187
|
- lib/debug_agent/inspectors/puma.rb
|
|
182
188
|
- lib/debug_agent/inspectors/rails.rb
|