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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +26 -0
  4. data/README.md +311 -0
  5. data/lib/brainzlab/configuration.rb +215 -0
  6. data/lib/brainzlab/context.rb +91 -0
  7. data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
  8. data/lib/brainzlab/instrumentation/active_record.rb +111 -0
  9. data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
  10. data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
  11. data/lib/brainzlab/instrumentation/faraday.rb +182 -0
  12. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  13. data/lib/brainzlab/instrumentation/graphql.rb +251 -0
  14. data/lib/brainzlab/instrumentation/httparty.rb +194 -0
  15. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  16. data/lib/brainzlab/instrumentation/net_http.rb +109 -0
  17. data/lib/brainzlab/instrumentation/redis.rb +331 -0
  18. data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
  19. data/lib/brainzlab/instrumentation.rb +132 -0
  20. data/lib/brainzlab/pulse/client.rb +132 -0
  21. data/lib/brainzlab/pulse/instrumentation.rb +364 -0
  22. data/lib/brainzlab/pulse/propagation.rb +241 -0
  23. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  24. data/lib/brainzlab/pulse/tracer.rb +111 -0
  25. data/lib/brainzlab/pulse.rb +224 -0
  26. data/lib/brainzlab/rails/log_formatter.rb +801 -0
  27. data/lib/brainzlab/rails/log_subscriber.rb +341 -0
  28. data/lib/brainzlab/rails/railtie.rb +590 -0
  29. data/lib/brainzlab/recall/buffer.rb +64 -0
  30. data/lib/brainzlab/recall/client.rb +86 -0
  31. data/lib/brainzlab/recall/logger.rb +118 -0
  32. data/lib/brainzlab/recall/provisioner.rb +113 -0
  33. data/lib/brainzlab/recall.rb +155 -0
  34. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  35. data/lib/brainzlab/reflex/client.rb +85 -0
  36. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  37. data/lib/brainzlab/reflex.rb +374 -0
  38. data/lib/brainzlab/version.rb +5 -0
  39. data/lib/brainzlab-sdk.rb +3 -0
  40. data/lib/brainzlab.rb +140 -0
  41. data/lib/generators/brainzlab/install/install_generator.rb +61 -0
  42. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  43. metadata +159 -0
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ class << self
6
+ def install!
7
+ config = BrainzLab.configuration
8
+
9
+ # HTTP client instrumentation
10
+ if config.instrument_http
11
+ install_net_http!
12
+ install_faraday!
13
+ install_httparty!
14
+ end
15
+
16
+ # Database instrumentation (breadcrumbs for Reflex)
17
+ install_active_record! if config.instrument_active_record
18
+
19
+ # Redis instrumentation
20
+ install_redis! if config.instrument_redis
21
+
22
+ # Background job instrumentation
23
+ install_sidekiq! if config.instrument_sidekiq
24
+
25
+ # GraphQL instrumentation
26
+ install_graphql! if config.instrument_graphql
27
+
28
+ # MongoDB instrumentation
29
+ install_mongodb! if config.instrument_mongodb
30
+
31
+ # Elasticsearch instrumentation
32
+ install_elasticsearch! if config.instrument_elasticsearch
33
+
34
+ # ActionMailer instrumentation
35
+ install_action_mailer! if config.instrument_action_mailer
36
+
37
+ # Delayed::Job instrumentation
38
+ install_delayed_job! if config.instrument_delayed_job
39
+
40
+ # Grape API instrumentation
41
+ install_grape! if config.instrument_grape
42
+ end
43
+
44
+ def install_net_http!
45
+ require_relative "instrumentation/net_http"
46
+ NetHttp.install!
47
+ end
48
+
49
+ def install_faraday!
50
+ return unless defined?(::Faraday)
51
+
52
+ require_relative "instrumentation/faraday"
53
+ FaradayMiddleware.install!
54
+ end
55
+
56
+ def install_httparty!
57
+ return unless defined?(::HTTParty)
58
+
59
+ require_relative "instrumentation/httparty"
60
+ HTTPartyInstrumentation.install!
61
+ end
62
+
63
+ def install_active_record!
64
+ require_relative "instrumentation/active_record"
65
+ ActiveRecord.install!
66
+ end
67
+
68
+ def install_redis!
69
+ return unless defined?(::Redis)
70
+
71
+ require_relative "instrumentation/redis"
72
+ RedisInstrumentation.install!
73
+ end
74
+
75
+ def install_sidekiq!
76
+ return unless defined?(::Sidekiq)
77
+
78
+ require_relative "instrumentation/sidekiq"
79
+ SidekiqInstrumentation.install!
80
+ end
81
+
82
+ def install_graphql!
83
+ return unless defined?(::GraphQL)
84
+
85
+ require_relative "instrumentation/graphql"
86
+ GraphQLInstrumentation.install!
87
+ end
88
+
89
+ def install_mongodb!
90
+ return unless defined?(::Mongo) || defined?(::Mongoid)
91
+
92
+ require_relative "instrumentation/mongodb"
93
+ MongoDBInstrumentation.install!
94
+ end
95
+
96
+ def install_elasticsearch!
97
+ return unless defined?(::Elasticsearch) || defined?(::OpenSearch)
98
+
99
+ require_relative "instrumentation/elasticsearch"
100
+ ElasticsearchInstrumentation.install!
101
+ end
102
+
103
+ def install_action_mailer!
104
+ return unless defined?(::ActionMailer)
105
+
106
+ require_relative "instrumentation/action_mailer"
107
+ ActionMailerInstrumentation.install!
108
+ end
109
+
110
+ def install_delayed_job!
111
+ return unless defined?(::Delayed::Job) || defined?(::Delayed::Backend)
112
+
113
+ require_relative "instrumentation/delayed_job"
114
+ DelayedJobInstrumentation.install!
115
+ end
116
+
117
+ def install_grape!
118
+ return unless defined?(::Grape::API)
119
+
120
+ require_relative "instrumentation/grape"
121
+ GrapeInstrumentation.install!
122
+ end
123
+
124
+ # Manual installation methods for lazy-loaded libraries
125
+ def install_http!
126
+ install_net_http!
127
+ install_faraday!
128
+ install_httparty!
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module BrainzLab
8
+ module Pulse
9
+ class Client
10
+ MAX_RETRIES = 3
11
+ RETRY_DELAY = 0.5
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ @buffer = []
16
+ @mutex = Mutex.new
17
+ @flush_thread = nil
18
+ end
19
+
20
+ def send_trace(payload)
21
+ return unless @config.pulse_enabled && @config.pulse_valid?
22
+
23
+ if @config.pulse_buffer_size > 1
24
+ buffer_trace(payload)
25
+ else
26
+ post("/api/v1/traces", payload)
27
+ end
28
+ end
29
+
30
+ def send_batch(payloads)
31
+ return unless @config.pulse_enabled && @config.pulse_valid?
32
+ return if payloads.empty?
33
+
34
+ post("/api/v1/traces/batch", { traces: payloads })
35
+ end
36
+
37
+ def send_metric(payload)
38
+ return unless @config.pulse_enabled && @config.pulse_valid?
39
+
40
+ post("/api/v1/metrics", payload)
41
+ end
42
+
43
+ def flush
44
+ traces_to_send = nil
45
+
46
+ @mutex.synchronize do
47
+ return if @buffer.empty?
48
+
49
+ traces_to_send = @buffer.dup
50
+ @buffer.clear
51
+ end
52
+
53
+ send_batch(traces_to_send) if traces_to_send&.any?
54
+ end
55
+
56
+ private
57
+
58
+ def buffer_trace(payload)
59
+ should_flush = false
60
+
61
+ @mutex.synchronize do
62
+ @buffer << payload
63
+ should_flush = @buffer.size >= @config.pulse_buffer_size
64
+ end
65
+
66
+ start_flush_timer unless @flush_thread&.alive?
67
+ flush if should_flush
68
+ end
69
+
70
+ def start_flush_timer
71
+ @flush_thread = Thread.new do
72
+ loop do
73
+ sleep(@config.pulse_flush_interval)
74
+ flush
75
+ end
76
+ end
77
+ end
78
+
79
+ def post(path, body)
80
+ uri = URI.join(@config.pulse_url, path)
81
+ request = Net::HTTP::Post.new(uri)
82
+ request["Content-Type"] = "application/json"
83
+ request["Authorization"] = "Bearer #{@config.pulse_auth_key}"
84
+ request["User-Agent"] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
85
+ request.body = JSON.generate(body)
86
+
87
+ execute_with_retry(uri, request)
88
+ rescue StandardError => e
89
+ log_error("Failed to send to Pulse: #{e.message}")
90
+ nil
91
+ end
92
+
93
+ def execute_with_retry(uri, request)
94
+ retries = 0
95
+ begin
96
+ http = Net::HTTP.new(uri.host, uri.port)
97
+ http.use_ssl = uri.scheme == "https"
98
+ http.open_timeout = 5
99
+ http.read_timeout = 10
100
+
101
+ response = http.request(request)
102
+
103
+ case response.code.to_i
104
+ when 200..299
105
+ JSON.parse(response.body) rescue {}
106
+ when 429, 500..599
107
+ raise RetryableError, "Server error: #{response.code}"
108
+ else
109
+ log_error("Pulse API error: #{response.code} - #{response.body}")
110
+ nil
111
+ end
112
+ rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
113
+ retries += 1
114
+ if retries <= MAX_RETRIES
115
+ sleep(RETRY_DELAY * retries)
116
+ retry
117
+ end
118
+ log_error("Failed after #{MAX_RETRIES} retries: #{e.message}")
119
+ nil
120
+ end
121
+ end
122
+
123
+ def log_error(message)
124
+ return unless @config.logger
125
+
126
+ @config.logger.error("[BrainzLab::Pulse] #{message}")
127
+ end
128
+
129
+ class RetryableError < StandardError; end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Pulse
5
+ class Instrumentation
6
+ class << self
7
+ def install!
8
+ return unless BrainzLab.configuration.pulse_enabled
9
+
10
+ install_active_record!
11
+ install_action_view!
12
+ install_active_support_cache!
13
+ install_action_controller!
14
+ install_http_clients!
15
+ install_active_job!
16
+ install_action_cable!
17
+ end
18
+
19
+ private
20
+
21
+ # Track SQL queries
22
+ def install_active_record!
23
+ return unless defined?(ActiveRecord)
24
+
25
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
26
+ event = ActiveSupport::Notifications::Event.new(*args)
27
+ next if skip_query?(event.payload)
28
+
29
+ record_span(
30
+ name: event.payload[:name] || "SQL",
31
+ kind: "db",
32
+ started_at: event.time,
33
+ ended_at: event.end,
34
+ duration_ms: event.duration,
35
+ data: {
36
+ sql: truncate_sql(event.payload[:sql]),
37
+ name: event.payload[:name],
38
+ cached: event.payload[:cached] || false
39
+ }
40
+ )
41
+ end
42
+ end
43
+
44
+ # Track view rendering
45
+ def install_action_view!
46
+ return unless defined?(ActionView)
47
+
48
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |*args|
49
+ event = ActiveSupport::Notifications::Event.new(*args)
50
+
51
+ record_span(
52
+ name: short_path(event.payload[:identifier]),
53
+ kind: "render",
54
+ started_at: event.time,
55
+ ended_at: event.end,
56
+ duration_ms: event.duration,
57
+ data: {
58
+ identifier: event.payload[:identifier],
59
+ layout: event.payload[:layout]
60
+ }
61
+ )
62
+ end
63
+
64
+ ActiveSupport::Notifications.subscribe("render_partial.action_view") do |*args|
65
+ event = ActiveSupport::Notifications::Event.new(*args)
66
+
67
+ record_span(
68
+ name: short_path(event.payload[:identifier]),
69
+ kind: "render",
70
+ started_at: event.time,
71
+ ended_at: event.end,
72
+ duration_ms: event.duration,
73
+ data: {
74
+ identifier: event.payload[:identifier],
75
+ partial: true
76
+ }
77
+ )
78
+ end
79
+
80
+ ActiveSupport::Notifications.subscribe("render_collection.action_view") do |*args|
81
+ event = ActiveSupport::Notifications::Event.new(*args)
82
+
83
+ record_span(
84
+ name: short_path(event.payload[:identifier]),
85
+ kind: "render",
86
+ started_at: event.time,
87
+ ended_at: event.end,
88
+ duration_ms: event.duration,
89
+ data: {
90
+ identifier: event.payload[:identifier],
91
+ count: event.payload[:count],
92
+ collection: true
93
+ }
94
+ )
95
+ end
96
+ end
97
+
98
+ # Track cache operations
99
+ def install_active_support_cache!
100
+ %w[cache_read.active_support cache_write.active_support cache_delete.active_support].each do |event_name|
101
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
102
+ event = ActiveSupport::Notifications::Event.new(*args)
103
+ operation = event_name.split(".").first.sub("cache_", "")
104
+
105
+ record_span(
106
+ name: "Cache #{operation}",
107
+ kind: "cache",
108
+ started_at: event.time,
109
+ ended_at: event.end,
110
+ duration_ms: event.duration,
111
+ data: {
112
+ key: truncate_key(event.payload[:key]),
113
+ hit: event.payload[:hit],
114
+ operation: operation
115
+ }
116
+ )
117
+ end
118
+ end
119
+ end
120
+
121
+ # Track controller processing for timing breakdown
122
+ def install_action_controller!
123
+ return unless defined?(ActionController)
124
+
125
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
126
+ event = ActiveSupport::Notifications::Event.new(*args)
127
+ payload = event.payload
128
+
129
+ # Store timing breakdown in thread local for the middleware
130
+ Thread.current[:brainzlab_pulse_breakdown] = {
131
+ view_ms: payload[:view_runtime]&.round(2),
132
+ db_ms: payload[:db_runtime]&.round(2)
133
+ }
134
+ end
135
+ end
136
+
137
+ # Track external HTTP requests
138
+ def install_http_clients!
139
+ # Net::HTTP instrumentation
140
+ if defined?(Net::HTTP)
141
+ ActiveSupport::Notifications.subscribe("request.net_http") do |*args|
142
+ event = ActiveSupport::Notifications::Event.new(*args)
143
+
144
+ record_span(
145
+ name: "HTTP #{event.payload[:method]} #{event.payload[:host]}",
146
+ kind: "http",
147
+ started_at: event.time,
148
+ ended_at: event.end,
149
+ duration_ms: event.duration,
150
+ data: {
151
+ method: event.payload[:method],
152
+ host: event.payload[:host],
153
+ path: event.payload[:path],
154
+ status: event.payload[:code]
155
+ }
156
+ )
157
+ end
158
+ end
159
+
160
+ # Faraday instrumentation
161
+ if defined?(Faraday)
162
+ ActiveSupport::Notifications.subscribe("request.faraday") do |*args|
163
+ event = ActiveSupport::Notifications::Event.new(*args)
164
+ env = event.payload[:env]
165
+ next unless env
166
+
167
+ record_span(
168
+ name: "HTTP #{env.method.to_s.upcase} #{env.url.host}",
169
+ kind: "http",
170
+ started_at: event.time,
171
+ ended_at: event.end,
172
+ duration_ms: event.duration,
173
+ data: {
174
+ method: env.method.to_s.upcase,
175
+ host: env.url.host,
176
+ path: env.url.path,
177
+ status: env.status
178
+ }
179
+ )
180
+ end
181
+ end
182
+ end
183
+
184
+ # Track ActiveJob/SolidQueue
185
+ def install_active_job!
186
+ return unless defined?(ActiveJob)
187
+
188
+ # Track job enqueuing
189
+ ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*args|
190
+ event = ActiveSupport::Notifications::Event.new(*args)
191
+ job = event.payload[:job]
192
+
193
+ record_span(
194
+ name: "Enqueue #{job.class.name}",
195
+ kind: "job",
196
+ started_at: event.time,
197
+ ended_at: event.end,
198
+ duration_ms: event.duration,
199
+ data: {
200
+ job_class: job.class.name,
201
+ job_id: job.job_id,
202
+ queue: job.queue_name
203
+ }
204
+ )
205
+ end
206
+
207
+ # Track job retry
208
+ ActiveSupport::Notifications.subscribe("retry_stopped.active_job") do |*args|
209
+ event = ActiveSupport::Notifications::Event.new(*args)
210
+ job = event.payload[:job]
211
+ error = event.payload[:error]
212
+
213
+ record_span(
214
+ name: "Retry stopped #{job.class.name}",
215
+ kind: "job",
216
+ started_at: event.time,
217
+ ended_at: event.end,
218
+ duration_ms: event.duration,
219
+ error: true,
220
+ error_class: error&.class&.name,
221
+ error_message: error&.message,
222
+ data: {
223
+ job_class: job.class.name,
224
+ job_id: job.job_id,
225
+ queue: job.queue_name,
226
+ executions: job.executions
227
+ }
228
+ )
229
+ end
230
+
231
+ # Track job discard
232
+ ActiveSupport::Notifications.subscribe("discard.active_job") do |*args|
233
+ event = ActiveSupport::Notifications::Event.new(*args)
234
+ job = event.payload[:job]
235
+ error = event.payload[:error]
236
+
237
+ record_span(
238
+ name: "Discarded #{job.class.name}",
239
+ kind: "job",
240
+ started_at: event.time,
241
+ ended_at: event.end,
242
+ duration_ms: event.duration,
243
+ error: true,
244
+ error_class: error&.class&.name,
245
+ error_message: error&.message,
246
+ data: {
247
+ job_class: job.class.name,
248
+ job_id: job.job_id,
249
+ queue: job.queue_name,
250
+ executions: job.executions
251
+ }
252
+ )
253
+ end
254
+ end
255
+
256
+ # Track ActionCable/SolidCable
257
+ def install_action_cable!
258
+ return unless defined?(ActionCable)
259
+
260
+ ActiveSupport::Notifications.subscribe("perform_action.action_cable") do |*args|
261
+ event = ActiveSupport::Notifications::Event.new(*args)
262
+
263
+ record_span(
264
+ name: "Cable #{event.payload[:channel_class]}##{event.payload[:action]}",
265
+ kind: "cable",
266
+ started_at: event.time,
267
+ ended_at: event.end,
268
+ duration_ms: event.duration,
269
+ data: {
270
+ channel: event.payload[:channel_class],
271
+ action: event.payload[:action]
272
+ }
273
+ )
274
+ end
275
+
276
+ ActiveSupport::Notifications.subscribe("transmit.action_cable") do |*args|
277
+ event = ActiveSupport::Notifications::Event.new(*args)
278
+
279
+ record_span(
280
+ name: "Cable transmit #{event.payload[:channel_class]}",
281
+ kind: "cable",
282
+ started_at: event.time,
283
+ ended_at: event.end,
284
+ duration_ms: event.duration,
285
+ data: {
286
+ channel: event.payload[:channel_class],
287
+ via: event.payload[:via]
288
+ }
289
+ )
290
+ end
291
+
292
+ ActiveSupport::Notifications.subscribe("broadcast.action_cable") do |*args|
293
+ event = ActiveSupport::Notifications::Event.new(*args)
294
+
295
+ record_span(
296
+ name: "Cable broadcast #{event.payload[:broadcasting]}",
297
+ kind: "cable",
298
+ started_at: event.time,
299
+ ended_at: event.end,
300
+ duration_ms: event.duration,
301
+ data: {
302
+ broadcasting: event.payload[:broadcasting],
303
+ coder: event.payload[:coder]
304
+ }
305
+ )
306
+ end
307
+ end
308
+
309
+ def record_span(name:, kind:, started_at:, ended_at:, duration_ms:, error: false, error_class: nil, error_message: nil, data: {})
310
+ spans = Thread.current[:brainzlab_pulse_spans]
311
+ return unless spans
312
+
313
+ span = {
314
+ span_id: SecureRandom.uuid,
315
+ name: name,
316
+ kind: kind,
317
+ started_at: started_at,
318
+ ended_at: ended_at,
319
+ duration_ms: duration_ms.round(2),
320
+ data: data.compact
321
+ }
322
+
323
+ if error
324
+ span[:error] = true
325
+ span[:error_class] = error_class
326
+ span[:error_message] = error_message
327
+ end
328
+
329
+ spans << span
330
+ end
331
+
332
+ def skip_query?(payload)
333
+ # Skip SCHEMA queries and internal Rails queries
334
+ return true if payload[:name] == "SCHEMA"
335
+ return true if payload[:name]&.start_with?("EXPLAIN")
336
+ return true if payload[:sql]&.include?("pg_")
337
+ return true if payload[:sql]&.include?("information_schema")
338
+ return true if payload[:cached] && !include_cached_queries?
339
+
340
+ false
341
+ end
342
+
343
+ def include_cached_queries?
344
+ false
345
+ end
346
+
347
+ def truncate_sql(sql)
348
+ return nil unless sql
349
+ sql.to_s[0, 1000]
350
+ end
351
+
352
+ def truncate_key(key)
353
+ return nil unless key
354
+ key.to_s[0, 200]
355
+ end
356
+
357
+ def short_path(path)
358
+ return nil unless path
359
+ path.to_s.split("/").last(2).join("/")
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end