brainzlab 0.1.0 → 0.1.2

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +30 -0
  4. data/lib/brainzlab/beacon/client.rb +209 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +341 -3
  8. data/lib/brainzlab/cortex/cache.rb +59 -0
  9. data/lib/brainzlab/cortex/client.rb +141 -0
  10. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  11. data/lib/brainzlab/cortex.rb +227 -0
  12. data/lib/brainzlab/dendrite/client.rb +232 -0
  13. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  14. data/lib/brainzlab/dendrite.rb +195 -0
  15. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  16. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  17. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  18. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  19. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  20. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  21. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  22. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  23. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  24. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  25. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  26. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  27. data/lib/brainzlab/devtools.rb +75 -0
  28. data/lib/brainzlab/flux/buffer.rb +96 -0
  29. data/lib/brainzlab/flux/client.rb +70 -0
  30. data/lib/brainzlab/flux/provisioner.rb +57 -0
  31. data/lib/brainzlab/flux.rb +174 -0
  32. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  33. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  34. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  35. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  36. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  37. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  38. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  39. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  40. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  41. data/lib/brainzlab/instrumentation.rb +72 -0
  42. data/lib/brainzlab/nerve/client.rb +217 -0
  43. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  44. data/lib/brainzlab/nerve.rb +219 -0
  45. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  46. data/lib/brainzlab/pulse/propagation.rb +1 -1
  47. data/lib/brainzlab/pulse/tracer.rb +1 -1
  48. data/lib/brainzlab/pulse.rb +1 -1
  49. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  50. data/lib/brainzlab/rails/railtie.rb +36 -3
  51. data/lib/brainzlab/recall/provisioner.rb +17 -0
  52. data/lib/brainzlab/recall.rb +6 -1
  53. data/lib/brainzlab/reflex.rb +20 -5
  54. data/lib/brainzlab/sentinel/client.rb +218 -0
  55. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  56. data/lib/brainzlab/sentinel.rb +165 -0
  57. data/lib/brainzlab/signal/client.rb +62 -0
  58. data/lib/brainzlab/signal/provisioner.rb +55 -0
  59. data/lib/brainzlab/signal.rb +136 -0
  60. data/lib/brainzlab/synapse/client.rb +290 -0
  61. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  62. data/lib/brainzlab/synapse.rb +270 -0
  63. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  64. data/lib/brainzlab/utilities/health_check.rb +296 -0
  65. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  66. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  67. data/lib/brainzlab/utilities.rb +17 -0
  68. data/lib/brainzlab/vault/cache.rb +80 -0
  69. data/lib/brainzlab/vault/client.rb +198 -0
  70. data/lib/brainzlab/vault/provisioner.rb +49 -0
  71. data/lib/brainzlab/vault.rb +268 -0
  72. data/lib/brainzlab/version.rb +1 -1
  73. data/lib/brainzlab/vision/client.rb +128 -0
  74. data/lib/brainzlab/vision/provisioner.rb +136 -0
  75. data/lib/brainzlab/vision.rb +157 -0
  76. data/lib/brainzlab.rb +101 -0
  77. metadata +62 -2
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nerve/client"
4
+ require_relative "nerve/provisioner"
5
+
6
+ module BrainzLab
7
+ module Nerve
8
+ class << self
9
+ # Report a completed job
10
+ # @param job_class [String] Job class name
11
+ # @param job_id [String] Job ID
12
+ # @param queue [String] Queue name
13
+ # @param started_at [Time] When job started
14
+ # @param ended_at [Time] When job ended (defaults to now)
15
+ # @param attributes [Hash] Additional attributes
16
+ #
17
+ # @example
18
+ # BrainzLab::Nerve.report_success(
19
+ # job_class: "ProcessOrderJob",
20
+ # job_id: "abc-123",
21
+ # queue: "default",
22
+ # started_at: 1.minute.ago
23
+ # )
24
+ #
25
+ def report_success(job_class:, job_id:, queue:, started_at:, ended_at: Time.now, **attributes)
26
+ return false unless enabled?
27
+
28
+ ensure_provisioned!
29
+ return false unless BrainzLab.configuration.nerve_valid?
30
+
31
+ client.report_job(
32
+ job_class: job_class,
33
+ job_id: job_id,
34
+ queue: queue,
35
+ status: "completed",
36
+ started_at: started_at,
37
+ ended_at: ended_at,
38
+ **attributes
39
+ )
40
+ end
41
+
42
+ # Report a failed job
43
+ def report_failure(job_class:, job_id:, queue:, error:, started_at: nil, **attributes)
44
+ return false unless enabled?
45
+
46
+ ensure_provisioned!
47
+ return false unless BrainzLab.configuration.nerve_valid?
48
+
49
+ client.report_failure(
50
+ job_class: job_class,
51
+ job_id: job_id,
52
+ queue: queue,
53
+ error_class: error.class.name,
54
+ error_message: error.message,
55
+ backtrace: error.backtrace,
56
+ started_at: started_at,
57
+ **attributes
58
+ )
59
+ end
60
+
61
+ # Report a job that's currently running
62
+ def report_started(job_class:, job_id:, queue:, **attributes)
63
+ return false unless enabled?
64
+
65
+ ensure_provisioned!
66
+ return false unless BrainzLab.configuration.nerve_valid?
67
+
68
+ client.report_job(
69
+ job_class: job_class,
70
+ job_id: job_id,
71
+ queue: queue,
72
+ status: "running",
73
+ started_at: Time.now,
74
+ ended_at: Time.now,
75
+ **attributes
76
+ )
77
+ end
78
+
79
+ # Get job statistics
80
+ # @param queue [String] Filter by queue (optional)
81
+ # @param job_class [String] Filter by job class (optional)
82
+ # @param period [String] Time period: "1h", "24h", "7d", "30d"
83
+ def stats(queue: nil, job_class: nil, period: "1h")
84
+ return nil unless enabled?
85
+
86
+ ensure_provisioned!
87
+ return nil unless BrainzLab.configuration.nerve_valid?
88
+
89
+ client.stats(queue: queue, job_class: job_class, period: period)
90
+ end
91
+
92
+ # List recent jobs
93
+ def jobs(queue: nil, status: nil, limit: 100)
94
+ return [] unless enabled?
95
+
96
+ ensure_provisioned!
97
+ return [] unless BrainzLab.configuration.nerve_valid?
98
+
99
+ client.list_jobs(queue: queue, status: status, limit: limit)
100
+ end
101
+
102
+ # List all queues
103
+ def queues
104
+ return [] unless enabled?
105
+
106
+ ensure_provisioned!
107
+ return [] unless BrainzLab.configuration.nerve_valid?
108
+
109
+ client.list_queues
110
+ end
111
+
112
+ # Get queue details
113
+ def queue(name)
114
+ return nil unless enabled?
115
+
116
+ ensure_provisioned!
117
+ return nil unless BrainzLab.configuration.nerve_valid?
118
+
119
+ client.get_queue(name)
120
+ end
121
+
122
+ # Retry a failed job
123
+ def retry(job_id)
124
+ return false unless enabled?
125
+
126
+ ensure_provisioned!
127
+ return false unless BrainzLab.configuration.nerve_valid?
128
+
129
+ client.retry_job(job_id)
130
+ end
131
+
132
+ # Delete a job
133
+ def delete(job_id)
134
+ return false unless enabled?
135
+
136
+ ensure_provisioned!
137
+ return false unless BrainzLab.configuration.nerve_valid?
138
+
139
+ client.delete_job(job_id)
140
+ end
141
+
142
+ # Report queue metrics (for custom job backends)
143
+ def report_metrics(queue:, size:, latency_ms: nil, workers: nil)
144
+ return false unless enabled?
145
+
146
+ ensure_provisioned!
147
+ return false unless BrainzLab.configuration.nerve_valid?
148
+
149
+ client.report_metrics(
150
+ queue: queue,
151
+ size: size,
152
+ latency_ms: latency_ms,
153
+ workers: workers
154
+ )
155
+ end
156
+
157
+ # Track a job execution (block helper)
158
+ # @example
159
+ # BrainzLab::Nerve.track(job_class: "MyJob", job_id: "123", queue: "default") do
160
+ # # job work
161
+ # end
162
+ #
163
+ def track(job_class:, job_id:, queue: "default", **attributes)
164
+ started_at = Time.now
165
+
166
+ begin
167
+ result = yield
168
+ report_success(
169
+ job_class: job_class,
170
+ job_id: job_id,
171
+ queue: queue,
172
+ started_at: started_at,
173
+ **attributes
174
+ )
175
+ result
176
+ rescue StandardError => e
177
+ report_failure(
178
+ job_class: job_class,
179
+ job_id: job_id,
180
+ queue: queue,
181
+ error: e,
182
+ started_at: started_at,
183
+ **attributes
184
+ )
185
+ raise
186
+ end
187
+ end
188
+
189
+ # === INTERNAL ===
190
+
191
+ def ensure_provisioned!
192
+ return if @provisioned
193
+
194
+ @provisioned = true
195
+ provisioner.ensure_project!
196
+ end
197
+
198
+ def provisioner
199
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
200
+ end
201
+
202
+ def client
203
+ @client ||= Client.new(BrainzLab.configuration)
204
+ end
205
+
206
+ def reset!
207
+ @client = nil
208
+ @provisioner = nil
209
+ @provisioned = false
210
+ end
211
+
212
+ private
213
+
214
+ def enabled?
215
+ BrainzLab.configuration.nerve_enabled
216
+ end
217
+ end
218
+ end
219
+ end
@@ -26,6 +26,7 @@ module BrainzLab
26
26
  event = ActiveSupport::Notifications::Event.new(*args)
