dead_bro 0.2.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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ class JobSqlTrackingMiddleware
5
+ def self.subscribe!
6
+ # Start SQL tracking when a job begins - use the start event, not the complete event
7
+ ActiveSupport::Notifications.subscribe("perform_start.active_job") do |name, started, finished, _unique_id, data|
8
+ # Clear logs for this job
9
+ DeadBro.logger.clear
10
+ DeadBro::SqlSubscriber.start_request_tracking
11
+
12
+ # Start lightweight memory tracking for this job
13
+ if defined?(DeadBro::LightweightMemoryTracker)
14
+ DeadBro::LightweightMemoryTracker.start_request_tracking
15
+ end
16
+
17
+ # Start detailed memory tracking when allocation tracking is enabled
18
+ if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
19
+ DeadBro::MemoryTrackingSubscriber.start_request_tracking
20
+ end
21
+ end
22
+ rescue
23
+ # Never raise from instrumentation install
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "active_support/notifications"
5
+ rescue LoadError
6
+ # ActiveSupport not available
7
+ end
8
+
9
+ module DeadBro
10
+ class JobSubscriber
11
+ JOB_EVENT_NAME = "perform.active_job"
12
+ JOB_EXCEPTION_EVENT_NAME = "exception.active_job"
13
+
14
+ def self.subscribe!(client: Client.new)
15
+ # Track job execution
16
+ ActiveSupport::Notifications.subscribe(JOB_EVENT_NAME) do |name, started, finished, _unique_id, data|
17
+ begin
18
+ job_class_name = data[:job].class.name
19
+ if DeadBro.configuration.excluded_job?(job_class_name)
20
+ next
21
+ end
22
+ # If exclusive_jobs is defined and not empty, only track matching jobs
23
+ unless DeadBro.configuration.exclusive_job?(job_class_name)
24
+ next
25
+ end
26
+ rescue
27
+ end
28
+
29
+ duration_ms = ((finished - started) * 1000.0).round(2)
30
+
31
+ # Get SQL queries executed during this job
32
+ sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
33
+
34
+ # Stop memory tracking and get collected memory data
35
+ if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
36
+ detailed_memory = DeadBro::MemoryTrackingSubscriber.stop_request_tracking
37
+ memory_performance = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
38
+ # Keep memory_events compact and user-friendly (no large raw arrays)
39
+ memory_events = {
40
+ memory_before: detailed_memory[:memory_before],
41
+ memory_after: detailed_memory[:memory_after],
42
+ duration_seconds: detailed_memory[:duration_seconds],
43
+ allocations_count: (detailed_memory[:allocations] || []).length,
44
+ memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
45
+ large_objects_count: (detailed_memory[:large_objects] || []).length
46
+ }
47
+ else
48
+ lightweight_memory = DeadBro::LightweightMemoryTracker.stop_request_tracking
49
+ # Separate raw readings from derived performance metrics to avoid duplicating data
50
+ memory_events = {
51
+ memory_before: lightweight_memory[:memory_before],
52
+ memory_after: lightweight_memory[:memory_after]
53
+ }
54
+ memory_performance = {
55
+ memory_growth_mb: lightweight_memory[:memory_growth_mb],
56
+ gc_count_increase: lightweight_memory[:gc_count_increase],
57
+ heap_pages_increase: lightweight_memory[:heap_pages_increase],
58
+ duration_seconds: lightweight_memory[:duration_seconds]
59
+ }
60
+ end
61
+
62
+ payload = {
63
+ job_class: data[:job].class.name,
64
+ job_id: data[:job].job_id,
65
+ queue_name: data[:job].queue_name,
66
+ arguments: safe_arguments(data[:job].arguments),
67
+ duration_ms: duration_ms,
68
+ status: "completed",
69
+ sql_queries: sql_queries,
70
+ rails_env: safe_rails_env,
71
+ host: safe_host,
72
+ memory_usage: memory_usage_mb,
73
+ gc_stats: gc_stats,
74
+ memory_events: memory_events,
75
+ memory_performance: memory_performance,
76
+ logs: DeadBro.logger.logs
77
+ }
78
+
79
+ client.post_metric(event_name: name, payload: payload)
80
+ end
81
+
82
+ # Track job exceptions
83
+ ActiveSupport::Notifications.subscribe(JOB_EXCEPTION_EVENT_NAME) do |name, started, finished, _unique_id, data|
84
+ begin
85
+ job_class_name = data[:job].class.name
86
+ if DeadBro.configuration.excluded_job?(job_class_name)
87
+ next
88
+ end
89
+ # If exclusive_jobs is defined and not empty, only track matching jobs
90
+ unless DeadBro.configuration.exclusive_job?(job_class_name)
91
+ next
92
+ end
93
+ rescue
94
+ end
95
+
96
+ duration_ms = ((finished - started) * 1000.0).round(2)
97
+ exception = data[:exception_object]
98
+
99
+ # Get SQL queries executed during this job
100
+ sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
101
+
102
+ # Stop memory tracking and get collected memory data
103
+ if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
104
+ detailed_memory = DeadBro::MemoryTrackingSubscriber.stop_request_tracking
105
+ memory_performance = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
106
+ # Keep memory_events compact and user-friendly (no large raw arrays)
107
+ memory_events = {
108
+ memory_before: detailed_memory[:memory_before],
109
+ memory_after: detailed_memory[:memory_after],
110
+ duration_seconds: detailed_memory[:duration_seconds],
111
+ allocations_count: (detailed_memory[:allocations] || []).length,
112
+ memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
113
+ large_objects_count: (detailed_memory[:large_objects] || []).length
114
+ }
115
+ else
116
+ lightweight_memory = DeadBro::LightweightMemoryTracker.stop_request_tracking
117
+ # Separate raw readings from derived performance metrics to avoid duplicating data
118
+ memory_events = {
119
+ memory_before: lightweight_memory[:memory_before],
120
+ memory_after: lightweight_memory[:memory_after]
121
+ }
122
+ memory_performance = {
123
+ memory_growth_mb: lightweight_memory[:memory_growth_mb],
124
+ gc_count_increase: lightweight_memory[:gc_count_increase],
125
+ heap_pages_increase: lightweight_memory[:heap_pages_increase],
126
+ duration_seconds: lightweight_memory[:duration_seconds]
127
+ }
128
+ end
129
+
130
+ payload = {
131
+ job_class: data[:job].class.name,
132
+ job_id: data[:job].job_id,
133
+ queue_name: data[:job].queue_name,
134
+ arguments: safe_arguments(data[:job].arguments),
135
+ duration_ms: duration_ms,
136
+ status: "failed",
137
+ sql_queries: sql_queries,
138
+ exception_class: exception&.class&.name,
139
+ message: exception&.message&.to_s&.[](0, 1000),
140
+ backtrace: Array(exception&.backtrace).first(50),
141
+ rails_env: safe_rails_env,
142
+ host: safe_host,
143
+ memory_usage: memory_usage_mb,
144
+ gc_stats: gc_stats,
145
+ memory_events: memory_events,
146
+ memory_performance: memory_performance,
147
+ logs: DeadBro.logger.logs
148
+ }
149
+
150
+ event_name = exception&.class&.name || "ActiveJob::Exception"
151
+ client.post_metric(event_name: event_name, payload: payload, error: true)
152
+ end
153
+ rescue
154
+ # Never raise from instrumentation install
155
+ end
156
+
157
+ private
158
+
159
+ def self.safe_arguments(arguments)
160
+ return [] unless arguments.is_a?(Array)
161
+
162
+ # Limit and sanitize job arguments
163
+ arguments.first(10).map do |arg|
164
+ case arg
165
+ when String
166
+ (arg.length > 200) ? arg[0, 200] + "..." : arg
167
+ when Hash
168
+ # Filter sensitive keys and limit size
169
+ filtered = arg.except(*%w[password token secret key])
170
+ (filtered.keys.size > 20) ? filtered.first(20).to_h : filtered
171
+ when Array
172
+ arg.first(5)
173
+ when ActiveRecord::Base
174
+ # Handle ActiveRecord objects safely
175
+ "#{arg.class.name}##{begin
176
+ arg.id
177
+ rescue
178
+ "unknown"
179
+ end}"
180
+ else
181
+ # Convert to string and truncate, but avoid object inspection
182
+ (arg.to_s.length > 200) ? arg.to_s[0, 200] + "..." : arg.to_s
183
+ end
184
+ end
185
+ rescue
186
+ []
187
+ end
188
+
189
+ def self.safe_rails_env
190
+ if defined?(Rails) && Rails.respond_to?(:env)
191
+ Rails.env
192
+ else
193
+ ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
194
+ end
195
+ rescue
196
+ "development"
197
+ end
198
+
199
+ def self.safe_host
200
+ if defined?(Rails) && Rails.respond_to?(:application)
201
+ begin
202
+ Rails.application.class.module_parent_name
203
+ rescue
204
+ ""
205
+ end
206
+ else
207
+ ""
208
+ end
209
+ end
210
+
211
+ def self.memory_usage_mb
212
+ if defined?(GC) && GC.respond_to?(:stat)
213
+ # Get memory usage in MB
214
+ memory_kb = begin
215
+ `ps -o rss= -p #{Process.pid}`.to_i
216
+ rescue
217
+ 0
218
+ end
219
+ (memory_kb / 1024.0).round(2)
220
+ else
221
+ 0
222
+ end
223
+ rescue
224
+ 0
225
+ end
226
+
227
+ def self.gc_stats
228
+ if defined?(GC) && GC.respond_to?(:stat)
229
+ stats = GC.stat
230
+ {
231
+ count: stats[:count] || 0,
232
+ heap_allocated_pages: stats[:heap_allocated_pages] || 0,
233
+ heap_sorted_pages: stats[:heap_sorted_pages] || 0,
234
+ total_allocated_objects: stats[:total_allocated_objects] || 0
235
+ }
236
+ else
237
+ {}
238
+ end
239
+ rescue
240
+ {}
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ class LightweightMemoryTracker
5
+ # Ultra-lightweight memory tracking with minimal performance impact
6
+ THREAD_LOCAL_KEY = :dead_bro_lightweight_memory
7
+
8
+ def self.start_request_tracking
9
+ return unless DeadBro.configuration.memory_tracking_enabled
10
+
11
+ # Only track essential metrics to minimize overhead
12
+ Thread.current[THREAD_LOCAL_KEY] = {
13
+ gc_before: lightweight_gc_stats,
14
+ memory_before: lightweight_memory_usage,
15
+ start_time: Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ }
17
+ end
18
+
19
+ def self.stop_request_tracking
20
+ events = Thread.current[THREAD_LOCAL_KEY]
21
+ Thread.current[THREAD_LOCAL_KEY] = nil
22
+
23
+ return {} unless events
24
+
25
+ # Calculate only essential metrics
26
+ gc_after = lightweight_gc_stats
27
+ memory_after = lightweight_memory_usage
28
+
29
+ {
30
+ memory_growth_mb: (memory_after - events[:memory_before]).round(2),
31
+ gc_count_increase: (gc_after[:count] || 0) - (events[:gc_before][:count] || 0),
32
+ heap_pages_increase: (gc_after[:heap_allocated_pages] || 0) - (events[:gc_before][:heap_allocated_pages] || 0),
33
+ duration_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - events[:start_time],
34
+ memory_before: events[:memory_before],
35
+ memory_after: memory_after
36
+ }
37
+ end
38
+
39
+ def self.lightweight_memory_usage
40
+ # Use only GC stats for memory estimation (no system calls)
41
+ return 0 unless defined?(GC) && GC.respond_to?(:stat)
42
+
43
+ gc_stats = GC.stat
44
+ heap_pages = gc_stats[:heap_allocated_pages] || 0
45
+ # Rough estimation: 4KB per page
46
+ (heap_pages * 4) / 1024.0 # Convert to MB
47
+ rescue
48
+ 0
49
+ end
50
+
51
+ def self.lightweight_gc_stats
52
+ return {} unless defined?(GC) && GC.respond_to?(:stat)
53
+
54
+ stats = GC.stat
55
+ {
56
+ count: stats[:count] || 0,
57
+ heap_allocated_pages: stats[:heap_allocated_pages] || 0
58
+ }
59
+ rescue
60
+ {}
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ class Logger
5
+ SEVERITY_LEVELS = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ fatal: 4
11
+ }.freeze
12
+
13
+ # ANSI color codes
14
+ COLOR_RESET = "\033[0m"
15
+ COLOR_DEBUG = "\033[36m" # Cyan
16
+ COLOR_INFO = "\033[32m" # Green
17
+ COLOR_WARN = "\033[33m" # Yellow
18
+ COLOR_ERROR = "\033[31m" # Red
19
+ COLOR_FATAL = "\033[35m" # Magenta
20
+
21
+ def initialize
22
+ @thread_logs_key = :dead_bro_logs
23
+ end
24
+
25
+ def debug(message)
26
+ log(:debug, message)
27
+ end
28
+
29
+ def info(message)
30
+ log(:info, message)
31
+ end
32
+
33
+ def warn(message)
34
+ log(:warn, message)
35
+ end
36
+
37
+ def error(message)
38
+ log(:error, message)
39
+ end
40
+
41
+ def fatal(message)
42
+ log(:fatal, message)
43
+ end
44
+
45
+ # Get all logs for the current thread
46
+ def logs
47
+ Thread.current[@thread_logs_key] || []
48
+ end
49
+
50
+ # Clear logs for the current thread
51
+ def clear
52
+ Thread.current[@thread_logs_key] = []
53
+ end
54
+
55
+ private
56
+
57
+ def log(severity, message)
58
+ timestamp = Time.now.utc
59
+ log_entry = {
60
+ sev: severity.to_s,
61
+ msg: message.to_s,
62
+ time: timestamp.iso8601(3) # Include milliseconds for better precision
63
+ }
64
+
65
+ # Store in thread-local storage
66
+ Thread.current[@thread_logs_key] ||= []
67
+ Thread.current[@thread_logs_key] << log_entry
68
+
69
+ # Print the message immediately
70
+ print_log(severity, message, timestamp)
71
+ end
72
+
73
+ def print_log(severity, message, timestamp)
74
+ formatted_message = format_log_message(severity, message, timestamp)
75
+
76
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
77
+ # Use Rails logger if available (Rails handles its own color formatting)
78
+ case severity
79
+ when :debug
80
+ Rails.logger.debug(formatted_message)
81
+ when :info
82
+ Rails.logger.info(formatted_message)
83
+ when :warn
84
+ Rails.logger.warn(formatted_message)
85
+ when :error
86
+ Rails.logger.error(formatted_message)
87
+ when :fatal
88
+ Rails.logger.fatal(formatted_message)
89
+ end
90
+ else
91
+ # Fallback to stdout with colors
92
+ colored_message = format_log_message_with_color(severity, message, timestamp)
93
+ $stdout.puts(colored_message)
94
+ end
95
+ rescue
96
+ # Never let logging break the application
97
+ $stdout.puts("[DeadBro] #{severity.to_s.upcase}: #{message}")
98
+ end
99
+
100
+ def format_log_message(severity, message, timestamp)
101
+ "[DeadBro] #{timestamp.iso8601(3)} #{severity.to_s.upcase}: #{message}"
102
+ end
103
+
104
+ def format_log_message_with_color(severity, message, timestamp)
105
+ color = color_for_severity(severity)
106
+ severity_str = severity.to_s.upcase
107
+ "#{color}[DeadBro] #{timestamp.iso8601(3)} #{severity_str}: #{message}#{COLOR_RESET}"
108
+ end
109
+
110
+ def color_for_severity(severity)
111
+ case severity
112
+ when :debug
113
+ COLOR_DEBUG
114
+ when :info
115
+ COLOR_INFO
116
+ when :warn
117
+ COLOR_WARN
118
+ when :error
119
+ COLOR_ERROR
120
+ when :fatal
121
+ COLOR_FATAL
122
+ else
123
+ COLOR_RESET
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ module MemoryHelpers
5
+ # Helper methods for memory tracking and leak detection
6
+
7
+ # Take a memory snapshot with a custom label
8
+ def self.snapshot(label)
9
+ DeadBro::MemoryTrackingSubscriber.take_memory_snapshot(label)
10
+ end
11
+
12
+ # Get current memory analysis
13
+ def self.analyze_memory
14
+ DeadBro::MemoryLeakDetector.get_memory_analysis
15
+ end
16
+
17
+ # Check for memory leaks
18
+ def self.check_for_leaks
19
+ analysis = analyze_memory
20
+ if analysis[:leak_alerts]&.any?
21
+ puts "🚨 Memory leak detected!"
22
+ analysis[:leak_alerts].each do |alert|
23
+ puts " - Growth: #{alert[:memory_growth_mb]}MB"
24
+ puts " - Rate: #{alert[:growth_rate_mb_per_second]}MB/sec"
25
+ puts " - Confidence: #{(alert[:confidence] * 100).round(1)}%"
26
+ puts " - Recent controllers: #{alert[:recent_controllers].join(", ")}"
27
+ end
28
+ else
29
+ puts "✅ No memory leaks detected"
30
+ end
31
+ analysis
32
+ end
33
+
34
+ # Get memory usage summary
35
+ def self.memory_summary
36
+ analysis = analyze_memory
37
+ return "Insufficient data" if analysis[:status] == "insufficient_data"
38
+
39
+ memory_stats = analysis[:memory_stats]
40
+ puts "📊 Memory Summary:"
41
+ puts " - Current: #{memory_stats[:mean]}MB (avg)"
42
+ puts " - Range: #{memory_stats[:min]}MB - #{memory_stats[:max]}MB"
43
+ puts " - Volatility: #{memory_stats[:std_dev]}MB"
44
+ puts " - Samples: #{analysis[:sample_count]}"
45
+
46
+ if analysis[:memory_trend][:slope] > 0
47
+ puts " - Trend: ↗️ Growing at #{analysis[:memory_trend][:slope].round(3)}MB/sec"
48
+ elsif analysis[:memory_trend][:slope] < 0
49
+ puts " - Trend: ↘️ Shrinking at #{analysis[:memory_trend][:slope].abs.round(3)}MB/sec"
50
+ else
51
+ puts " - Trend: ➡️ Stable"
52
+ end
53
+
54
+ analysis
55
+ end
56
+
57
+ # Monitor memory during a block execution
58
+ def self.monitor_memory(label, &block)
59
+ snapshot("before_#{label}")
60
+ result = yield
61
+ snapshot("after_#{label}")
62
+
63
+ # Get the difference
64
+ analysis = analyze_memory
65
+ if analysis[:memory_stats]
66
+ puts "🔍 Memory monitoring for '#{label}':"
67
+ puts " - Memory change: #{analysis[:memory_stats][:max] - analysis[:memory_stats][:min]}MB"
68
+ puts " - Peak usage: #{analysis[:memory_stats][:max]}MB"
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ # Clear memory history (useful for testing)
75
+ def self.clear_history
76
+ DeadBro::MemoryLeakDetector.clear_history
77
+ end
78
+
79
+ # Get top memory allocating classes
80
+ def self.top_allocators
81
+ # This would need to be called from within a request context
82
+ # where memory_events are available
83
+ puts "Top memory allocators:"
84
+ puts " (Call this from within a request to see allocation data)"
85
+ end
86
+ end
87
+ end