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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/FEATURES.md +338 -0
- data/README.md +274 -0
- data/lib/dead_bro/cache_subscriber.rb +106 -0
- data/lib/dead_bro/circuit_breaker.rb +117 -0
- data/lib/dead_bro/client.rb +110 -0
- data/lib/dead_bro/configuration.rb +146 -0
- data/lib/dead_bro/error_middleware.rb +112 -0
- data/lib/dead_bro/http_instrumentation.rb +113 -0
- data/lib/dead_bro/job_sql_tracking_middleware.rb +26 -0
- data/lib/dead_bro/job_subscriber.rb +243 -0
- data/lib/dead_bro/lightweight_memory_tracker.rb +63 -0
- data/lib/dead_bro/logger.rb +127 -0
- data/lib/dead_bro/memory_helpers.rb +87 -0
- data/lib/dead_bro/memory_leak_detector.rb +196 -0
- data/lib/dead_bro/memory_tracking_subscriber.rb +361 -0
- data/lib/dead_bro/railtie.rb +90 -0
- data/lib/dead_bro/redis_subscriber.rb +282 -0
- data/lib/dead_bro/sql_subscriber.rb +467 -0
- data/lib/dead_bro/sql_tracking_middleware.rb +78 -0
- data/lib/dead_bro/subscriber.rb +357 -0
- data/lib/dead_bro/version.rb +5 -0
- data/lib/dead_bro/view_rendering_subscriber.rb +151 -0
- data/lib/dead_bro.rb +69 -0
- metadata +66 -0
|
@@ -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
|