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,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadBro
|
|
4
|
+
class RedisSubscriber
|
|
5
|
+
THREAD_LOCAL_KEY = :dead_bro_redis_events
|
|
6
|
+
|
|
7
|
+
def self.subscribe!
|
|
8
|
+
install_redis_instrumentation!
|
|
9
|
+
rescue
|
|
10
|
+
# Never raise from instrumentation install
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.install_redis_instrumentation!
|
|
14
|
+
# Only instrument Redis::Client - this is where commands actually execute
|
|
15
|
+
# Don't instrument Redis class as it has public methods with different signatures
|
|
16
|
+
if defined?(::Redis::Client)
|
|
17
|
+
install_redis_client!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Also try ActiveSupport::Notifications if events are available
|
|
21
|
+
install_notifications_subscription!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.install_redis_client!
|
|
25
|
+
# Only instrument if Redis::Client actually has the call method
|
|
26
|
+
# Check both public and private methods
|
|
27
|
+
has_call = ::Redis::Client.instance_methods(false).include?(:call) ||
|
|
28
|
+
::Redis::Client.private_instance_methods(false).include?(:call)
|
|
29
|
+
return unless has_call
|
|
30
|
+
|
|
31
|
+
mod = Module.new do
|
|
32
|
+
# Use method_missing alternative or alias_method pattern
|
|
33
|
+
# We'll use prepend but make the method signature as flexible as possible
|
|
34
|
+
def call(*args, &block)
|
|
35
|
+
# Extract command from args - first arg is typically the command array
|
|
36
|
+
command = args.first
|
|
37
|
+
# Only track if thread-local storage is set up
|
|
38
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && !command.nil?
|
|
39
|
+
record_redis_command(command) do
|
|
40
|
+
super(*args, &block)
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
# If not tracking, just pass through unchanged
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call_pipeline(pipeline)
|
|
49
|
+
record_redis_pipeline(pipeline) do
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call_multi(multi)
|
|
55
|
+
record_redis_multi(multi) do
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def record_redis_command(command)
|
|
63
|
+
return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
64
|
+
|
|
65
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
error = nil
|
|
67
|
+
begin
|
|
68
|
+
result = yield
|
|
69
|
+
result
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
error = e
|
|
72
|
+
raise
|
|
73
|
+
ensure
|
|
74
|
+
finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
|
+
duration_ms = ((finish_time - start_time) * 1000.0).round(2)
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
cmd_info = extract_command_info(command)
|
|
79
|
+
event = {
|
|
80
|
+
event: "redis.command",
|
|
81
|
+
command: cmd_info[:command],
|
|
82
|
+
key: cmd_info[:key],
|
|
83
|
+
args_count: cmd_info[:args_count],
|
|
84
|
+
duration_ms: duration_ms,
|
|
85
|
+
db: safe_db(@db),
|
|
86
|
+
error: error ? error.class.name : nil
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
90
|
+
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
91
|
+
end
|
|
92
|
+
rescue
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def record_redis_pipeline(pipeline)
|
|
98
|
+
return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
99
|
+
|
|
100
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
101
|
+
begin
|
|
102
|
+
result = yield
|
|
103
|
+
result
|
|
104
|
+
ensure
|
|
105
|
+
finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
|
+
duration_ms = ((finish_time - start_time) * 1000.0).round(2)
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
commands_count = pipeline.commands&.length || 0
|
|
110
|
+
event = {
|
|
111
|
+
event: "redis.pipeline",
|
|
112
|
+
commands_count: commands_count,
|
|
113
|
+
duration_ms: duration_ms,
|
|
114
|
+
db: safe_db(@db)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
118
|
+
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
119
|
+
end
|
|
120
|
+
rescue
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def record_redis_multi(multi)
|
|
126
|
+
return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
127
|
+
|
|
128
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
129
|
+
begin
|
|
130
|
+
result = yield
|
|
131
|
+
result
|
|
132
|
+
ensure
|
|
133
|
+
finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
|
+
duration_ms = ((finish_time - start_time) * 1000.0).round(2)
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
commands_count = multi.commands&.length || 0
|
|
138
|
+
event = {
|
|
139
|
+
event: "redis.multi",
|
|
140
|
+
commands_count: commands_count,
|
|
141
|
+
duration_ms: duration_ms,
|
|
142
|
+
db: safe_db(@db)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
146
|
+
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
147
|
+
end
|
|
148
|
+
rescue
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def extract_command_info(command)
|
|
154
|
+
parts = Array(command).map(&:to_s)
|
|
155
|
+
command_name = parts.first&.upcase
|
|
156
|
+
key = parts[1]
|
|
157
|
+
args_count = (parts.length > 1) ? parts.length - 1 : 0
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
command: safe_command(command_name),
|
|
161
|
+
key: safe_key(key),
|
|
162
|
+
args_count: args_count
|
|
163
|
+
}
|
|
164
|
+
rescue
|
|
165
|
+
{command: nil, key: nil, args_count: nil}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def safe_command(cmd)
|
|
169
|
+
return nil if cmd.nil?
|
|
170
|
+
cmd.to_s[0, 20]
|
|
171
|
+
rescue
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def safe_key(key)
|
|
176
|
+
return nil if key.nil?
|
|
177
|
+
s = key.to_s
|
|
178
|
+
(s.length > 200) ? s[0, 200] + "…" : s
|
|
179
|
+
rescue
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def safe_db(db)
|
|
184
|
+
Integer(db)
|
|
185
|
+
rescue
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
::Redis::Client.prepend(mod) unless ::Redis::Client.ancestors.include?(mod)
|
|
191
|
+
rescue
|
|
192
|
+
# Redis::Client may not be available or may have different structure
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.install_notifications_subscription!
|
|
196
|
+
# Try to subscribe to ActiveSupport::Notifications if available
|
|
197
|
+
# This covers cases where other libraries emit redis.* events
|
|
198
|
+
if defined?(ActiveSupport::Notifications)
|
|
199
|
+
begin
|
|
200
|
+
ActiveSupport::Notifications.subscribe(/\Aredis\..+\z/) do |name, started, finished, _unique_id, data|
|
|
201
|
+
next unless Thread.current[THREAD_LOCAL_KEY]
|
|
202
|
+
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
203
|
+
event = build_event(name, data, duration_ms)
|
|
204
|
+
Thread.current[THREAD_LOCAL_KEY] << event if event
|
|
205
|
+
end
|
|
206
|
+
rescue
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def self.start_request_tracking
|
|
212
|
+
Thread.current[THREAD_LOCAL_KEY] = []
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.stop_request_tracking
|
|
216
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
217
|
+
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
218
|
+
events || []
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def self.build_event(name, data, duration_ms)
|
|
222
|
+
cmd = extract_command(data)
|
|
223
|
+
{
|
|
224
|
+
event: name.to_s,
|
|
225
|
+
command: cmd[:command],
|
|
226
|
+
key: cmd[:key],
|
|
227
|
+
args_count: cmd[:args_count],
|
|
228
|
+
duration_ms: duration_ms,
|
|
229
|
+
db: safe_db(data[:db])
|
|
230
|
+
}
|
|
231
|
+
rescue
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def self.extract_command(data)
|
|
236
|
+
return {command: nil, key: nil, args_count: nil} unless data.is_a?(Hash)
|
|
237
|
+
|
|
238
|
+
parts = if data[:command]
|
|
239
|
+
Array(data[:command]).map(&:to_s)
|
|
240
|
+
elsif data[:commands]
|
|
241
|
+
Array(data[:commands]).flatten.map(&:to_s)
|
|
242
|
+
elsif data[:cmd]
|
|
243
|
+
Array(data[:cmd]).map(&:to_s)
|
|
244
|
+
else
|
|
245
|
+
[]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
command_name = parts.first&.upcase
|
|
249
|
+
key = parts[1]
|
|
250
|
+
args_count = parts.length - 1 if parts.any?
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
command: safe_command(command_name),
|
|
254
|
+
key: safe_key(key),
|
|
255
|
+
args_count: args_count
|
|
256
|
+
}
|
|
257
|
+
rescue
|
|
258
|
+
{command: nil, key: nil, args_count: nil}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def self.safe_command(cmd)
|
|
262
|
+
return nil if cmd.nil?
|
|
263
|
+
cmd.to_s[0, 20]
|
|
264
|
+
rescue
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.safe_key(key)
|
|
269
|
+
return nil if key.nil?
|
|
270
|
+
s = key.to_s
|
|
271
|
+
(s.length > 200) ? s[0, 200] + "…" : s
|
|
272
|
+
rescue
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.safe_db(db)
|
|
277
|
+
Integer(db)
|
|
278
|
+
rescue
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|