dead_bro 0.2.2 → 0.2.3

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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ class Monitor
5
+ def initialize(client: DeadBro.client)
6
+ @client = client
7
+ @thread = nil
8
+ @running = false
9
+ end
10
+
11
+ def start
12
+ return if @running
13
+ return unless DeadBro.configuration.job_queue_monitoring_enabled
14
+ return unless DeadBro.configuration.enabled
15
+
16
+ @running = true
17
+ @thread = Thread.new do
18
+ Thread.current.abort_on_exception = false
19
+ loop do
20
+ break unless @running
21
+
22
+ begin
23
+ collect_and_send_stats
24
+ rescue => e
25
+ log_error("Error collecting stats: #{e.message}")
26
+ end
27
+
28
+ # Sleep for 60 seconds (1 minute)
29
+ sleep(60)
30
+ end
31
+ end
32
+
33
+ @thread
34
+ end
35
+
36
+ def stop
37
+ @running = false
38
+ @thread&.join(5) # Wait up to 5 seconds for thread to finish
39
+ @thread = nil
40
+ end
41
+
42
+ private
43
+
44
+ def collect_and_send_stats
45
+ payload = {
46
+ environment: DeadBro.env,
47
+ host: process_hostname,
48
+ pid: Process.pid,
49
+ current_time: Time.now.utc.iso8601,
50
+ jobs: DeadBro::Collectors::Jobs.collect,
51
+ network: DeadBro::Collectors::Network.collect
52
+ }
53
+
54
+ if DeadBro.configuration.respond_to?(:enable_db_stats) && DeadBro.configuration.enable_db_stats
55
+ payload[:db] = safe_collect { DeadBro::Collectors::Database.collect }
56
+ end
57
+
58
+ if DeadBro.configuration.respond_to?(:enable_process_stats) && DeadBro.configuration.enable_process_stats
59
+ payload[:process] = safe_collect { DeadBro::Collectors::ProcessInfo.collect }
60
+ end
61
+
62
+ if DeadBro.configuration.respond_to?(:enable_system_stats) && DeadBro.configuration.enable_system_stats
63
+ payload[:system] = safe_collect { DeadBro::Collectors::System.collect }
64
+ end
65
+ @client.post_monitor_stats(payload)
66
+ end
67
+
68
+ def process_hostname
69
+ require "socket"
70
+ Socket.gethostname
71
+ rescue
72
+ "unknown"
73
+ end
74
+
75
+ def log_error(message)
76
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
77
+ Rails.logger.error("[DeadBro::Monitor] #{message}")
78
+ else
79
+ $stderr.puts("[DeadBro::Monitor] #{message}")
80
+ end
81
+ end
82
+
83
+ def safe_collect
84
+ yield
85
+ rescue => e
86
+ {error_class: e.class.name, error_message: e.message.to_s[0, 500]}
87
+ end
88
+ end
89
+ end
@@ -10,12 +10,11 @@ end
10
10
  if defined?(Rails) && defined?(Rails::Railtie)
11
11
  module DeadBro
12
12
  class Railtie < ::Rails::Railtie
13
-
14
13
  initializer "dead_bro.subscribe" do |app|
15
14
  app.config.after_initialize do
16
15
  # Use the shared Client instance for all subscribers
17
16
  shared_client = DeadBro.client
18
-
17
+
19
18
  DeadBro::Subscriber.subscribe!(client: shared_client)
20
19
  # Install outgoing HTTP instrumentation
21
20
  require "dead_bro/http_instrumentation"
@@ -58,9 +57,9 @@ if defined?(Rails) && defined?(Rails::Railtie)
58
57
 
59
58
  # Start job queue monitoring if enabled
60
59
  if DeadBro.configuration.job_queue_monitoring_enabled
61
- require "dead_bro/job_queue_monitor"
62
- DeadBro.job_queue_monitor = DeadBro::JobQueueMonitor.new(client: shared_client)
63
- DeadBro.job_queue_monitor.start
60
+ require "dead_bro/monitor"
61
+ DeadBro.monitor = DeadBro::Monitor.new(client: shared_client)
62
+ DeadBro.monitor.start
64
63
  end