27
27
  next if skip_query?(event.payload)
28
28
 
29
+ sql = event.payload[:sql]
29
30
  record_span(
30
31
  name: event.payload[:name] || "SQL",
31
32
  kind: "db",
@@ -33,9 +34,11 @@ module BrainzLab
33
34
  ended_at: event.end,
34
35
  duration_ms: event.duration,
35
36
  data: {
36
- sql: truncate_sql(event.payload[:sql]),
37
+ sql: truncate_sql(sql),
37
38
  name: event.payload[:name],
38
- cached: event.payload[:cached] || false
39
+ cached: event.payload[:cached] || false,
40
+ table: extract_table(sql),
41
+ operation: extract_operation(sql)
39
42
  }
40
43
  )
41
44
  end
@@ -358,6 +361,36 @@ module BrainzLab
358
361
  return nil unless path
359
362
  path.to_s.split("/").last(2).join("/")
360
363
  end
364
+
365
+ def extract_table(sql)
366
+ return nil unless sql
367
+
368
+ # Match FROM "table" or FROM table patterns
369
+ # Also handles INSERT INTO, UPDATE, DELETE FROM
370
+ case sql.to_s
371
+ when /\bFROM\s+["'`]?(\w+)["'`]?/i
372
+ Regexp.last_match(1)
373
+ when /\bINTO\s+["'`]?(\w+)["'`]?/i
374
+ Regexp.last_match(1)
375
+ when /\bUPDATE\s+["'`]?(\w+)["'`]?/i
376
+ Regexp.last_match(1)
377
+ when /\bJOIN\s+["'`]?(\w+)["'`]?/i
378
+ Regexp.last_match(1)
379
+ end
380
+ end
381
+
382
+ def extract_operation(sql)
383
+ return nil unless sql
384
+
385
+ case sql.to_s.strip.upcase
386
+ when /\ASELECT/i then "SELECT"
387
+ when /\AINSERT/i then "INSERT"
388
+ when /\AUPDATE/i then "UPDATE"
389
+ when /\ADELETE/i then "DELETE"
390
+ when /\ABEGIN/i, /\ACOMMIT/i, /\AROLLBACK/i then "TRANSACTION"
391
+ else "QUERY"
392
+ end
393
+ end
361
394
  end
362
395
  end
363
396
  end
@@ -31,7 +31,7 @@ module BrainzLab
31
31
  end
32
32
 
33
33
  def valid?
34
- @trace_id && @span_id
34
+ !trace_id.nil? && !span_id.nil?
35
35
  end
36
36
 
37
37
  def to_h
@@ -52,7 +52,7 @@ module BrainzLab
52
52
  ).compact
53
53
 
54
54
  # Add request context if available
55
- ctx = Context.current
55
+ ctx = BrainzLab::Context.current
56
56
  payload[:request_id] ||= ctx.request_id
57
57
  payload[:user_id] ||= ctx.user&.dig(:id)&.to_s
58
58
 
@@ -160,7 +160,7 @@ module BrainzLab
160
160
  end
161
161
 
162
162
  def enabled?
163
- BrainzLab.configuration.pulse_enabled
163
+ BrainzLab.configuration.pulse_effectively_enabled?
164
164
  end
165
165
 
166
166
  def build_trace_payload(name, kind, started_at, ended_at, attributes)
@@ -149,8 +149,7 @@ module BrainzLab
149
149
 
150
150
  sql
151
151
  .gsub(/\b\d+\b/, "?") # Replace numbers
152
- .gsub(/'[^']*'/, "?") # Replace strings
153
- .gsub(/"[^"]*"/, "?") # Replace double-quoted strings
152
+ .gsub(/'[^']*'/, "?") # Replace single-quoted strings
154
153
  .gsub(/\$\d+/, "?") # Replace positional params
155
154
  .gsub(/\/\*.*?\*\//, "") # Remove comments
156
155
  .gsub(/\s+/, " ") # Normalize whitespace
@@ -7,6 +7,17 @@ module BrainzLab
7
7
  require "generators/brainzlab/install/install_generator"
8
8
  end
9
9
 
10
+ # Load Vault secrets early, before configuration
11
+ # This allows secrets to be used in config files
12
+ initializer "brainzlab.load_vault_secrets", before: :load_environment_config do
13
+ if BrainzLab.configuration.vault_enabled && BrainzLab.configuration.vault_auto_load
14
+ BrainzLab.debug_log("[Vault] Auto-loading secrets into ENV...")
15
+ BrainzLab::Vault.load!(
16
+ provider_keys: BrainzLab.configuration.vault_load_provider_keys
17
+ )
18
+ end
19
+ end
20
+
10
21
  initializer "brainzlab.configure_rails_initialization" do |app|
11
22
  # Set defaults from Rails
12
23
  BrainzLab.configure do |config|
@@ -20,11 +31,33 @@ module BrainzLab
20
31
 
21
32
  # Add request context middleware (runs early)
22
33
  app.middleware.insert_after ActionDispatch::RequestId, BrainzLab::Rails::Middleware
34
+
35
+ # Add DevTools middlewares if enabled
36
+ if BrainzLab.configuration.devtools_enabled
37
+ require_relative "../devtools"
38
+
39
+ # Asset server (handles /__brainzlab__/* requests)
40
+ app.middleware.insert_before ActionDispatch::Static, BrainzLab::DevTools::Middleware::AssetServer
41
+
42
+ # Database handler (handles /_brainzlab/devtools/database POST requests)
43
+ # Allows running migrations from the error page
44
+ app.middleware.insert_before ActionDispatch::Static, BrainzLab::DevTools::Middleware::DatabaseHandler
45
+
46
+ # Error page (catches exceptions and renders branded error page)
47
+ # Insert BEFORE DebugExceptions so we can intercept the HTML error page
48
+ # that DebugExceptions renders and replace it with our own
49
+ if defined?(ActionDispatch::DebugExceptions)
50
+ app.middleware.insert_before ActionDispatch::DebugExceptions, BrainzLab::DevTools::Middleware::ErrorPage
51
+ end
52
+
53
+ # Debug panel (injects panel into HTML responses)
54
+ app.middleware.use BrainzLab::DevTools::Middleware::DebugPanel
55
+ end
23
56
  end
24
57
 
25
58
  config.after_initialize do
26
59
  # Set up custom log formatter
27
- setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
60
+ BrainzLab::Rails::Railtie.setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
28
61
 
29
62
  # Install instrumentation (HTTP tracking, etc.)
30
63
  BrainzLab::Instrumentation.install!
@@ -569,9 +602,9 @@ module BrainzLab
569
602
  end
570
603
  end
571
604
 
572
- # Sidekiq error handler
605
+ # Sidekiq error handler - Sidekiq 7.x+ requires 3 arguments
573
606
  class SidekiqErrorHandler
574
- def call(exception, context)
607
+ def call(exception, context, _config = nil)
575
608
  BrainzLab::Reflex.capture(exception,
576
609
  tags: { type: "sidekiq" },
577
610
  extra: {
@@ -37,14 +37,31 @@ module BrainzLab
37
37
  private
38
38
 
39
39
  def should_provision?
40
+ if @config.debug
41
+ log_debug("Checking provision conditions:")
42
+ log_debug(" recall_auto_provision: #{@config.recall_auto_provision}")
43
+ log_debug(" app_name: '#{@config.app_name}'")
44
+ log_debug(" secret_key set: #{@config.secret_key.to_s.strip.length > 0}")
45
+ log_debug(" recall_master_key set: #{@config.recall_master_key.to_s.strip.length > 0}")
46
+ end
47
+
40
48
  return false unless @config.recall_auto_provision
41
49
  return false unless @config.app_name.to_s.strip.length > 0
42
50
  return false if @config.secret_key.to_s.strip.length > 0
43
51
  return false unless @config.recall_master_key.to_s.strip.length > 0
44
52
 
53
+ log_debug("Will provision Recall project") if @config.debug
45
54
  true
46
55
  end
47
56
 
57
+ def log_debug(message)
58
+ if @config.logger
59
+ @config.logger.info("[BrainzLab::Debug] #{message}")
60
+ else
61
+ puts "[BrainzLab::Debug] #{message}"
62
+ end
63
+ end
64
+
48
65
  def provision_project
49
66
  uri = URI.parse("#{@config.recall_url}/api/v1/projects/provision")
50
67
  request = Net::HTTP::Post.new(uri)
@@ -30,7 +30,7 @@ module BrainzLab
30
30
 
31
31
  def log(level, message, **data)
32
32
  config = BrainzLab.configuration
33
- return unless config.recall_enabled
33
+ return unless config.recall_effectively_enabled?
34
34
 
35
35
  # Auto-provision project on first log if app_name is configured
36
36
  ensure_provisioned!
@@ -43,6 +43,11 @@ module BrainzLab
43
43
  end
44
44
 
45
45
  def ensure_provisioned!
46
+ config = BrainzLab.configuration
47
+ if config.debug
48
+ puts "[BrainzLab::Debug] Recall.ensure_provisioned! called, @provisioned=#{@provisioned}"
49
+ end
50
+
46
51
  return if @provisioned
47
52
 
48
53
  @provisioned = true
@@ -77,7 +77,7 @@ module BrainzLab
77
77
  private
78
78
 
79
79
  def enabled?
80
- BrainzLab.configuration.reflex_enabled
80
+ BrainzLab.configuration.reflex_effectively_enabled?
81
81
  end
82
82
 
83
83
  def capture_disabled?
@@ -241,16 +241,27 @@ module BrainzLab
241
241
  end
242
242
 
243
243
  def parse_backtrace_line(line)
244
- # Parse "path/to/file.rb:42:in `method_name'"
245
- if line =~ /\A(.+):(\d+):in `(.+)'\z/
244
+ # Parse various Ruby backtrace formats:
245
+ # - "path/to/file.rb:42:in `method_name'" (backtick + single quote)
246
+ # - "path/to/file.rb:42:in 'method_name'" (single quotes)
247
+ # - "path/to/file.rb:42" (no method)
248
+ if line =~ /\A(.+):(\d+):in [`']([^']+)'?\z/
246
249
  {
247
250
  file: $1,
248
251
  line: $2.to_i,
249
252
  function: $3,
250
253
  in_app: in_app_frame?($1)
251
254
  }
255
+ elsif line =~ /\A(.+):(\d+)\z/
256
+ {
257
+ file: $1,
258
+ line: $2.to_i,
259
+ function: nil,
260
+ in_app: in_app_frame?($1)
261
+ }
252
262
  else
253
- { raw: line }
263
+ # Still store file for display even if format is unexpected
264
+ { file: line, line: nil, function: nil, in_app: false }
254
265
  end
255
266
  end
256
267
 
@@ -258,8 +269,12 @@ module BrainzLab
258
269
  return false if path.nil?
259
270
  return false if path.include?("vendor/")
260
271
  return false if path.include?("/gems/")
272
+ return false if path.include?("/ruby/")
261
273
 
262
- path.start_with?("app/", "lib/", "./app/", "./lib/")
274
+ # Match both relative and absolute paths containing app/ or lib/
275
+ path.start_with?("app/", "lib/", "./app/", "./lib/") ||
276
+ path.include?("/app/") ||
277
+ path.include?("/lib/")
263
278
  end
264
279
 
265
280
  def filter_params(params)