brainzlab 0.1.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 +52 -0
- data/LICENSE +26 -0
- data/README.md +311 -0
- data/lib/brainzlab/configuration.rb +215 -0
- data/lib/brainzlab/context.rb +91 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
- data/lib/brainzlab/instrumentation/active_record.rb +111 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
- data/lib/brainzlab/instrumentation/faraday.rb +182 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +251 -0
- data/lib/brainzlab/instrumentation/httparty.rb +194 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +109 -0
- data/lib/brainzlab/instrumentation/redis.rb +331 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
- data/lib/brainzlab/instrumentation.rb +132 -0
- data/lib/brainzlab/pulse/client.rb +132 -0
- data/lib/brainzlab/pulse/instrumentation.rb +364 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +224 -0
- data/lib/brainzlab/rails/log_formatter.rb +801 -0
- data/lib/brainzlab/rails/log_subscriber.rb +341 -0
- data/lib/brainzlab/rails/railtie.rb +590 -0
- data/lib/brainzlab/recall/buffer.rb +64 -0
- data/lib/brainzlab/recall/client.rb +86 -0
- data/lib/brainzlab/recall/logger.rb +118 -0
- data/lib/brainzlab/recall/provisioner.rb +113 -0
- data/lib/brainzlab/recall.rb +155 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +85 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +374 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +140 -0
- data/lib/generators/brainzlab/install/install_generator.rb +61 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +159 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module NetHttp
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return if @installed
|
|
11
|
+
|
|
12
|
+
::Net::HTTP.prepend(Patch)
|
|
13
|
+
@installed = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def installed?
|
|
17
|
+
@installed
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# For testing purposes
|
|
21
|
+
def reset!
|
|
22
|
+
@installed = false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Patch
|
|
27
|
+
def request(req, body = nil, &block)
|
|
28
|
+
return super unless should_track?
|
|
29
|
+
|
|
30
|
+
# Inject distributed tracing context into outgoing request headers
|
|
31
|
+
inject_trace_context(req)
|
|
32
|
+
|
|
33
|
+
url = build_url(req)
|
|
34
|
+
method = req.method
|
|
35
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
response = super
|
|
39
|
+
track_request(method, url, response.code.to_i, started_at)
|
|
40
|
+
response
|
|
41
|
+
rescue => e
|
|
42
|
+
track_request(method, url, nil, started_at, e.class.name)
|
|
43
|
+
raise
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inject_trace_context(req)
|
|
48
|
+
return unless BrainzLab.configuration.pulse_enabled
|
|
49
|
+
|
|
50
|
+
# Build headers hash and inject trace context
|
|
51
|
+
headers = {}
|
|
52
|
+
BrainzLab::Pulse.inject(headers, format: :all)
|
|
53
|
+
|
|
54
|
+
# Apply headers to request
|
|
55
|
+
headers.each do |key, value|
|
|
56
|
+
req[key] = value
|
|
57
|
+
end
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
BrainzLab.debug_log("Failed to inject trace context: #{e.message}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def should_track?
|
|
65
|
+
return false unless BrainzLab.configuration.instrument_http
|
|
66
|
+
|
|
67
|
+
ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
|
|
68
|
+
!ignore_hosts.include?(address)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_url(req)
|
|
72
|
+
scheme = use_ssl? ? "https" : "http"
|
|
73
|
+
port_str = if (use_ssl? && port == 443) || (!use_ssl? && port == 80)
|
|
74
|
+
""
|
|
75
|
+
else
|
|
76
|
+
":#{port}"
|
|
77
|
+
end
|
|
78
|
+
"#{scheme}://#{address}#{port_str}#{req.path}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def track_request(method, url, status, started_at, error = nil)
|
|
82
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
83
|
+
level = error || (status && status >= 400) ? :error : :info
|
|
84
|
+
|
|
85
|
+
# Add breadcrumb for Reflex
|
|
86
|
+
if BrainzLab.configuration.reflex_enabled
|
|
87
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
88
|
+
"#{method} #{url}",
|
|
89
|
+
category: "http",
|
|
90
|
+
level: level,
|
|
91
|
+
data: { method: method, url: url, status_code: status, duration_ms: duration_ms, error: error }.compact
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Log to Recall at debug level (avoid noise)
|
|
96
|
+
if BrainzLab.configuration.recall_enabled
|
|
97
|
+
BrainzLab::Recall.debug(
|
|
98
|
+
"HTTP #{method} #{url} -> #{status || 'ERROR'}",
|
|
99
|
+
method: method, url: url, status_code: status, duration_ms: duration_ms, error: error
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
rescue => e
|
|
103
|
+
# Don't let instrumentation errors crash the app
|
|
104
|
+
BrainzLab.configuration.logger&.error("[BrainzLab] HTTP instrumentation error: #{e.message}")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module RedisInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::Redis)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
# Redis 5+ uses middleware, older versions need patching
|
|
14
|
+
if redis_5_or_newer?
|
|
15
|
+
install_middleware!
|
|
16
|
+
else
|
|
17
|
+
install_patch!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@installed = true
|
|
21
|
+
BrainzLab.debug_log("Redis instrumentation installed")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def installed?
|
|
25
|
+
@installed
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@installed = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def redis_5_or_newer?
|
|
35
|
+
defined?(::Redis::VERSION) && Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("5.0")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def install_middleware!
|
|
39
|
+
# Redis 5+ uses RedisClient with middleware support
|
|
40
|
+
return unless defined?(::RedisClient)
|
|
41
|
+
|
|
42
|
+
::RedisClient.register(Middleware)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def install_patch!
|
|
46
|
+
# Redis < 5 - patch the client
|
|
47
|
+
::Redis::Client.prepend(LegacyPatch)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Middleware for Redis 5+ (RedisClient)
|
|
52
|
+
module Middleware
|
|
53
|
+
def connect(redis_config)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(command, redis_config)
|
|
58
|
+
return super unless should_track?
|
|
59
|
+
|
|
60
|
+
track_command(command) { super }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def call_pipelined(commands, redis_config)
|
|
64
|
+
return super unless should_track?
|
|
65
|
+
|
|
66
|
+
track_pipeline(commands) { super }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def should_track?
|
|
72
|
+
BrainzLab.configuration.instrument_redis
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def should_skip_command?(command)
|
|
76
|
+
cmd_name = command.first.to_s.downcase
|
|
77
|
+
ignore = BrainzLab.configuration.redis_ignore_commands || []
|
|
78
|
+
ignore.map(&:downcase).include?(cmd_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def track_command(command, &block)
|
|
82
|
+
return yield if should_skip_command?(command)
|
|
83
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
84
|
+
error_info = nil
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
result = yield
|
|
88
|
+
record_command(command, started_at)
|
|
89
|
+
result
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
error_info = e.class.name
|
|
92
|
+
record_command(command, started_at, error_info)
|
|
93
|
+
raise
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def track_pipeline(commands, &block)
|
|
98
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
99
|
+
error_info = nil
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
result = yield
|
|
103
|
+
record_pipeline(commands, started_at)
|
|
104
|
+
result
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
error_info = e.class.name
|
|
107
|
+
record_pipeline(commands, started_at, error_info)
|
|
108
|
+
raise
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def record_command(command, started_at, error = nil)
|
|
113
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
114
|
+
cmd_name = command.first.to_s.upcase
|
|
115
|
+
key = extract_key(command)
|
|
116
|
+
level = error ? :error : :info
|
|
117
|
+
|
|
118
|
+
# Add breadcrumb for Reflex
|
|
119
|
+
if BrainzLab.configuration.reflex_enabled
|
|
120
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
121
|
+
"Redis #{cmd_name}",
|
|
122
|
+
category: "redis",
|
|
123
|
+
level: level,
|
|
124
|
+
data: {
|
|
125
|
+
command: cmd_name,
|
|
126
|
+
key: truncate_key(key),
|
|
127
|
+
duration_ms: duration_ms,
|
|
128
|
+
error: error
|
|
129
|
+
}.compact
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Record span for Pulse APM
|
|
134
|
+
record_pulse_span(cmd_name, key, duration_ms, error)
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
BrainzLab.debug_log("Redis instrumentation error: #{e.message}")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def record_pipeline(commands, started_at, error = nil)
|
|
140
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
141
|
+
cmd_names = commands.map { |c| c.first.to_s.upcase }.uniq.join(", ")
|
|
142
|
+
level = error ? :error : :info
|
|
143
|
+
|
|
144
|
+
# Add breadcrumb for Reflex
|
|
145
|
+
if BrainzLab.configuration.reflex_enabled
|
|
146
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
147
|
+
"Redis PIPELINE (#{commands.size} commands)",
|
|
148
|
+
category: "redis",
|
|
149
|
+
level: level,
|
|
150
|
+
data: {
|
|
151
|
+
commands: cmd_names,
|
|
152
|
+
count: commands.size,
|
|
153
|
+
duration_ms: duration_ms,
|
|
154
|
+
error: error
|
|
155
|
+
}.compact
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Record span for Pulse APM
|
|
160
|
+
record_pulse_span("PIPELINE", nil, duration_ms, error, commands.size)
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
BrainzLab.debug_log("Redis instrumentation error: #{e.message}")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def record_pulse_span(command, key, duration_ms, error, pipeline_count = nil)
|
|
166
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
167
|
+
return unless spans
|
|
168
|
+
|
|
169
|
+
span = {
|
|
170
|
+
span_id: SecureRandom.uuid,
|
|
171
|
+
name: "Redis #{command}",
|
|
172
|
+
kind: "redis",
|
|
173
|
+
started_at: Time.now.utc - (duration_ms / 1000.0),
|
|
174
|
+
ended_at: Time.now.utc,
|
|
175
|
+
duration_ms: duration_ms,
|
|
176
|
+
data: {
|
|
177
|
+
command: command,
|
|
178
|
+
key: truncate_key(key),
|
|
179
|
+
pipeline_count: pipeline_count
|
|
180
|
+
}.compact
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if error
|
|
184
|
+
span[:error] = true
|
|
185
|
+
span[:error_class] = error
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
spans << span
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def extract_key(command)
|
|
192
|
+
return nil if command.size < 2
|
|
193
|
+
|
|
194
|
+
# Most Redis commands have the key as the second argument
|
|
195
|
+
key = command[1]
|
|
196
|
+
key.is_a?(String) ? key : key.to_s
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def truncate_key(key)
|
|
200
|
+
return nil unless key
|
|
201
|
+
|
|
202
|
+
key.to_s[0, 100]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Patch for Redis < 5
|
|
207
|
+
module LegacyPatch
|
|
208
|
+
def call(command)
|
|
209
|
+
return super unless should_track?
|
|
210
|
+
return super if should_skip_command?(command)
|
|
211
|
+
|
|
212
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
213
|
+
error_info = nil
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
result = super
|
|
217
|
+
record_command(command, started_at)
|
|
218
|
+
result
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
error_info = e.class.name
|
|
221
|
+
record_command(command, started_at, error_info)
|
|
222
|
+
raise
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def call_pipeline(pipeline)
|
|
227
|
+
return super unless should_track?
|
|
228
|
+
|
|
229
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
230
|
+
error_info = nil
|
|
231
|
+
commands = pipeline.commands
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
result = super
|
|
235
|
+
record_pipeline(commands, started_at)
|
|
236
|
+
result
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
error_info = e.class.name
|
|
239
|
+
record_pipeline(commands, started_at, error_info)
|
|
240
|
+
raise
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def should_track?
|
|
247
|
+
BrainzLab.configuration.instrument_redis
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def should_skip_command?(command)
|
|
251
|
+
cmd_name = command.first.to_s.downcase
|
|
252
|
+
ignore = BrainzLab.configuration.redis_ignore_commands || []
|
|
253
|
+
ignore.map(&:downcase).include?(cmd_name)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def record_command(command, started_at, error = nil)
|
|
257
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
258
|
+
cmd_name = command.first.to_s.upcase
|
|
259
|
+
key = command[1]&.to_s
|
|
260
|
+
level = error ? :error : :info
|
|
261
|
+
|
|
262
|
+
if BrainzLab.configuration.reflex_enabled
|
|
263
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
264
|
+
"Redis #{cmd_name}",
|
|
265
|
+
category: "redis",
|
|
266
|
+
level: level,
|
|
267
|
+
data: {
|
|
268
|
+
command: cmd_name,
|
|
269
|
+
key: key&.slice(0, 100),
|
|
270
|
+
duration_ms: duration_ms,
|
|
271
|
+
error: error
|
|
272
|
+
}.compact
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
record_pulse_span(cmd_name, key, duration_ms, error)
|
|
277
|
+
rescue StandardError => e
|
|
278
|
+
BrainzLab.debug_log("Redis instrumentation error: #{e.message}")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def record_pipeline(commands, started_at, error = nil)
|
|
282
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2)
|
|
283
|
+
level = error ? :error : :info
|
|
284
|
+
|
|
285
|
+
if BrainzLab.configuration.reflex_enabled
|
|
286
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
287
|
+
"Redis PIPELINE (#{commands.size} commands)",
|
|
288
|
+
category: "redis",
|
|
289
|
+
level: level,
|
|
290
|
+
data: {
|
|
291
|
+
count: commands.size,
|
|
292
|
+
duration_ms: duration_ms,
|
|
293
|
+
error: error
|
|
294
|
+
}.compact
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
record_pulse_span("PIPELINE", nil, duration_ms, error, commands.size)
|
|
299
|
+
rescue StandardError => e
|
|
300
|
+
BrainzLab.debug_log("Redis instrumentation error: #{e.message}")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def record_pulse_span(command, key, duration_ms, error, pipeline_count = nil)
|
|
304
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
305
|
+
return unless spans
|
|
306
|
+
|
|
307
|
+
span = {
|
|
308
|
+
span_id: SecureRandom.uuid,
|
|
309
|
+
name: "Redis #{command}",
|
|
310
|
+
kind: "redis",
|
|
311
|
+
started_at: Time.now.utc - (duration_ms / 1000.0),
|
|
312
|
+
ended_at: Time.now.utc,
|
|
313
|
+
duration_ms: duration_ms,
|
|
314
|
+
data: {
|
|
315
|
+
command: command,
|
|
316
|
+
key: key&.slice(0, 100),
|
|
317
|
+
pipeline_count: pipeline_count
|
|
318
|
+
}.compact
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if error
|
|
322
|
+
span[:error] = true
|
|
323
|
+
span[:error_class] = error
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
spans << span
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module SidekiqInstrumentation
|
|
6
|
+
@installed = false
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def install!
|
|
10
|
+
return unless defined?(::Sidekiq)
|
|
11
|
+
return if @installed
|
|
12
|
+
|
|
13
|
+
::Sidekiq.configure_server do |config|
|
|
14
|
+
config.server_middleware do |chain|
|
|
15
|
+
chain.add ServerMiddleware
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Also add client middleware for distributed tracing
|
|
19
|
+
config.client_middleware do |chain|
|
|
20
|
+
chain.add ClientMiddleware
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Client-side middleware for when jobs are enqueued
|
|
25
|
+
::Sidekiq.configure_client do |config|
|
|
26
|
+
config.client_middleware do |chain|
|
|
27
|
+
chain.add ClientMiddleware
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@installed = true
|
|
32
|
+
BrainzLab.debug_log("Sidekiq instrumentation installed")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def installed?
|
|
36
|
+
@installed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@installed = false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Server middleware - runs when jobs are processed
|
|
45
|
+
class ServerMiddleware
|
|
46
|
+
def call(worker, job, queue)
|
|
47
|
+
return yield unless should_trace?
|
|
48
|
+
|
|
49
|
+
started_at = Time.now.utc
|
|
50
|
+
job_class = job["class"] || worker.class.name
|
|
51
|
+
job_id = job["jid"]
|
|
52
|
+
|
|
53
|
+
# Calculate queue wait time
|
|
54
|
+
enqueued_at = job["enqueued_at"] ? Time.at(job["enqueued_at"]) : nil
|
|
55
|
+
queue_wait_ms = enqueued_at ? ((started_at - enqueued_at) * 1000).round(2) : nil
|
|
56
|
+
|
|
57
|
+
# Extract parent trace context if present (distributed tracing)
|
|
58
|
+
parent_context = extract_trace_context(job)
|
|
59
|
+
|
|
60
|
+
# Set up context
|
|
61
|
+
setup_context(job, queue)
|
|
62
|
+
|
|
63
|
+
# Add breadcrumb
|
|
64
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
65
|
+
"Sidekiq #{job_class}",
|
|
66
|
+
category: "job.sidekiq",
|
|
67
|
+
level: :info,
|
|
68
|
+
data: { job_id: job_id, queue: queue, retry_count: job["retry_count"] }
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Initialize Pulse tracing
|
|
72
|
+
Thread.current[:brainzlab_pulse_spans] = []
|
|
73
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
74
|
+
|
|
75
|
+
error_occurred = nil
|
|
76
|
+
begin
|
|
77
|
+
yield
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
error_occurred = e
|
|
80
|
+
raise
|
|
81
|
+
ensure
|
|
82
|
+
record_trace(
|
|
83
|
+
job_class: job_class,
|
|
84
|
+
job_id: job_id,
|
|
85
|
+
queue: queue,
|
|
86
|
+
started_at: started_at,
|
|
87
|
+
queue_wait_ms: queue_wait_ms,
|
|
88
|
+
retry_count: job["retry_count"] || 0,
|
|
89
|
+
parent_context: parent_context,
|
|
90
|
+
error: error_occurred
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
cleanup_context
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def should_trace?
|
|
100
|
+
BrainzLab.configuration.pulse_enabled
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def setup_context(job, queue)
|
|
104
|
+
BrainzLab::Context.current.set_context(
|
|
105
|
+
job_class: job["class"],
|
|
106
|
+
job_id: job["jid"],
|
|
107
|
+
queue_name: queue,
|
|
108
|
+
retry_count: job["retry_count"],
|
|
109
|
+
arguments: job["args"]&.map(&:to_s)&.first(5)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cleanup_context
|
|
114
|
+
Thread.current[:brainzlab_pulse_spans] = nil
|
|
115
|
+
Thread.current[:brainzlab_pulse_breakdown] = nil
|
|
116
|
+
BrainzLab::Context.clear!
|
|
117
|
+
BrainzLab::Pulse::Propagation.clear!
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_trace_context(job)
|
|
121
|
+
return nil unless job["_brainzlab_trace"]
|
|
122
|
+
|
|
123
|
+
trace_data = job["_brainzlab_trace"]
|
|
124
|
+
BrainzLab::Pulse::Propagation::Context.new(
|
|
125
|
+
trace_id: trace_data["trace_id"],
|
|
126
|
+
span_id: trace_data["span_id"],
|
|
127
|
+
sampled: trace_data["sampled"] != false
|
|
128
|
+
)
|
|
129
|
+
rescue StandardError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def record_trace(job_class:, job_id:, queue:, started_at:, queue_wait_ms:, retry_count:, parent_context:, error:)
|
|
134
|
+
ended_at = Time.now.utc
|
|
135
|
+
duration_ms = ((ended_at - started_at) * 1000).round(2)
|
|
136
|
+
|
|
137
|
+
# Collect spans
|
|
138
|
+
spans = Thread.current[:brainzlab_pulse_spans] || []
|
|
139
|
+
breakdown = Thread.current[:brainzlab_pulse_breakdown] || {}
|
|
140
|
+
|
|
141
|
+
formatted_spans = spans.map do |span|
|
|
142
|
+
{
|
|
143
|
+
span_id: span[:span_id],
|
|
144
|
+
name: span[:name],
|
|
145
|
+
kind: span[:kind],
|
|
146
|
+
started_at: format_timestamp(span[:started_at]),
|
|
147
|
+
ended_at: format_timestamp(span[:ended_at]),
|
|
148
|
+
duration_ms: span[:duration_ms],
|
|
149
|
+
data: span[:data],
|
|
150
|
+
error: span[:error],
|
|
151
|
+
error_class: span[:error_class],
|
|
152
|
+
error_message: span[:error_message]
|
|
153
|
+
}.compact
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
payload = {
|
|
157
|
+
trace_id: SecureRandom.uuid,
|
|
158
|
+
name: job_class,
|
|
159
|
+
kind: "job",
|
|
160
|
+
started_at: started_at.utc.iso8601(3),
|
|
161
|
+
ended_at: ended_at.utc.iso8601(3),
|
|
162
|
+
duration_ms: duration_ms,
|
|
163
|
+
job_class: job_class,
|
|
164
|
+
job_id: job_id,
|
|
165
|
+
queue: queue,
|
|
166
|
+
queue_wait_ms: queue_wait_ms,
|
|
167
|
+
executions: retry_count + 1,
|
|
168
|
+
db_ms: breakdown[:db_ms],
|
|
169
|
+
error: error.present?,
|
|
170
|
+
error_class: error&.class&.name,
|
|
171
|
+
error_message: error&.message&.slice(0, 1000),
|
|
172
|
+
spans: formatted_spans,
|
|
173
|
+
environment: BrainzLab.configuration.environment,
|
|
174
|
+
commit: BrainzLab.configuration.commit,
|
|
175
|
+
host: BrainzLab.configuration.host
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Add parent trace info for distributed tracing
|
|
179
|
+
if parent_context&.valid?
|
|
180
|
+
payload[:parent_trace_id] = parent_context.trace_id
|
|
181
|
+
payload[:parent_span_id] = parent_context.span_id
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
BrainzLab::Pulse.client.send_trace(payload.compact)
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
BrainzLab.debug_log("Sidekiq trace recording failed: #{e.message}")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def format_timestamp(ts)
|
|
190
|
+
return nil unless ts
|
|
191
|
+
|
|
192
|
+
case ts
|
|
193
|
+
when Time, DateTime then ts.utc.iso8601(3)
|
|
194
|
+
when Float, Integer then Time.at(ts).utc.iso8601(3)
|
|
195
|
+
when String then ts
|
|
196
|
+
else ts.to_s
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Client middleware - runs when jobs are enqueued
|
|
202
|
+
class ClientMiddleware
|
|
203
|
+
def call(worker_class, job, queue, redis_pool)
|
|
204
|
+
# Inject trace context for distributed tracing
|
|
205
|
+
inject_trace_context(job)
|
|
206
|
+
|
|
207
|
+
# Add breadcrumb for job enqueue
|
|
208
|
+
if BrainzLab.configuration.reflex_enabled
|
|
209
|
+
BrainzLab::Reflex.add_breadcrumb(
|
|
210
|
+
"Enqueue #{job['class']}",
|
|
211
|
+
category: "job.sidekiq.enqueue",
|
|
212
|
+
level: :info,
|
|
213
|
+
data: { queue: queue, job_id: job["jid"] }
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Record span for Pulse
|
|
218
|
+
record_enqueue_span(job, queue)
|
|
219
|
+
|
|
220
|
+
yield
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def inject_trace_context(job)
|
|
226
|
+
return unless BrainzLab.configuration.pulse_enabled
|
|
227
|
+
|
|
228
|
+
# Get or create propagation context
|
|
229
|
+
ctx = BrainzLab::Pulse::Propagation.current
|
|
230
|
+
ctx ||= BrainzLab::Pulse.send(:create_propagation_context)
|
|
231
|
+
|
|
232
|
+
return unless ctx&.valid?
|
|
233
|
+
|
|
234
|
+
job["_brainzlab_trace"] = {
|
|
235
|
+
"trace_id" => ctx.trace_id,
|
|
236
|
+
"span_id" => ctx.span_id,
|
|
237
|
+
"sampled" => ctx.sampled
|
|
238
|
+
}
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
BrainzLab.debug_log("Failed to inject Sidekiq trace context: #{e.message}")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def record_enqueue_span(job, queue)
|
|
244
|
+
spans = Thread.current[:brainzlab_pulse_spans]
|
|
245
|
+
return unless spans
|
|
246
|
+
|
|
247
|
+
spans << {
|
|
248
|
+
span_id: SecureRandom.uuid,
|
|
249
|
+
name: "Enqueue #{job['class']}",
|
|
250
|
+
kind: "job",
|
|
251
|
+
started_at: Time.now.utc,
|
|
252
|
+
ended_at: Time.now.utc,
|
|
253
|
+
duration_ms: 0,
|
|
254
|
+
data: {
|
|
255
|
+
job_class: job["class"],
|
|
256
|
+
job_id: job["jid"],
|
|
257
|
+
queue: queue
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|