65
64
  rescue
66
65
  # Never raise in Railtie init
@@ -70,7 +69,7 @@ if defined?(Rails) && defined?(Rails::Railtie)
70
69
  # Insert Rack middleware early enough to observe uncaught exceptions
71
70
  initializer "dead_bro.middleware" do |app|
72
71
  require "dead_bro/error_middleware"
73
-
72
+
74
73
  # Use the shared Client instance for the middleware
75
74
  shared_client = DeadBro.client
76
75
 
@@ -225,17 +225,17 @@ module DeadBro
225
225
  def self.should_continue_tracking?
226
226
  events = Thread.current[THREAD_LOCAL_KEY]
227
227
  return false unless events
228
-
228
+
229
229
  # Check count limit
230
230
  return false if events.length >= MAX_TRACKED_EVENTS
231
-
231
+
232
232
  # Check time limit
233
233
  start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
234
234
  if start_time
235
235
  elapsed_seconds = Time.now - start_time
236
236
  return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
237
237
  end
238
-
238
+
239
239
  true
240
240
  end
241
241
 
@@ -20,17 +20,17 @@ module DeadBro
20
20
  def self.should_continue_tracking?(thread_local_key, max_count)
21
21
  events = Thread.current[thread_local_key]
22
22
  return false unless events
23
-
23
+
24
24
  # Check count limit
25
25
  return false if events.length >= max_count
26
-
26
+
27
27
  # Check time limit
28
28
  start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
29
29
  if start_time
30
30
  elapsed_seconds = Time.now - start_time
31
31
  return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
32
32
  end
33
-
33
+
34
34
  true
35
35
  end
36
36
 
@@ -102,10 +102,10 @@ module DeadBro
102
102
  # This must happen BEFORE we get the queries array reference to ensure
103
103
  # all explain_plan fields are populated
104
104
  wait_for_pending_explains(5.0) # 5 second timeout
105
-
105
+
106
106
  # Get queries after waiting for EXPLAIN to complete
107
107
  queries = Thread.current[THREAD_LOCAL_KEY]
108
-
108
+
109
109
  Thread.current[THREAD_LOCAL_KEY] = nil
110
110
  Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
111
111
  Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
@@ -117,12 +117,12 @@ module DeadBro
117
117
  def self.wait_for_pending_explains(timeout_seconds)
118
118
  pending = Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY]
119
119
  return unless pending && !pending.empty?
120
-
120
+
121
121
  start_time = Time.now
122
122
  pending.each do |thread|
123
123
  remaining_time = timeout_seconds - (Time.now - start_time)
124
124
  break if remaining_time <= 0
125
-
125
+
126
126
  begin
127
127
  thread.join(remaining_time)
128
128
  rescue => e
@@ -152,7 +152,7 @@ module DeadBro
152
152
  return false if duration_ms < DeadBro.configuration.slow_query_threshold_ms
153
153
  return false unless sql.is_a?(String)
154
154
  return false if sql.strip.empty?
155
-
155
+
156
156
  # Skip EXPLAIN for certain query types that don't benefit from it
157
157
  sql_upper = sql.upcase.strip
158
158
  return false if sql_upper.start_with?("EXPLAIN")
@@ -161,55 +161,53 @@ module DeadBro
161
161
  return false if sql_upper.start_with?("ROLLBACK")
162
162
  return false if sql_upper.start_with?("SAVEPOINT")
163
163
  return false if sql_upper.start_with?("RELEASE")
164
-
164
+
165
165
  true
166
166
  end
167
167
 
168
168
  def self.start_explain_analyze_background(sql, connection_id, query_info, binds = nil)
169
169
  return unless defined?(ActiveRecord)
170
170
  return unless ActiveRecord::Base.respond_to?(:connection)
171
-
171
+
172
172
  # Capture the main thread reference to append logs to the correct thread
173
173
  main_thread = Thread.current
174
-
174
+
175
175
  # Run EXPLAIN in a background thread to avoid blocking the main request
