dead_bro 0.2.6 → 0.2.7

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: 7e977f41766dfbcf7ca52cf14aafb217595f35860746a553bd9386b1ddfe7797
4
- data.tar.gz: 24858e179f45181d6d144352347ed3f760a19abe53cead83c80eb4644e281f39
3
+ metadata.gz: 9a78b3a99d00159acb1bf2fd97a0eed54ba0e03a24d7bbd2ec840e4bf779107f
4
+ data.tar.gz: 53aba3dcf00e53f210020529413561812de25b3399db8ce040b9976ab798e8d1
5
5
  SHA512:
6
- metadata.gz: 7310571657927f6404088b2605d710cb9e107d5e63231cddcf785ffc9cc22a78ff634d5bc5969e34d08e634ed0cdc46f85f8f2d6937bc05ebe9af14b00c4968c
7
- data.tar.gz: eaaec05db2cdf1d2da03cfa873c333286d891e33cb2e37d6b347b319a3ae8481f142182d87c3ce20bd2990829075aa236e52c9f52388d2799fc862b1511ceabd
6
+ metadata.gz: c150a7a452b46c4a600afbccb4a520a2f52b0c0bb3a85fec8f0a41f0ece56ea5161575a30a15002c18a8fa2b82e5692ccf13e49920ea3fffc031f7d23188027b
7
+ data.tar.gz: 22b3ea56482f38005ccbd98eae13be751fade782a69f355573ccd49fb2524e3f9bd2c9e40128df5bbb0c2e521924d7f8cba2c49c41a88ad67efafb6041c2f50d
@@ -31,6 +31,9 @@ module DeadBro
31
31
  end
32
32
  end
33
33
 
34
+ # Truncate large arrays to avoid 413 Request Entity Too Large
35
+ payload = truncate_payload_for_request(payload)
36
+
34
37
  # Make the HTTP request (async)
35
38
  make_http_request(event_name, payload, @configuration.api_key)
36
39
 
@@ -62,6 +65,28 @@ module DeadBro
62
65
 
63
66
  private
64
67
 
68
+ # Limit payload size to avoid 413 from nginx/reverse proxies. Returns a new hash.
69
+ def truncate_payload_for_request(payload)
70
+ return payload unless payload.is_a?(Hash)
71
+
72
+ max_sql = @configuration.respond_to?(:max_sql_queries_to_send) ? @configuration.max_sql_queries_to_send : 500
73
+ max_logs = @configuration.respond_to?(:max_logs_to_send) ? @configuration.max_logs_to_send : 100
74
+
75
+ out = payload.dup
76
+
77
+ if out.key?(:sql_queries) && out[:sql_queries].is_a?(Array) && out[:sql_queries].size > max_sql
78
+ out[:sql_queries_total_count] = out[:sql_queries].size
79
+ out[:sql_queries] = out[:sql_queries].first(max_sql)
80
+ end
81
+
82
+ if out.key?(:logs) && out[:logs].is_a?(Array) && out[:logs].size > max_logs
83
+ out[:logs_total_count] = out[:logs].size
84
+ out[:logs] = out[:logs].first(max_logs)
85
+ end
86
+
87
+ out
88
+ end
89
+
65
90
  def create_circuit_breaker
66
91
  return nil unless @configuration.circuit_breaker_enabled
67
92
 
@@ -7,7 +7,7 @@ module DeadBro
7
7
  :circuit_breaker_retry_timeout, :sample_rate, :excluded_controllers, :excluded_jobs,
8
8
  :exclusive_controllers, :exclusive_jobs, :deploy_id, :slow_query_threshold_ms, :explain_analyze_enabled,
9
9
  :job_queue_monitoring_enabled, :enable_db_stats, :enable_process_stats, :enable_system_stats,
10
- :disk_paths, :interfaces_ignore
10
+ :disk_paths, :interfaces_ignore, :max_sql_queries_to_send, :max_logs_to_send
11
11
 
