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,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