176
176
  explain_thread = Thread.new do
177
177
  connection = nil
178
178
  begin
179
179
  # Use a separate connection to avoid interfering with the main query
180
- if ActiveRecord::Base.connection_pool.respond_to?(:checkout)
181
- connection = ActiveRecord::Base.connection_pool.checkout
180
+ connection = if ActiveRecord::Base.connection_pool.respond_to?(:checkout)
181
+ ActiveRecord::Base.connection_pool.checkout
182
182
  else
183
- connection = ActiveRecord::Base.connection
183
+ ActiveRecord::Base.connection
184
184
  end
185
-
185
+
186
186
  # Interpolate binds if present to ensure EXPLAIN works with placeholders
187
187
  final_sql = interpolate_sql_with_binds(sql, binds, connection)
188
-
188
+
189
189
  # Build EXPLAIN query based on database adapter
190
190
  explain_sql = build_explain_query(final_sql, connection)
191
-
191
+
192
192
  # Execute the EXPLAIN query
193
193
  # For PostgreSQL, use select_all which returns ActiveRecord::Result
194
194
  # For other databases, use execute
195
195
  adapter_name = connection.adapter_name.downcase
196
- if adapter_name == "postgresql" || adapter_name == "postgis"
196
+ result = if adapter_name == "postgresql" || adapter_name == "postgis"
197
197
  # PostgreSQL: select_all returns ActiveRecord::Result with rows
198
- result = connection.select_all(explain_sql)
198
+ connection.select_all(explain_sql)
199
199
  else
200
200
  # Other databases: use execute
201
- result = connection.execute(explain_sql)
201
+ connection.execute(explain_sql)
202
202
  end
203
-
203
+
204
204
  # Format the result based on database adapter
205
205
  explain_plan = format_explain_result(result, connection)
206
-
206
+
207
207
  # Update the query_info with the explain plan
208
208
  # This updates the hash that's already in the queries array
209
- if explain_plan && !explain_plan.to_s.strip.empty?
210
- query_info[:explain_plan] = explain_plan
211
- else
212
- query_info[:explain_plan] = nil
209
+ query_info[:explain_plan] = if explain_plan && !explain_plan.to_s.strip.empty?
210
+ explain_plan
213
211
  end
214
212
  rescue => e
215
213
  # Silently fail - don't let EXPLAIN break the application
@@ -218,11 +216,15 @@ module DeadBro
218
216
  ensure
219
217
  # Return connection to pool if we checked it out
220
218
  if connection && ActiveRecord::Base.connection_pool.respond_to?(:checkin)
221
- ActiveRecord::Base.connection_pool.checkin(connection) rescue nil
219
+ begin
220
+ ActiveRecord::Base.connection_pool.checkin(connection)
221
+ rescue
222
+ nil
223
+ end
222
224
  end
223
225
  end
224
226
  end
225
-
227
+
226
228
  # Track the thread so we can wait for it when stopping request tracking
227
229
  pending = Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] ||= []
228
230
  pending << explain_thread
@@ -274,7 +276,7 @@ module DeadBro
274
276
 
275
277
  def self.build_explain_query(sql, connection)
276
278
  adapter_name = connection.adapter_name.downcase
277
-
279
+
278
280
  case adapter_name
279
281
  when "postgresql", "postgis"
280
282
  # PostgreSQL supports ANALYZE and BUFFERS
@@ -296,7 +298,7 @@ module DeadBro
296
298
  return sql unless connection
297
299
 
298
300
  interpolated_sql = sql.dup
299
-
301
+
300
302
  # Handle $1, $2 style placeholders (PostgreSQL)
301
303
  if interpolated_sql.include?("$1")
302
304
  binds.each_with_index do |val, index|
@@ -307,7 +309,7 @@ module DeadBro
307
309
  elsif val.respond_to?(:value)
308
310
  value = val.value
309
311
  end
310
-
312
+
311
313
  quoted_value = connection.quote(value)