12
12
  def initialize
13
13
  @api_key = nil
@@ -36,6 +36,8 @@ module DeadBro
36
36
  @enable_system_stats = false
37
37
  @disk_paths = ["/"]
38
38
  @interfaces_ignore = %w[lo lo0 docker0]
39
+ @max_sql_queries_to_send = 500 # Cap to avoid 413 Request Entity Too Large
40
+ @max_logs_to_send = 100
39
41
  end
40
42
 
41
43
  def resolve_deploy_id
@@ -30,7 +30,7 @@ module DeadBro
30
30
 
31
31
  # Ensure tracking was started (fallback if perform_start.active_job didn't fire)
32
32
  # This handles job backends that don't emit perform_start events
33
- unless Thread.current[DeadBro::SqlSubscriber::THREAD_LOCAL_KEY]
33
+ unless DeadBro::SqlSubscriber.tracking_active?
34
34
  DeadBro.logger.clear
35
35
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
36
36
  DeadBro::SqlSubscriber.start_request_tracking
@@ -103,10 +103,10 @@ module DeadBro
103
103
 
104
104
  duration_ms = ((finished - started) * 1000.0).round(2)
105
105
  exception = data[:exception_object]
106
+ job_class = data[:job].class.name
106
107
 
107
108
  # Ensure tracking was started (fallback if perform_start.active_job didn't fire)
108
- # This handles job backends that don't emit perform_start events
109
- unless Thread.current[DeadBro::SqlSubscriber::THREAD_LOCAL_KEY]
109
+ unless DeadBro::SqlSubscriber.tracking_active?
110
110
  DeadBro.logger.clear
111
111
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
112
112
  DeadBro::SqlSubscriber.start_request_tracking
@@ -8,19 +8,25 @@ module DeadBro
8
8
  def self.start_request_tracking
9
9
  return unless DeadBro.configuration.memory_tracking_enabled
10
10
 
