Module: DebugAgent
- Extended by:
- ClassMethods
- Defined in:
- lib/debug_agent.rb,
lib/debug_agent/config.rb,
lib/debug_agent/engine.rb,
lib/debug_agent/version.rb,
lib/debug_agent/chat_page.rb,
lib/debug_agent/llm_client.rb,
lib/debug_agent/middleware.rb,
lib/debug_agent/chat_session.rb,
lib/debug_agent/inspectors/gc.rb,
lib/debug_agent/tool_registry.rb,
lib/debug_agent/inspectors/puma.rb,
lib/debug_agent/inspectors/cache.rb,
lib/debug_agent/inspectors/rails.rb,
lib/debug_agent/inspectors/redis.rb,
lib/debug_agent/inspectors/routes.rb,
lib/debug_agent/inspectors/system.rb,
lib/debug_agent/context_compressor.rb,
lib/debug_agent/inspectors/faraday.rb,
lib/debug_agent/inspectors/logging.rb,
lib/debug_agent/inspectors/metrics.rb,
lib/debug_agent/inspectors/runtime.rb,
lib/debug_agent/inspectors/sidekiq.rb,
lib/debug_agent/inspectors/threads.rb,
lib/debug_agent/inspectors/core_ext.rb,
lib/debug_agent/inspectors/concurrent.rb,
lib/debug_agent/system_prompt_builder.rb,
lib/debug_agent/inspectors/http_client.rb,
lib/debug_agent/inspectors/http_tracker.rb,
lib/debug_agent/inspectors/object_space.rb,
lib/debug_agent/inspectors/process_info.rb,
lib/debug_agent/inspectors/active_record_stats.rb
Defined Under Namespace
Modules: ChatPage, ClassMethods, HttpRequestTracker, Middleware, MiddlewareCore, OutboundHttpTracker, ToolDefinitionExt Classes: ChatCallback, ChatSession, CompressionResult, Config, ContextCompressor, DebugEngine, EngineStreamHandler, Error, LLMClient, LLMConfig, RackMiddleware, RetriableError, SseCallback, StreamHandler, SystemPromptBuilder, ToolDefinition, ToolParam, ToolRegistry
Constant Summary collapse
- PROCESS_START_TIME =
Process start time for uptime tracking
Time.now
- VERSION =
'0.4.0'.freeze
- REGISTRY =
Global registry singleton
ToolRegistry.new
- MAX_LOGS =
Ring buffer of recent log entries and a registry of named loggers.
DebugAgent.register_logger(:app, Rails.logger) 100- LEVEL_MAP =
{ 'debug' => defined?(::Logger) ? ::Logger::DEBUG : 0, 'info' => defined?(::Logger) ? ::Logger::INFO : 1, 'warn' => defined?(::Logger) ? ::Logger::WARN : 2, 'error' => defined?(::Logger) ? ::Logger::ERROR : 3, 'fatal' => defined?(::Logger) ? ::Logger::FATAL : 4 }.freeze
- CATEGORY_MAP =
{ 'gc' => 'Memory & GC', 'object_space' => 'Memory & GC', 'memory' => 'Memory & GC', 'object_count' => 'Memory & GC', 'allocations' => 'Memory & GC', 'force_gc' => 'Memory & GC', 'trigger_gc' => 'Memory & GC', 'process' => 'Process Info', 'cpu' => 'Process Info', 'uptime' => 'Process Info', 'thread' => 'Threads', 'system' => 'System Info', 'disk' => 'System Info', 'environment' => 'System Info', 'routes' => 'Framework & Routes', 'middleware' => 'Framework & Routes', 'runtime' => 'Runtime Info', 'recent' => 'HTTP Requests', 'slow' => 'HTTP Requests', 'error' => 'HTTP Requests', 'request' => 'HTTP Requests', 'gem' => 'Dependencies', 'module' => 'Module Info', }.freeze
- MAX_REQUESTS =
500
Class Attribute Summary collapse
-
.app ⇒ Object
Returns the value of attribute app.
-
.ar_stats ⇒ Object
readonly
Returns the value of attribute ar_stats.
-
.caches ⇒ Object
readonly
Returns the value of attribute caches.
-
.concurrent_promises ⇒ Object
readonly
Returns the value of attribute concurrent_promises.
-
.faraday_connections ⇒ Object
readonly
Returns the value of attribute faraday_connections.
-
.loggers ⇒ Object
readonly
Returns the value of attribute loggers.
-
.outbound_stats ⇒ Object
readonly
Returns the value of attribute outbound_stats.
-
.redis_clients ⇒ Object
readonly
Returns the value of attribute redis_clients.
-
.sidekiq_queues ⇒ Object
readonly
Returns the value of attribute sidekiq_queues.
Class Method Summary collapse
-
.ar_fingerprint(sql) ⇒ Object
Normalize SQL into a fingerprint for grouping / N+1 detection.
-
.best_effort_cache_stats(cache) ⇒ Object
Extract hit/miss and size where the cache exposes them (e.g. Dalli).
- .cache_keys_for(cache) ⇒ Object
- .cache_size_for(cache) ⇒ Object
-
.cache_stats_for(cache) ⇒ Object
Introspect a cache object, supporting ActiveSupport::Cache::MemoryStore, generic ActiveSupport::Cache::Store, plain Hashes, and custom caches.
-
.capture_log(severity, args) ⇒ Object
Invoked by the wrapped Logger#add to push an entry into the ring buffer.
- .executor_info(executor) ⇒ Object
- .faraday_conn_info(name, conn) ⇒ Object
- .faraday_handler_name(handler) ⇒ Object
-
.format_uptime(seconds) ⇒ Object
Helper method for formatting uptime.
-
.install_ar_tracker ⇒ Object
Subscribe to sql.active_record notifications (idempotent).
-
.install_log_capture ⇒ Object
Wrap the standard Logger#add / << so all log output flows into the ring buffer.
-
.install_outbound_tracker ⇒ Object
Wrap Net::HTTP once to capture outbound request metrics.
-
.prometheus_metric_value(metric) ⇒ Object
Safely read a metric’s value(s).
-
.prometheus_registry ⇒ Object
Resolve the Prometheus registry to inspect.
- .promise_info(name, promise) ⇒ Object
- .record_ar_query(started, finished, payload) ⇒ Object
- .record_outbound(http, req, latency_ms, error) ⇒ Object
- .register_cache(name, cache) ⇒ Object
- .register_concurrent(name, promise) ⇒ Object
- .register_faraday(name, conn) ⇒ Object
- .register_logger(name, logger) ⇒ Object
- .register_redis_client(name, client) ⇒ Object
- .register_sidekiq_queue(name, queue) ⇒ Object
-
.severity_label(severity) ⇒ Object
Map a Logger severity integer to a human-readable label.
- .track_http_connect(http) ⇒ Object
- .track_http_disconnect(http) ⇒ Object
-
.with_redis(name = nil) ⇒ Object
Resolve a registered Redis client.
Methods included from ClassMethods
Class Attribute Details
.app ⇒ Object
Returns the value of attribute app.
44 45 46 |
# File 'lib/debug_agent.rb', line 44 def app @app end |
.ar_stats ⇒ Object (readonly)
Returns the value of attribute ar_stats.
18 19 20 |
# File 'lib/debug_agent/inspectors/active_record_stats.rb', line 18 def ar_stats @ar_stats end |
.caches ⇒ Object (readonly)
Returns the value of attribute caches.
9 10 11 |
# File 'lib/debug_agent/inspectors/cache.rb', line 9 def caches @caches end |
.concurrent_promises ⇒ Object (readonly)
Returns the value of attribute concurrent_promises.
9 10 11 |
# File 'lib/debug_agent/inspectors/concurrent.rb', line 9 def concurrent_promises @concurrent_promises end |
.faraday_connections ⇒ Object (readonly)
Returns the value of attribute faraday_connections.
8 9 10 |
# File 'lib/debug_agent/inspectors/faraday.rb', line 8 def faraday_connections @faraday_connections end |
.loggers ⇒ Object (readonly)
Returns the value of attribute loggers.
16 17 18 |
# File 'lib/debug_agent/inspectors/logging.rb', line 16 def loggers @loggers end |
.outbound_stats ⇒ Object (readonly)
Returns the value of attribute outbound_stats.
12 13 14 |
# File 'lib/debug_agent/inspectors/http_client.rb', line 12 def outbound_stats @outbound_stats end |
.redis_clients ⇒ Object (readonly)
Returns the value of attribute redis_clients.
9 10 11 |
# File 'lib/debug_agent/inspectors/redis.rb', line 9 def redis_clients @redis_clients end |
.sidekiq_queues ⇒ Object (readonly)
Returns the value of attribute sidekiq_queues.
9 10 11 |
# File 'lib/debug_agent/inspectors/sidekiq.rb', line 9 def sidekiq_queues @sidekiq_queues end |
Class Method Details
.ar_fingerprint(sql) ⇒ Object
Normalize SQL into a fingerprint for grouping / N+1 detection.
61 62 63 64 65 66 67 |
# File 'lib/debug_agent/inspectors/active_record_stats.rb', line 61 def ar_fingerprint(sql) sql .gsub(/'[^']*'/, '?') .gsub(/\b\d+\b/, '?') .gsub(/\s+/, ' ') .strip[0..200] end |
.best_effort_cache_stats(cache) ⇒ Object
Extract hit/miss and size where the cache exposes them (e.g. Dalli).
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/debug_agent/inspectors/cache.rb', line 45 def best_effort_cache_stats(cache) result = {} result[:size] = cache_size_for(cache) if cache.respond_to?(:stats) raw = begin cache.stats rescue {} end if raw.is_a?(Hash) result[:raw_stats] = raw hits = raw['get_hits'] || raw[:get_hits] misses = raw['get_misses'] || raw[:get_misses] if hits && misses total = hits.to_i + misses.to_i result[:hits] = hits.to_i result[:misses] = misses.to_i result[:hit_rate] = total.zero? ? nil : format('%.1f%%', hits.to_f / total * 100) end end end result end |
.cache_keys_for(cache) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/debug_agent/inspectors/cache.rb', line 71 def cache_keys_for(cache) if defined?(::ActiveSupport::Cache::MemoryStore) && cache.is_a?(::ActiveSupport::Cache::MemoryStore) (cache.instance_variable_get(:@data) || {}).keys elsif cache.is_a?(Hash) cache.keys elsif cache.respond_to?(:keys) begin cache.keys rescue [] end else [] end end |
.cache_size_for(cache) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/debug_agent/inspectors/cache.rb', line 87 def cache_size_for(cache) if defined?(::ActiveSupport::Cache::MemoryStore) && cache.is_a?(::ActiveSupport::Cache::MemoryStore) (cache.instance_variable_get(:@data) || {}).size elsif cache.respond_to?(:size) begin cache.size rescue nil end elsif cache.respond_to?(:length) begin cache.length rescue nil end end end |
.cache_stats_for(cache) ⇒ Object
Introspect a cache object, supporting ActiveSupport::Cache::MemoryStore, generic ActiveSupport::Cache::Store, plain Hashes, and custom caches.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/debug_agent/inspectors/cache.rb', line 17 def cache_stats_for(cache) if defined?(::ActiveSupport::Cache::MemoryStore) && cache.is_a?(::ActiveSupport::Cache::MemoryStore) data = cache.instance_variable_get(:@data) || {} key_access = cache.instance_variable_get(:@key_access) || {} { type: 'ActiveSupport::Cache::MemoryStore', size: data.size, max_size: cache.instance_variable_get(:@max_size), tracked_keys: key_access.size, sample_keys: data.keys.first(50) } elsif defined?(::ActiveSupport::Cache::Store) && defined?(::ActiveSupport::Cache) && cache.is_a?(::ActiveSupport::Cache::Store) stats = best_effort_cache_stats(cache) stats.merge(type: cache.class.name) elsif cache.is_a?(Hash) { type: 'Hash', size: cache.size, sample_keys: cache.keys.first(50) } else stats = best_effort_cache_stats(cache) stats.merge(type: cache.class.name) end end |
.capture_log(severity, args) ⇒ Object
Invoked by the wrapped Logger#add to push an entry into the ring buffer.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/debug_agent/inspectors/logging.rb', line 23 def capture_log(severity, args) args = args.is_a?(Array) ? args : [args] # Logger passes (message, progname); pick the meaningful value. msg = args.compact.first entry = { timestamp: Time.now.iso8601, severity: severity_label(severity), message: msg.respond_to?(:to_str) ? msg.to_s : msg.inspect } @log_buffer_lock.synchronize do @log_buffer << entry @log_buffer.shift if @log_buffer.size > MAX_LOGS end end |
.executor_info(executor) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/debug_agent/inspectors/concurrent.rb', line 15 def executor_info(executor) return nil unless executor info = { class: executor.class.name } %i[running?].each do |m| info[m] = executor.public_send(m) if executor.respond_to?(m) end %i[length largest_length queue_length scheduled_task_count completed_task_count max_threads min_threads idletime max_queue].each do |m| info[m] = (executor.public_send(m) rescue nil) if executor.respond_to?(m) end info rescue => e { class: executor&.class&.name, error: e. } end |
.faraday_conn_info(name, conn) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/debug_agent/inspectors/faraday.rb', line 14 def faraday_conn_info(name, conn) info = { name: name, class: conn.class.name } if conn.respond_to?(:url_prefix) info[:url] = conn.url_prefix.to_s info[:host] = conn.url_prefix.host info[:port] = conn.url_prefix.port info[:scheme] = conn.url_prefix.scheme end builder = conn.respond_to?(:builder) ? conn.builder : nil if builder handlers = if builder.respond_to?(:handlers) builder.handlers.map { |h| faraday_handler_name(h) } else [] end info[:middleware] = handlers adapter = if builder.respond_to?(:adapter) faraday_handler_name(builder.adapter) end info[:adapter] = adapter if adapter end info[:headers] = conn.headers.to_h if conn.respond_to?(:headers) && conn.headers.respond_to?(:to_h) info rescue => e { name: name, error: e. } end |
.faraday_handler_name(handler) ⇒ Object
48 49 50 51 52 |
# File 'lib/debug_agent/inspectors/faraday.rb', line 48 def faraday_handler_name(handler) return handler.name if handler.respond_to?(:name) return handler.class.name if handler.respond_to?(:class) handler.to_s end |
.format_uptime(seconds) ⇒ Object
Helper method for formatting uptime
48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/debug_agent/inspectors/process_info.rb', line 48 def self.format_uptime(seconds) days = (seconds / 86400).to_i hours = ((seconds % 86400) / 3600).to_i minutes = ((seconds % 3600) / 60).to_i secs = (seconds % 60).to_i parts = [] parts << "#{days}d" if days > 0 parts << "#{hours}h" if hours > 0 || days > 0 parts << "#{minutes}m" if minutes > 0 || hours > 0 || days > 0 parts << "#{secs}s" parts.join(' ') end |
.install_ar_tracker ⇒ Object
Subscribe to sql.active_record notifications (idempotent).
21 22 23 24 25 26 27 28 29 30 |
# File 'lib/debug_agent/inspectors/active_record_stats.rb', line 21 def install_ar_tracker return true if @ar_tracker_installed return false unless defined?(::ActiveSupport::Notifications) ::ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, started, finished, _id, payload| DebugAgent.record_ar_query(started, finished, payload) end @ar_tracker_installed = true true end |
.install_log_capture ⇒ Object
Wrap the standard Logger#add / << so all log output flows into the ring buffer. Only wraps once — guarded by checking for the aliased method.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/debug_agent/inspectors/logging.rb', line 40 def install_log_capture return false unless defined?(::Logger) return true if ::Logger.method_defined?(:_original_add) ::Logger.class_eval do alias_method :_original_add, :add alias_method :_original_lshift, :<< def add(severity, *args, &block) if block msg = args[0] msg = block.call if msg.nil? DebugAgent.capture_log(severity, [msg]) rescue nil _original_add(severity, msg, *args[1..-1]) else DebugAgent.capture_log(severity, args) rescue nil _original_add(severity, *args) end end def <<(msg) DebugAgent.capture_log(nil, [msg]) rescue nil _original_lshift(msg) end end true end |
.install_outbound_tracker ⇒ Object
Wrap Net::HTTP once to capture outbound request metrics.
53 54 55 56 57 58 59 |
# File 'lib/debug_agent/inspectors/http_client.rb', line 53 def install_outbound_tracker return false unless defined?(::Net::HTTP) return true if ::Net::HTTP.include?(OutboundHttpTracker) ::Net::HTTP.prepend(OutboundHttpTracker) true end |
.prometheus_metric_value(metric) ⇒ Object
Safely read a metric’s value(s). Different metric types return different shapes from #get.
14 15 16 17 18 19 20 21 22 23 24 25 26 |
# File 'lib/debug_agent/inspectors/metrics.rb', line 14 def prometheus_metric_value(metric) begin value = metric.get({}) # Counter/Gauge return a Hash of {labels => value}; unwrap the unlabeled value. if value.is_a?(Hash) && value.size == 1 && value.key?({}) value[{}] else value end rescue => e { error: e. } end end |
.prometheus_registry ⇒ Object
Resolve the Prometheus registry to inspect.
7 8 9 10 |
# File 'lib/debug_agent/inspectors/metrics.rb', line 7 def prometheus_registry return nil unless defined?(::Prometheus) && defined?(::Prometheus::Client) ::Prometheus::Client.respond_to?(:registry) ? ::Prometheus::Client.registry : nil end |
.promise_info(name, promise) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/debug_agent/inspectors/concurrent.rb', line 30 def promise_info(name, promise) info = { name: name, class: promise.class.name } info[:state] = promise.state if promise.respond_to?(:state) if promise.respond_to?(:fulfilled?) info[:fulfilled] = promise.fulfilled? info[:rejected] = promise.rejected? if promise.respond_to?(:rejected?) info[:pending] = promise.pending? if promise.respond_to?(:pending?) end if promise.respond_to?(:reason) reason = promise.reason info[:reason] = reason.is_a?(Exception) ? reason. : reason.inspect unless reason.nil? end info rescue => e { name: name, error: e. } end |
.record_ar_query(started, finished, payload) ⇒ Object
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/debug_agent/inspectors/active_record_stats.rb', line 32 def record_ar_query(started, finished, payload) duration_ms = ((finished - started) * 1000.0) sql = payload[:sql].to_s.strip return if sql.empty? fp = ar_fingerprint(sql) name = payload[:name] @ar_lock.synchronize do s = @ar_stats s[:total_queries] += 1 s[:total_time_ms] += duration_ms next unless fp s[:query_counts][fp] += 1 s[:query_times][fp] += duration_ms if duration_ms >= s[:slow_threshold_ms] && s[:slow_queries].size < 200 s[:slow_queries] << { sql: sql[0..500], name: name, duration_ms: duration_ms.round(2), timestamp: finished.to_s } end end rescue nil end |
.record_outbound(http, req, latency_ms, error) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/debug_agent/inspectors/http_client.rb', line 14 def record_outbound(http, req, latency_ms, error) @outbound_lock.synchronize do s = @outbound_stats s[:total] += 1 s[:latencies] << latency_ms s[:latencies].shift if s[:latencies].size > 1000 host_key = "#{http.address}:#{http.port}" h = (s[:hosts][host_key] ||= { count: 0, latencies: [], errors: 0 }) h[:count] += 1 h[:latencies] << latency_ms h[:latencies].shift if h[:latencies].size > 200 if error s[:errors] += 1 h[:errors] += 1 end end end |
.register_cache(name, cache) ⇒ Object
11 12 13 |
# File 'lib/debug_agent/inspectors/cache.rb', line 11 def register_cache(name, cache) @caches[name.to_s] = cache end |
.register_concurrent(name, promise) ⇒ Object
11 12 13 |
# File 'lib/debug_agent/inspectors/concurrent.rb', line 11 def register_concurrent(name, promise) @concurrent_promises[name.to_s] = promise end |
.register_faraday(name, conn) ⇒ Object
10 11 12 |
# File 'lib/debug_agent/inspectors/faraday.rb', line 10 def register_faraday(name, conn) @faraday_connections[name.to_s] = conn end |
.register_logger(name, logger) ⇒ Object
18 19 20 |
# File 'lib/debug_agent/inspectors/logging.rb', line 18 def register_logger(name, logger) @loggers[name.to_s] = logger end |
.register_redis_client(name, client) ⇒ Object
11 12 13 |
# File 'lib/debug_agent/inspectors/redis.rb', line 11 def register_redis_client(name, client) @redis_clients[name.to_s] = client end |
.register_sidekiq_queue(name, queue) ⇒ Object
11 12 13 |
# File 'lib/debug_agent/inspectors/sidekiq.rb', line 11 def register_sidekiq_queue(name, queue) @sidekiq_queues[name.to_s] = queue end |
.severity_label(severity) ⇒ Object
Map a Logger severity integer to a human-readable label.
69 70 71 72 73 |
# File 'lib/debug_agent/inspectors/logging.rb', line 69 def severity_label(severity) labels = %w[DEBUG INFO WARN ERROR FATAL ANY] idx = severity.is_a?(Integer) ? severity : (defined?(::Logger) ? ::Logger::UNKNOWN : 5) labels[idx] || 'UNKNOWN' end |
.track_http_connect(http) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/debug_agent/inspectors/http_client.rb', line 33 def track_http_connect(http) @outbound_lock.synchronize do @http_connections[http.object_id] = { host: http.address, port: http.port, use_ssl: http.use_ssl?, started_at: Time.now.iso8601, active: true } end end |
.track_http_disconnect(http) ⇒ Object
45 46 47 48 49 50 |
# File 'lib/debug_agent/inspectors/http_client.rb', line 45 def track_http_disconnect(http) @outbound_lock.synchronize do conn = @http_connections[http.object_id] conn[:active] = false if conn end end |
.with_redis(name = nil) ⇒ Object
Resolve a registered Redis client. Accepts a bare Redis object or a ConnectionPool (redis-rb ships ConnectionPool support). We yield a usable connection object to the block.
19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/debug_agent/inspectors/redis.rb', line 19 def self.with_redis(name = nil) name, client = if name [name.to_s, redis_clients[name.to_s]] else redis_clients.first end return [nil, nil] unless client [name, client] end |