312
314
  interpolated_sql = interpolated_sql.gsub("$#{index + 1}", quoted_value)
313
315
  end
@@ -322,18 +324,18 @@ module DeadBro
322
324
  elsif val.respond_to?(:value)
323
325
  value = val.value
324
326
  end
325
-
327
+
326
328
  quoted_value = connection.quote(value)
327
329
  interpolated_sql = interpolated_sql.sub("?", quoted_value)
328
330
  end
329
331
  end
330
-
332
+
331
333
  interpolated_sql
332
334
  end
333
335
 
334
336
  def self.format_explain_result(result, connection)
335
337
  adapter_name = connection.adapter_name.downcase
336
-
338
+
337
339
  case adapter_name
338
340
  when "postgresql", "postgis"
339
341
  # PostgreSQL returns ActiveRecord::Result from select_all
@@ -343,12 +345,12 @@ module DeadBro
343
345
  plan_text = result.rows.map { |row| row.is_a?(Array) ? row.first.to_s : row.to_s }.join("\n")
344
346
  return plan_text unless plan_text.strip.empty?
345
347
  end
346
-
348
+
347
349
  # Try alternative methods to extract the plan
348
350
  if result.respond_to?(:each) && result.respond_to?(:columns)
349
351
  # ActiveRecord::Result with columns
350
352
  plan_column = result.columns.find { |col| col.downcase.include?("plan") || col.downcase.include?("query") } || result.columns.first