11
- # Only track essential metrics to minimize overhead
12
- Thread.current[THREAD_LOCAL_KEY] = {
11
+ # Stack allows nested job tracking (e.g. one job performing others in the same thread)
12
+ mem_before = lightweight_memory_usage
13
+ frame = {
13
14
  gc_before: lightweight_gc_stats,
14
- memory_before: lightweight_memory_usage,
15
+ memory_before: mem_before,
15
16
  start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
17
  }
18
+ (Thread.current[THREAD_LOCAL_KEY] ||= []) << frame
17
19
  end
18
20
 
19
21
  def self.stop_request_tracking
20
- events = Thread.current[THREAD_LOCAL_KEY]
21
- Thread.current[THREAD_LOCAL_KEY] = nil
22
+ stack = Thread.current[THREAD_LOCAL_KEY]
23
+ unless stack.is_a?(Array) && stack.any?
24
+ Thread.current[THREAD_LOCAL_KEY] = nil
25
+ return {}
26
+ end
22
27
 
23
- return {} unless events
28
+ events = stack.pop
29
+ Thread.current[THREAD_LOCAL_KEY] = nil if stack.empty?
24
30
 
25
31
  # Calculate only essential metrics
26
32
  gc_after = lightweight_gc_stats
@@ -54,11 +54,18 @@ module DeadBro
54
54
  # Never raise from instrumentation install
55
55
  end
56
56
 
57
+ # Current frame (top of stack) for nested job tracking; nil if none.
58
+ def self.current_events
59
+ stack = Thread.current[THREAD_LOCAL_KEY]
60
+ return nil unless stack.is_a?(Array) && stack.any?
61
+ stack.last
62
+ end
63
+
57
64
  def self.start_request_tracking
58
65
  # Only track if memory tracking is enabled
59
66
  return unless DeadBro.configuration.memory_tracking_enabled
60
67
 
61
- Thread.current[THREAD_LOCAL_KEY] = {
68
+ frame = {
62
69
  allocations: [],
63
70
  memory_snapshots: [],
64
71
  large_objects: [],
@@ -68,11 +75,13 @@ module DeadBro
68
75
  start_time: Time.now.to_f,
69
76
  object_counts_before: count_objects_snapshot
70
77
  }
78
+ (Thread.current[THREAD_LOCAL_KEY] ||= []) << frame
71
79
  end
72
80
 
73
81
  def self.stop_request_tracking
74
- events = Thread.current[THREAD_LOCAL_KEY]
75
- Thread.current[THREAD_LOCAL_KEY] = nil
82
+ stack = Thread.current[THREAD_LOCAL_KEY]
83
+ events = stack.is_a?(Array) && stack.any? ? stack.pop : nil
84
+ Thread.current[THREAD_LOCAL_KEY] = nil if stack.nil? || stack.empty?
76
85
 
77
86
  if events
78
87
  events[:gc_after] = gc_stats
@@ -92,22 +101,24 @@ module DeadBro
92
101
 
93
102
  # Record request-level allocation counters from Rails instrumentation.
94
103
  def self.record_request_allocations(allocations:, allocated_bytes:)
95
- return unless Thread.current[THREAD_LOCAL_KEY]
104
+ events = current_events
105
+ return unless events
96
106
 
97
- Thread.current[THREAD_LOCAL_KEY][:request_allocations] = {
107
+ events[:request_allocations] = {
98
108
  allocations: allocations,
99
109
  allocated_bytes: allocated_bytes
100
110
  }
101
111
  end
102
112
 
103
113
  def self.track_allocation(data, started, finished)
104
- return unless Thread.current[THREAD_LOCAL_KEY]
114
+ events = current_events
115
+ return unless events
105
116
 
106
117
  # Only track if we have meaningful allocation data
107
118
  return unless data.is_a?(Hash) && data[:count] && data[:size]
108
119
 
109
120
  # Limit allocations per request to prevent memory bloat
110
- allocations = Thread.current[THREAD_LOCAL_KEY][:allocations]
121
+ allocations = events[:allocations]
111
122
  return if allocations.length >= MAX_ALLOCATIONS_PER_REQUEST
112
123
 
113
124
  # Simplified allocation tracking (avoid expensive operations)
@@ -124,14 +135,15 @@ module DeadBro
124
135
  large_object: true,
125
136
  size_mb: (data[:size] / 1_000_000.0).round(2)
126
137
  )
127
- Thread.current[THREAD_LOCAL_KEY][:large_objects] << large_object
138
+ events[:large_objects] << large_object
128
139
  end
129
140
 
130
- Thread.current[THREAD_LOCAL_KEY][:allocations] << allocation
141
+ events[:allocations] << allocation
131
142
  end
132
143
 
133
144
  def self.take_memory_snapshot(label = nil)
134
- return unless Thread.current[THREAD_LOCAL_KEY]
145
+ events = current_events
146
+ return unless events
135
147
 
136
148
  snapshot = {
137
149
  label: label || "snapshot_#{Time.now.to_i}",
@@ -142,7 +154,7 @@ module DeadBro
142
154
  heap_pages: heap_pages
143
155
  }
144
156
 
145
- Thread.current[THREAD_LOCAL_KEY][:memory_snapshots] << snapshot
157
+ events[:memory_snapshots] << snapshot
146
158
  end
147
159
 
148
160
  def self.analyze_memory_performance(memory_events)
@@ -16,13 +16,25 @@ module DeadBro
16
16
  THREAD_LOCAL_EXPLAIN_PENDING_KEY = :dead_bro_explain_pending
17
17
  MAX_TRACKED_QUERIES = 1000
18
18
 
19
+ # True when there is at least one active tracking context (e.g. for nested jobs).
20
+ def self.tracking_active?
21
+ stack = Thread.current[THREAD_LOCAL_KEY]
22
+ stack.is_a?(Array) && stack.any?
23
+ end
24
+
25
+ # Current queries array (top of stack); nil if no active tracking.
26
+ def self.current_queries_array
27
+ stack = Thread.current[THREAD_LOCAL_KEY]
28
+ return nil unless stack.is_a?(Array) && stack.any?
29
+ stack.last
30
+ end
31
+
19
32
  # Check if we should continue tracking based on count and time limits
20
- def self.should_continue_tracking?(thread_local_key, max_count)
21
- events = Thread.current[thread_local_key]
22
- return false unless events
33
+ def self.should_continue_tracking?(current_queries_array, max_count)
34
+ return false unless current_queries_array.is_a?(Array)
23
35
 
24
36
  # Check count limit
25
- return false if events.length >= max_count
37
+ return false if current_queries_array.length >= max_count
26
38
 
27
39
  # Check time limit
28
40
  start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
@@ -44,9 +56,10 @@ module DeadBro
44
56
  end
45
57
 
46
58
  ActiveSupport::Notifications.subscribe(SQL_EVENT_NAME) do |name, started, finished, _unique_id, data|
47
- next if data[:name] == "SCHEMA"
48
- # Only track queries that are part of the current request
49
- next unless Thread.current[THREAD_LOCAL_KEY]
59
+ next if data[:name] == "SCHEMA" || data[:name] == "CACHE" || data[:name] == "BEGIN" || data[:name] == "COMMIT" || data[:name] == "ROLLBACK" || data[:name] == "SAVEPOINT" || data[:name] == "RELEASE"
60
+ # Only track queries that are part of the current request (top of stack for nested jobs)
61
+ current = current_queries_array
62
+ next unless current
50
63
  unique_id = _unique_id
51
64
  allocations = nil
52
65
  captured_backtrace = nil
@@ -82,15 +95,16 @@ module DeadBro
82
95
  start_explain_analyze_background(original_sql, data[:connection_id], query_info, binds)
83
96
  end
84
97
 
85
- # Add to thread-local storage, but only if we haven't exceeded the limits
86
- if should_continue_tracking?(THREAD_LOCAL_KEY, MAX_TRACKED_QUERIES)
87
- Thread.current[THREAD_LOCAL_KEY] << query_info
98
+ # Add to current context (top of stack), but only if we haven't exceeded the limits
99
+ if should_continue_tracking?(current, MAX_TRACKED_QUERIES)
100
+ current << query_info
88
101
  end
89
102
  end
90
103
  end
91
104
 
92
105
  def self.start_request_tracking
93
- Thread.current[THREAD_LOCAL_KEY] = []
106
+ # Stack allows nested job tracking (e.g. one job performing others in the same thread)
107
+ (Thread.current[THREAD_LOCAL_KEY] ||= []) << []
94
108
  Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = {}
95
109
  Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = {}
96
110
  Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = {}
@@ -103,15 +117,17 @@ module DeadBro
103
117
  # all explain_plan fields are populated
104
118
  wait_for_pending_explains(5.0) # 5 second timeout
105
119
 
106
- # Get queries after waiting for EXPLAIN to complete
107
- queries = Thread.current[THREAD_LOCAL_KEY]
108
-
109
- Thread.current[THREAD_LOCAL_KEY] = nil
110
- Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
111
- Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
112
- Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
113
- Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = nil
114
- queries || []
120
+ stack = Thread.current[THREAD_LOCAL_KEY]
121
+ queries = stack.is_a?(Array) && stack.any? ? stack.pop : []
122
+ # Clear thread locals when stack is empty so "tracking not started" behaves correctly
123
+ if stack.nil? || stack.empty?
124
+ Thread.current[THREAD_LOCAL_KEY] = nil
125
+ Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
126
+ Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
127
+ Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
128
+ Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = nil
129
+ end
130
+ queries
115
131
  end
116
132
 
117
133
  def self.wait_for_pending_explains(timeout_seconds)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.6"
4
+ VERSION = "0.2.7"
5
5
  end
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.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa