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,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module DeadBro
|
|
6
|
+
class Subscriber
|
|
7
|
+
EVENT_NAME = "process_action.action_controller"
|
|
8
|
+
|
|
9
|
+
def self.subscribe!(client: Client.new)
|
|
10
|
+
ActiveSupport::Notifications.subscribe(EVENT_NAME) do |name, started, finished, _unique_id, data|
|
|
11
|
+
# Skip excluded controllers or controller#action pairs
|
|
12
|
+
# Also check exclusive_controller_actions - if defined, only track those
|
|
13
|
+
begin
|
|
14
|
+
controller_name = data[:controller].to_s
|
|
15
|
+
action_name = data[:action].to_s
|
|
16
|
+
if DeadBro.configuration.excluded_controller?(controller_name, action_name)
|
|
17
|
+
puts "excluded controller"
|
|
18
|
+
next
|
|
19
|
+
end
|
|
20
|
+
# If exclusive_controller_actions is defined and not empty, only track matching actions
|
|
21
|
+
unless DeadBro.configuration.exclusive_controller?(controller_name, action_name)
|
|
22
|
+
puts "exclusive controller"
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
rescue
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
29
|
+
# Stop SQL tracking and get collected queries (this was started by the request)
|
|
30
|
+
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
31
|
+
|
|
32
|
+
# Stop cache and redis tracking
|
|
33
|
+
cache_events = defined?(DeadBro::CacheSubscriber) ? DeadBro::CacheSubscriber.stop_request_tracking : []
|
|
34
|
+
redis_events = defined?(DeadBro::RedisSubscriber) ? DeadBro::RedisSubscriber.stop_request_tracking : []
|
|
35
|
+
|
|
36
|
+
# Stop view rendering tracking and get collected view events
|
|
37
|
+
view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
|
|
38
|
+
view_performance = DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events)
|
|
39
|
+
|
|
40
|
+
# Stop memory tracking and get collected memory data
|
|
41
|
+
if DeadBro.configuration.allocation_tracking_enabled && defined?(DeadBro::MemoryTrackingSubscriber)
|
|
42
|
+
detailed_memory = DeadBro::MemoryTrackingSubscriber.stop_request_tracking
|
|
43
|
+
memory_performance = DeadBro::MemoryTrackingSubscriber.analyze_memory_performance(detailed_memory)
|
|
44
|
+
# Keep memory_events compact and user-friendly (no large raw arrays)
|
|
45
|
+
memory_events = {
|
|
46
|
+
memory_before: detailed_memory[:memory_before],
|
|
47
|
+
memory_after: detailed_memory[:memory_after],
|
|
48
|
+
duration_seconds: detailed_memory[:duration_seconds],
|
|
49
|
+
allocations_count: (detailed_memory[:allocations] || []).length,
|
|
50
|
+
memory_snapshots_count: (detailed_memory[:memory_snapshots] || []).length,
|
|
51
|
+
large_objects_count: (detailed_memory[:large_objects] || []).length
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
lightweight_memory = DeadBro::LightweightMemoryTracker.stop_request_tracking
|
|
55
|
+
# Separate raw readings from derived performance metrics to avoid duplicating data
|
|
56
|
+
memory_events = {
|
|
57
|
+
memory_before: lightweight_memory[:memory_before],
|
|
58
|
+
memory_after: lightweight_memory[:memory_after]
|
|
59
|
+
}
|
|
60
|
+
memory_performance = {
|
|
61
|
+
memory_growth_mb: lightweight_memory[:memory_growth_mb],
|
|
62
|
+
gc_count_increase: lightweight_memory[:gc_count_increase],
|
|
63
|
+
heap_pages_increase: lightweight_memory[:heap_pages_increase],
|
|
64
|
+
duration_seconds: lightweight_memory[:duration_seconds]
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Record memory sample for leak detection (only if memory tracking enabled)
|
|
69
|
+
if DeadBro.configuration.memory_tracking_enabled
|
|
70
|
+
DeadBro::MemoryLeakDetector.record_memory_sample({
|
|
71
|
+
memory_usage: memory_usage_mb,
|
|
72
|
+
gc_count: gc_stats[:count],
|
|
73
|
+
heap_pages: gc_stats[:heap_allocated_pages],
|
|
74
|
+
object_count: gc_stats[:heap_live_slots],
|
|
75
|
+
request_id: data[:request_id],
|
|
76
|
+
controller: data[:controller],
|
|
77
|
+
action: data[:action]
|
|
78
|
+
})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Report exceptions attached to this action (e.g. controller/view errors)
|
|
82
|
+
if data[:exception] || data[:exception_object]
|
|
83
|
+
begin
|
|
84
|
+
exception_class, exception_message = data[:exception] if data[:exception]
|
|
85
|
+
exception_obj = data[:exception_object]
|
|
86
|
+
backtrace = Array(exception_obj&.backtrace).first(50)
|
|
87
|
+
|
|
88
|
+
error_payload = {
|
|
89
|
+
controller: data[:controller],
|
|
90
|
+
action: data[:action],
|
|
91
|
+
format: data[:format],
|
|
92
|
+
method: data[:method],
|
|
93
|
+
path: safe_path(data),
|
|
94
|
+
status: data[:status],
|
|
95
|
+
duration_ms: duration_ms,
|
|
96
|
+
rails_env: rails_env,
|
|
97
|
+
host: safe_host,
|
|
98
|
+
params: safe_params(data),
|
|
99
|
+
user_agent: safe_user_agent(data),
|
|
100
|
+
user_id: extract_user_id(data),
|
|
101
|
+
exception_class: exception_class || exception_obj&.class&.name,
|
|
102
|
+
message: (exception_message || exception_obj&.message).to_s[0, 1000],
|
|
103
|
+
backtrace: backtrace,
|
|
104
|
+
error: true,
|
|
105
|
+
logs: DeadBro.logger.logs
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
event_name = (exception_class || exception_obj&.class&.name || "exception").to_s
|
|
109
|
+
client.post_metric(event_name: event_name, payload: error_payload)
|
|
110
|
+
rescue
|
|
111
|
+
ensure
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
payload = {
|
|
117
|
+
controller: data[:controller],
|
|
118
|
+
action: data[:action],
|
|
119
|
+
format: data[:format],
|
|
120
|
+
method: data[:method],
|
|
121
|
+
path: safe_path(data),
|
|
122
|
+
status: data[:status],
|
|
123
|
+
duration_ms: duration_ms,
|
|
124
|
+
view_runtime_ms: data[:view_runtime],
|
|
125
|
+
db_runtime_ms: data[:db_runtime],
|
|
126
|
+
host: safe_host,
|
|
127
|
+
rails_env: rails_env,
|
|
128
|
+
params: safe_params(data),
|
|
129
|
+
user_agent: safe_user_agent(data),
|
|
130
|
+
user_id: extract_user_id(data),
|
|
131
|
+
memory_usage: memory_usage_mb,
|
|
132
|
+
gc_stats: gc_stats,
|
|
133
|
+
sql_count: sql_count(data),
|
|
134
|
+
sql_queries: sql_queries,
|
|
135
|
+
http_outgoing: Thread.current[:dead_bro_http_events] || [],
|
|
136
|
+
cache_events: cache_events,
|
|
137
|
+
redis_events: redis_events,
|
|
138
|
+
cache_hits: cache_hits(data),
|
|
139
|
+
cache_misses: cache_misses(data),
|
|
140
|
+
view_events: view_events,
|
|
141
|
+
view_performance: view_performance,
|
|
142
|
+
memory_events: memory_events,
|
|
143
|
+
memory_performance: memory_performance,
|
|
144
|
+
logs: DeadBro.logger.logs
|
|
145
|
+
}
|
|
146
|
+
client.post_metric(event_name: name, payload: payload)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.safe_path(data)
|
|
151
|
+
path = data[:path] || (data[:request] && data[:request].path)
|
|
152
|
+
path.to_s
|
|
153
|
+
rescue
|
|
154
|
+
""
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.safe_host
|
|
158
|
+
if defined?(Rails) && Rails.respond_to?(:application)
|
|
159
|
+
begin
|
|
160
|
+
Rails.application.class.module_parent_name
|
|
161
|
+
rescue
|
|
162
|
+
""
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
""
|
|
166
|
+
end
|
|
167
|
+
end
|
|
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
|
+
def self.safe_params(data)
|
|
178
|
+
return {} unless data[:params]
|
|
179
|
+
|
|
180
|
+
params = data[:params]
|
|
181
|
+
begin
|
|
182
|
+
params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
|
|
183
|
+
rescue
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
unless params.is_a?(Hash)
|
|
187
|
+
return {}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Remove router-provided keys that we already send at top-level
|
|
191
|
+
router_keys = %w[controller action format]
|
|
192
|
+
|
|
193
|
+
# Filter out sensitive parameters
|
|
194
|
+
sensitive_keys = %w[password password_confirmation token secret key]
|
|
195
|
+
|
|
196
|
+
filtered = params.dup
|
|
197
|
+
router_keys.each { |k| filtered.delete(k) || filtered.delete(k.to_sym) }
|
|
198
|
+
filtered = filtered.except(*sensitive_keys, *sensitive_keys.map(&:to_sym)) if filtered.respond_to?(:except)
|
|
199
|
+
|
|
200
|
+
# Truncate deeply to keep payload small and safe
|
|
201
|
+
truncate_value(filtered)
|
|
202
|
+
rescue
|
|
203
|
+
{}
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Recursively truncate values to reasonable sizes to avoid huge payloads
|
|
207
|
+
def self.truncate_value(value, max_str: 200, max_array: 20, max_hash_keys: 30)
|
|
208
|
+
case value
|
|
209
|
+
when String
|
|
210
|
+
(value.length > max_str) ? value[0, max_str] + "…" : value
|
|
211
|
+
when Numeric, TrueClass, FalseClass, NilClass
|
|
212
|
+
value
|
|
213
|
+
when Array
|
|
214
|
+
value[0, max_array].map { |v| truncate_value(v, max_str: max_str, max_array: max_array, max_hash_keys: max_hash_keys) }
|
|
215
|
+
when Hash
|
|
216
|
+
entries = value.to_a[0, max_hash_keys]
|
|
217
|
+
entries.each_with_object({}) do |(k, v), memo|
|
|
218
|
+
memo[k] = truncate_value(v, max_str: max_str, max_array: max_array, max_hash_keys: max_hash_keys)
|
|
219
|
+
end
|
|
220
|
+
else
|
|
221
|
+
(value.to_s.length > max_str) ? value.to_s[0, max_str] + "…" : value.to_s
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.safe_user_agent(data)
|
|
226
|
+
begin
|
|
227
|
+
# Prefer request object if available
|
|
228
|
+
if data[:request]
|
|
229
|
+
ua = nil
|
|
230
|
+
if data[:request].respond_to?(:user_agent)
|
|
231
|
+
ua = data[:request].user_agent
|
|
232
|
+
elsif data[:request].respond_to?(:env)
|
|
233
|
+
ua = data[:request].env && data[:request].env["HTTP_USER_AGENT"]
|
|
234
|
+
end
|
|
235
|
+
return ua.to_s[0..200]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Fallback to headers object/hash if present in notification data
|
|
239
|
+
if data[:headers]
|
|
240
|
+
headers = data[:headers]
|
|
241
|
+
if headers.respond_to?(:[])
|
|
242
|
+
ua = headers["HTTP_USER_AGENT"] || headers["User-Agent"] || headers["user-agent"]
|
|
243
|
+
return ua.to_s[0..200]
|
|
244
|
+
elsif headers.respond_to?(:to_h)
|
|
245
|
+
h = begin
|
|
246
|
+
headers.to_h
|
|
247
|
+
rescue
|
|
248
|
+
{}
|
|
249
|
+
end
|
|
250
|
+
ua = h["HTTP_USER_AGENT"] || h["User-Agent"] || h["user-agent"]
|
|
251
|
+
return ua.to_s[0..200]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Fallback to env hash if present in notification data
|
|
256
|
+
if data[:env].is_a?(Hash)
|
|
257
|
+
ua = data[:env]["HTTP_USER_AGENT"]
|
|
258
|
+
return ua.to_s[0..200]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
""
|
|
262
|
+
rescue
|
|
263
|
+
""
|
|
264
|
+
end
|
|
265
|
+
rescue
|
|
266
|
+
""
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.memory_usage_mb
|
|
270
|
+
if defined?(GC) && GC.respond_to?(:stat)
|
|
271
|
+
# Get memory usage in MB
|
|
272
|
+
memory_kb = begin
|
|
273
|
+
`ps -o rss= -p #{Process.pid}`.to_i
|
|
274
|
+
rescue
|
|
275
|
+
0
|
|
276
|
+
end
|
|
277
|
+
(memory_kb / 1024.0).round(2)
|
|
278
|
+
else
|
|
279
|
+
0
|
|
280
|
+
end
|
|
281
|
+
rescue
|
|
282
|
+
0
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.gc_stats
|
|
286
|
+
if defined?(GC) && GC.respond_to?(:stat)
|
|
287
|
+
stats = GC.stat
|
|
288
|
+
{
|
|
289
|
+
count: stats[:count] || 0,
|
|
290
|
+
heap_allocated_pages: stats[:heap_allocated_pages] || 0,
|
|
291
|
+
heap_sorted_pages: stats[:heap_sorted_pages] || 0,
|
|
292
|
+
total_allocated_objects: stats[:total_allocated_objects] || 0
|
|
293
|
+
}
|
|
294
|
+
else
|
|
295
|
+
{}
|
|
296
|
+
end
|
|
297
|
+
rescue
|
|
298
|
+
{}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def self.sql_count(data)
|
|
302
|
+
# Count SQL queries from the payload if available
|
|
303
|
+
if data[:sql_count]
|
|
304
|
+
data[:sql_count]
|
|
305
|
+
elsif defined?(ActiveRecord) && ActiveRecord::Base.connection
|
|
306
|
+
# Try to get from ActiveRecord connection
|
|
307
|
+
begin
|
|
308
|
+
ActiveRecord::Base.connection.query_cache.size
|
|
309
|
+
rescue
|
|
310
|
+
0
|
|
311
|
+
end
|
|
312
|
+
else
|
|
313
|
+
0
|
|
314
|
+
end
|
|
315
|
+
rescue
|
|
316
|
+
0
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def self.cache_hits(data)
|
|
320
|
+
if data[:cache_hits]
|
|
321
|
+
data[:cache_hits]
|
|
322
|
+
elsif defined?(Rails) && Rails.cache.respond_to?(:stats)
|
|
323
|
+
begin
|
|
324
|
+
Rails.cache.stats[:hits]
|
|
325
|
+
rescue
|
|
326
|
+
0
|
|
327
|
+
end
|
|
328
|
+
else
|
|
329
|
+
0
|
|
330
|
+
end
|
|
331
|
+
rescue
|
|
332
|
+
0
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def self.cache_misses(data)
|
|
336
|
+
if data[:cache_misses]
|
|
337
|
+
data[:cache_misses]
|
|
338
|
+
elsif defined?(Rails) && Rails.cache.respond_to?(:stats)
|
|
339
|
+
begin
|
|
340
|
+
Rails.cache.stats[:misses]
|
|
341
|
+
rescue
|
|
342
|
+
0
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
0
|
|
346
|
+
end
|
|
347
|
+
rescue
|
|
348
|
+
0
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def self.extract_user_id(data)
|
|
352
|
+
data[:headers].env["warden"].user.id
|
|
353
|
+
rescue
|
|
354
|
+
nil
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module DeadBro
|
|
6
|
+
class ViewRenderingSubscriber
|
|
7
|
+
# Rails view rendering events
|
|
8
|
+
RENDER_TEMPLATE_EVENT = "render_template.action_view"
|
|
9
|
+
RENDER_PARTIAL_EVENT = "render_partial.action_view"
|
|
10
|
+
RENDER_COLLECTION_EVENT = "render_collection.action_view"
|
|
11
|
+
|
|
12
|
+
THREAD_LOCAL_KEY = :dead_bro_view_events
|
|
13
|
+
|
|
14
|
+
def self.subscribe!(client: Client.new)
|
|
15
|
+
# Track template rendering
|
|
16
|
+
ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_EVENT) do |name, started, finished, _unique_id, data|
|
|
17
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
18
|
+
|
|
19
|
+
view_info = {
|
|
20
|
+
type: "template",
|
|
21
|
+
identifier: safe_identifier(data[:identifier]),
|
|
22
|
+
layout: data[:layout],
|
|
23
|
+
duration_ms: duration_ms,
|
|
24
|
+
virtual_path: data[:virtual_path],
|
|
25
|
+
rendered_at: Time.now.utc.to_i
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
add_view_event(view_info)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Track partial rendering
|
|
32
|
+
ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_EVENT) do |name, started, finished, _unique_id, data|
|
|
33
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
34
|
+
|
|
35
|
+
view_info = {
|
|
36
|
+
type: "partial",
|
|
37
|
+
identifier: safe_identifier(data[:identifier]),
|
|
38
|
+
layout: data[:layout],
|
|
39
|
+
duration_ms: duration_ms,
|
|
40
|
+
virtual_path: data[:virtual_path],
|
|
41
|
+
cache_key: data[:cache_key],
|
|
42
|
+
rendered_at: Time.now.utc.to_i
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
add_view_event(view_info)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Track collection rendering (for partials rendered in loops)
|
|
49
|
+
ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_EVENT) do |name, started, finished, _unique_id, data|
|
|
50
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
51
|
+
|
|
52
|
+
view_info = {
|
|
53
|
+
type: "collection",
|
|
54
|
+
identifier: safe_identifier(data[:identifier]),
|
|
55
|
+
layout: data[:layout],
|
|
56
|
+
duration_ms: duration_ms,
|
|
57
|
+
virtual_path: data[:virtual_path],
|
|
58
|
+
cache_key: data[:cache_key],
|
|
59
|
+
count: data[:count],
|
|
60
|
+
cached_count: data[:cached_count],
|
|
61
|
+
rendered_at: Time.now.utc.to_i
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
add_view_event(view_info)
|
|
65
|
+
end
|
|
66
|
+
rescue
|
|
67
|
+
# Never raise from instrumentation install
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.start_request_tracking
|
|
71
|
+
Thread.current[THREAD_LOCAL_KEY] = []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.stop_request_tracking
|
|
75
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
76
|
+
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
77
|
+
events || []
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.add_view_event(view_info)
|
|
81
|
+
if Thread.current[THREAD_LOCAL_KEY]
|
|
82
|
+
Thread.current[THREAD_LOCAL_KEY] << view_info
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.safe_identifier(identifier)
|
|
87
|
+
return "" unless identifier.is_a?(String)
|
|
88
|
+
|
|
89
|
+
# Extract meaningful parts of the file path
|
|
90
|
+
# e.g., "/app/views/users/show.html.erb" -> "users/show.html.erb"
|
|
91
|
+
identifier.split("/").last(3).join("/")
|
|
92
|
+
rescue
|
|
93
|
+
identifier.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Analyze view rendering performance
|
|
97
|
+
def self.analyze_view_performance(view_events)
|
|
98
|
+
return {} if view_events.empty?
|
|
99
|
+
|
|
100
|
+
total_duration = view_events.sum { |event| event[:duration_ms] }
|
|
101
|
+
|
|
102
|
+
# Group by view type
|
|
103
|
+
by_type = view_events.group_by { |event| event[:type] }
|
|
104
|
+
|
|
105
|
+
# Find slowest views
|
|
106
|
+
slowest_views = view_events.sort_by { |event| -event[:duration_ms] }.first(5)
|
|
107
|
+
|
|
108
|
+
# Find most frequently rendered views
|
|
109
|
+
view_frequency = view_events.group_by { |event| event[:identifier] }
|
|
110
|
+
.transform_values(&:count)
|
|
111
|
+
.sort_by { |_, count| -count }
|
|
112
|
+
.first(5)
|
|
113
|
+
|
|
114
|
+
# Calculate cache hit rates for partials
|
|
115
|
+
partials = view_events.select { |event| event[:type] == "partial" }
|
|
116
|
+
cache_hits = partials.count { |event| event[:cache_key] }
|
|
117
|
+
cache_hit_rate = partials.any? ? (cache_hits.to_f / partials.count * 100).round(2) : 0
|
|
118
|
+
|
|
119
|
+
# Collection rendering analysis
|
|
120
|
+
collections = view_events.select { |event| event[:type] == "collection" }
|
|
121
|
+
total_collection_items = collections.sum { |event| event[:count] || 0 }
|
|
122
|
+
total_cached_items = collections.sum { |event| event[:cached_count] || 0 }
|
|
123
|
+
collection_cache_hit_rate = (total_collection_items > 0) ?
|
|
124
|
+
(total_cached_items.to_f / total_collection_items * 100).round(2) : 0
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
total_views_rendered: view_events.count,
|
|
128
|
+
total_view_duration_ms: total_duration.round(2),
|
|
129
|
+
average_view_duration_ms: (total_duration / view_events.count).round(2),
|
|
130
|
+
by_type: by_type.transform_values(&:count),
|
|
131
|
+
slowest_views: slowest_views.map { |view|
|
|
132
|
+
{
|
|
133
|
+
identifier: view[:identifier],
|
|
134
|
+
duration_ms: view[:duration_ms],
|
|
135
|
+
type: view[:type]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
most_frequent_views: view_frequency.map { |identifier, count|
|
|
139
|
+
{
|
|
140
|
+
identifier: identifier,
|
|
141
|
+
count: count
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
partial_cache_hit_rate: cache_hit_rate,
|
|
145
|
+
collection_cache_hit_rate: collection_cache_hit_rate,
|
|
146
|
+
total_collection_items: total_collection_items,
|
|
147
|
+
total_cached_collection_items: total_cached_items
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/dead_bro.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dead_bro/version"
|
|
4
|
+
|
|
5
|
+
module DeadBro
|
|
6
|
+
autoload :Configuration, "dead_bro/configuration"
|
|
7
|
+
autoload :Client, "dead_bro/client"
|
|
8
|
+
autoload :CircuitBreaker, "dead_bro/circuit_breaker"
|
|
9
|
+
autoload :Subscriber, "dead_bro/subscriber"
|
|
10
|
+
autoload :SqlSubscriber, "dead_bro/sql_subscriber"
|
|
11
|
+
autoload :SqlTrackingMiddleware, "dead_bro/sql_tracking_middleware"
|
|
12
|
+
autoload :CacheSubscriber, "dead_bro/cache_subscriber"
|
|
13
|
+
autoload :RedisSubscriber, "dead_bro/redis_subscriber"
|
|
14
|
+
autoload :ViewRenderingSubscriber, "dead_bro/view_rendering_subscriber"
|
|
15
|
+
autoload :MemoryTrackingSubscriber, "dead_bro/memory_tracking_subscriber"
|
|
16
|
+
autoload :MemoryLeakDetector, "dead_bro/memory_leak_detector"
|
|
17
|
+
autoload :LightweightMemoryTracker, "dead_bro/lightweight_memory_tracker"
|
|
18
|
+
autoload :MemoryHelpers, "dead_bro/memory_helpers"
|
|
19
|
+
autoload :JobSubscriber, "dead_bro/job_subscriber"
|
|
20
|
+
autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
|
|
21
|
+
autoload :Logger, "dead_bro/logger"
|
|
22
|
+
begin
|
|
23
|
+
require "dead_bro/railtie"
|
|
24
|
+
rescue LoadError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Error < StandardError; end
|
|
28
|
+
|
|
29
|
+
def self.configure
|
|
30
|
+
yield configuration
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.configuration
|
|
34
|
+
@configuration ||= Configuration.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.reset_configuration!
|
|
38
|
+
@configuration = nil
|
|
39
|
+
@client = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns a shared Client instance for use across the application
|
|
43
|
+
def self.client
|
|
44
|
+
@client ||= Client.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a process-stable deploy identifier used when none is configured.
|
|
48
|
+
# Memoized per-Ruby process to avoid generating a new UUID per request.
|
|
49
|
+
def self.process_deploy_id
|
|
50
|
+
@process_deploy_id ||= begin
|
|
51
|
+
require "securerandom"
|
|
52
|
+
SecureRandom.uuid
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns the logger instance for storing and retrieving log messages
|
|
57
|
+
def self.logger
|
|
58
|
+
@logger ||= Logger.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the current environment (Rails.env or ENV fallback)
|
|
62
|
+
def self.env
|
|
63
|
+
if defined?(Rails) && Rails.respond_to?(:env)
|
|
64
|
+
Rails.env
|
|
65
|
+
else
|
|
66
|
+
ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dead_bro
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Emanuel Comsa
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Gem used by DeadBro - Rails APM to track performance metrics of Rails
|
|
13
|
+
apps.
|
|
14
|
+
email:
|
|
15
|
+
- office@rubydev.ro
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- FEATURES.md
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/dead_bro.rb
|
|
24
|
+
- lib/dead_bro/cache_subscriber.rb
|
|
25
|
+
- lib/dead_bro/circuit_breaker.rb
|
|
26
|
+
- lib/dead_bro/client.rb
|
|
27
|
+
- lib/dead_bro/configuration.rb
|
|
28
|
+
- lib/dead_bro/error_middleware.rb
|
|
29
|
+
- lib/dead_bro/http_instrumentation.rb
|
|
30
|
+
- lib/dead_bro/job_sql_tracking_middleware.rb
|
|
31
|
+
- lib/dead_bro/job_subscriber.rb
|
|
32
|
+
- lib/dead_bro/lightweight_memory_tracker.rb
|
|
33
|
+
- lib/dead_bro/logger.rb
|
|
34
|
+
- lib/dead_bro/memory_helpers.rb
|
|
35
|
+
- lib/dead_bro/memory_leak_detector.rb
|
|
36
|
+
- lib/dead_bro/memory_tracking_subscriber.rb
|
|
37
|
+
- lib/dead_bro/railtie.rb
|
|
38
|
+
- lib/dead_bro/redis_subscriber.rb
|
|
39
|
+
- lib/dead_bro/sql_subscriber.rb
|
|
40
|
+
- lib/dead_bro/sql_tracking_middleware.rb
|
|
41
|
+
- lib/dead_bro/subscriber.rb
|
|
42
|
+
- lib/dead_bro/version.rb
|
|
43
|
+
- lib/dead_bro/view_rendering_subscriber.rb
|
|
44
|
+
homepage: https://www.deadbro.com
|
|
45
|
+
licenses: []
|
|
46
|
+
metadata:
|
|
47
|
+
homepage_uri: https://www.deadbro.com
|
|
48
|
+
source_code_uri: https://github.com/rubydevro/dead_bro
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: 3.0.0
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.6.7
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Minimal APM for Rails apps.
|
|
66
|
+
test_files: []
|