351
- plan_text = result.map { |row|
353
+ plan_text = result.map { |row|
352
354
  if row.is_a?(Hash)
353
355
  row[plan_column] || row[plan_column.to_sym] || row.values.first
354
356
  else
@@ -357,7 +359,7 @@ module DeadBro
357
359
  }.join("\n")
358
360
  return plan_text unless plan_text.strip.empty?
359
361
  end
360
-
362
+
361
363
  if result.is_a?(Array)
362
364
  # Array of hashes or arrays
363
365
  plan_text = result.map do |row|
@@ -371,7 +373,7 @@ module DeadBro
371
373
  end.join("\n")
372
374
  return plan_text unless plan_text.strip.empty?
373
375
  end
374
-
376
+
375
377
  # Fallback to string representation
376
378
  result.to_s
377
379
  when "mysql", "mysql2", "trilogy"
@@ -396,7 +398,7 @@ module DeadBro
396
398
  # Generic fallback
397
399
  result.to_s
398
400
  end
399
- rescue => e
401
+ rescue
400
402
  # Fallback to string representation
401
403
  result.to_s
402
404
  end
@@ -42,7 +42,7 @@ module DeadBro
42
42
 
43
43
  # Start outgoing HTTP accumulation for this request
44
44
  Thread.current[:dead_bro_http_events] = []
45
-
45
+
46
46
  # Set tracking start time once for all subscribers (before starting any tracking)
47
47
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
48
48
 
@@ -93,7 +93,7 @@ module DeadBro
93
93
  path: safe_path(data),
94
94
  status: data[:status],
95
95
  duration_ms: duration_ms,
96
- rails_env: rails_env,
96
+ rails_env: DeadBro.env,
97
97
  host: safe_host,
98
98
  params: safe_params(data),
99
99
  user_agent: safe_user_agent(data),
@@ -166,14 +166,6 @@ module DeadBro
166
166
  end
167
167
  end
168
168
 
169
- def self.rails_env
170
- if defined?(Rails) && Rails.respond_to?(:env)
171
- Rails.env
172
- else
173
- ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
174
- end
175
- end
176
-
177
169
  def self.safe_params(data)
178
170
  return {} unless data[:params]
179
171
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.3"
5
5
  end
@@ -88,17 +88,17 @@ module DeadBro
88
88
  def self.should_continue_tracking?
89
89
  events = Thread.current[THREAD_LOCAL_KEY]
90
90
  return false unless events
91
-
91
+
92
92
  # Check count limit
93
93
  return false if events.length >= MAX_TRACKED_EVENTS
94
-
94
+
95
95
  # Check time limit
96
96
  start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
97
97
  if start_time
98
98
  elapsed_seconds = Time.now - start_time
99
99
  return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
100
100
  end
101
-
101
+
102
102
  true
103
103
  end
104
104
 
data/lib/dead_bro.rb CHANGED
@@ -6,6 +6,7 @@ module DeadBro
6
6
  autoload :Configuration, "dead_bro/configuration"
7
7
  autoload :Client, "dead_bro/client"
8
8
  autoload :CircuitBreaker, "dead_bro/circuit_breaker"
9
+ autoload :Collectors, "dead_bro/collectors"
9
10
  autoload :Subscriber, "dead_bro/subscriber"
10
11
  autoload :SqlSubscriber, "dead_bro/sql_subscriber"
11
12
  autoload :SqlTrackingMiddleware, "dead_bro/sql_tracking_middleware"
@@ -18,7 +19,7 @@ module DeadBro
18
19
  autoload :MemoryHelpers, "dead_bro/memory_helpers"
19
20
  autoload :JobSubscriber, "dead_bro/job_subscriber"
20
21
  autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
21
- autoload :JobQueueMonitor, "dead_bro/job_queue_monitor"
22
+ autoload :Monitor, "dead_bro/monitor"
22
23
  autoload :Logger, "dead_bro/logger"
23
24
  begin
24
25
  require "dead_bro/railtie"
@@ -34,7 +35,7 @@ module DeadBro
34
35
  def self.configuration
35
36
  @configuration ||= Configuration.new
36
37
  end
37
-
38
+
38
39
  def self.reset_configuration!
39
40
  @configuration = nil
40
41
  @client = nil
@@ -66,16 +67,18 @@ module DeadBro
66
67
  else
67
68
  ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
68
69
  end
70
+ rescue
71
+ "development"
69
72
  end
70
73
 
71
- # Returns the job queue monitor instance
72
- def self.job_queue_monitor
73
- @job_queue_monitor
74
+ # Returns the monitor instance
75
+ def self.monitor
76
+ @monitor
74
77
  end
75
78
 
76
- # Sets the job queue monitor instance
77
- def self.job_queue_monitor=(monitor)
78
- @job_queue_monitor = monitor
79
+ # Sets the monitor instance
80
+ def self.monitor=(monitor)
81
+ @monitor = monitor
79
82
  end
80
83
 
81
84
  # Shared constant for tracking start time (used by all subscribers)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -24,10 +24,17 @@ files:
24
24
  - lib/dead_bro/cache_subscriber.rb
25
25
  - lib/dead_bro/circuit_breaker.rb
26
26
  - lib/dead_bro/client.rb
27
+ - lib/dead_bro/collectors.rb
28
+ - lib/dead_bro/collectors/database.rb
29
+ - lib/dead_bro/collectors/filesystem.rb
30
+ - lib/dead_bro/collectors/jobs.rb
31
+ - lib/dead_bro/collectors/network.rb
32
+ - lib/dead_bro/collectors/process_info.rb
33
+ - lib/dead_bro/collectors/sample_store.rb
34
+ - lib/dead_bro/collectors/system.rb
27
35
  - lib/dead_bro/configuration.rb
28
36
  - lib/dead_bro/error_middleware.rb
29
37
  - lib/dead_bro/http_instrumentation.rb
30
- - lib/dead_bro/job_queue_monitor.rb
31
38
  - lib/dead_bro/job_sql_tracking_middleware.rb
32
39
  - lib/dead_bro/job_subscriber.rb
33
40
  - lib/dead_bro/lightweight_memory_tracker.rb
@@ -35,6 +42,7 @@ files:
35
42
  - lib/dead_bro/memory_helpers.rb
36
43
  - lib/dead_bro/memory_leak_detector.rb
37
44
  - lib/dead_bro/memory_tracking_subscriber.rb
45
+ - lib/dead_bro/monitor.rb
38
46
  - lib/dead_bro/railtie.rb
39
47
  - lib/dead_bro/redis_subscriber.rb
40
48
  - lib/dead_bro/sql_subscriber.rb