brainzlab 0.1.1 → 0.1.3

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +6 -21
  3. data/README.md +24 -2
  4. data/lib/brainzlab/beacon/client.rb +207 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +372 -32
  8. data/lib/brainzlab/context.rb +2 -3
  9. data/lib/brainzlab/cortex/cache.rb +59 -0
  10. data/lib/brainzlab/cortex/client.rb +139 -0
  11. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  12. data/lib/brainzlab/cortex.rb +223 -0
  13. data/lib/brainzlab/dendrite/client.rb +230 -0
  14. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  15. data/lib/brainzlab/dendrite.rb +195 -0
  16. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  17. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  18. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  19. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  20. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  21. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  22. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  23. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  24. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  25. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  26. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  27. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  28. data/lib/brainzlab/devtools.rb +75 -0
  29. data/lib/brainzlab/flux/buffer.rb +96 -0
  30. data/lib/brainzlab/flux/client.rb +68 -0
  31. data/lib/brainzlab/flux/provisioner.rb +57 -0
  32. data/lib/brainzlab/flux.rb +174 -0
  33. data/lib/brainzlab/instrumentation/action_mailer.rb +14 -13
  34. data/lib/brainzlab/instrumentation/active_record.rb +28 -13
  35. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  36. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  37. data/lib/brainzlab/instrumentation/delayed_job.rb +27 -29
  38. data/lib/brainzlab/instrumentation/elasticsearch.rb +23 -24
  39. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  40. data/lib/brainzlab/instrumentation/faraday.rb +3 -4
  41. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  42. data/lib/brainzlab/instrumentation/grape.rb +24 -24
  43. data/lib/brainzlab/instrumentation/graphql.rb +24 -23
  44. data/lib/brainzlab/instrumentation/httparty.rb +13 -14
  45. data/lib/brainzlab/instrumentation/mongodb.rb +7 -7
  46. data/lib/brainzlab/instrumentation/net_http.rb +6 -6
  47. data/lib/brainzlab/instrumentation/redis.rb +14 -21
  48. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  49. data/lib/brainzlab/instrumentation/sidekiq.rb +29 -28
  50. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  51. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  52. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  53. data/lib/brainzlab/instrumentation.rb +84 -12
  54. data/lib/brainzlab/nerve/client.rb +215 -0
  55. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  56. data/lib/brainzlab/nerve.rb +219 -0
  57. data/lib/brainzlab/pulse/client.rb +15 -11
  58. data/lib/brainzlab/pulse/instrumentation.rb +90 -53
  59. data/lib/brainzlab/pulse/propagation.rb +29 -29
  60. data/lib/brainzlab/pulse/provisioner.rb +12 -12
  61. data/lib/brainzlab/pulse/tracer.rb +4 -4
  62. data/lib/brainzlab/pulse.rb +14 -14
  63. data/lib/brainzlab/rails/log_formatter.rb +127 -121
  64. data/lib/brainzlab/rails/log_subscriber.rb +70 -77
  65. data/lib/brainzlab/rails/railtie.rb +96 -86
  66. data/lib/brainzlab/recall/buffer.rb +1 -1
  67. data/lib/brainzlab/recall/client.rb +14 -10
  68. data/lib/brainzlab/recall/logger.rb +16 -18
  69. data/lib/brainzlab/recall/provisioner.rb +29 -12
  70. data/lib/brainzlab/recall.rb +14 -11
  71. data/lib/brainzlab/reflex/breadcrumbs.rb +2 -2
  72. data/lib/brainzlab/reflex/client.rb +14 -10
  73. data/lib/brainzlab/reflex/provisioner.rb +12 -12
  74. data/lib/brainzlab/reflex.rb +31 -31
  75. data/lib/brainzlab/sentinel/client.rb +216 -0
  76. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  77. data/lib/brainzlab/sentinel.rb +165 -0
  78. data/lib/brainzlab/signal/client.rb +60 -0
  79. data/lib/brainzlab/signal/provisioner.rb +55 -0
  80. data/lib/brainzlab/signal.rb +136 -0
  81. data/lib/brainzlab/synapse/client.rb +288 -0
  82. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  83. data/lib/brainzlab/synapse.rb +270 -0
  84. data/lib/brainzlab/utilities/circuit_breaker.rb +261 -0
  85. data/lib/brainzlab/utilities/health_check.rb +294 -0
  86. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  87. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  88. data/lib/brainzlab/utilities.rb +17 -0
  89. data/lib/brainzlab/vault/cache.rb +80 -0
  90. data/lib/brainzlab/vault/client.rb +196 -0
  91. data/lib/brainzlab/vault/provisioner.rb +49 -0
  92. data/lib/brainzlab/vault.rb +262 -0
  93. data/lib/brainzlab/version.rb +1 -1
  94. data/lib/brainzlab/vision/client.rb +128 -0
  95. data/lib/brainzlab/vision/provisioner.rb +136 -0
  96. data/lib/brainzlab/vision.rb +155 -0
  97. data/lib/brainzlab-sdk.rb +1 -1
  98. data/lib/brainzlab.rb +112 -13
  99. data/lib/generators/brainzlab/install/install_generator.rb +29 -27
  100. metadata +60 -1
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Flux
5
+ class Buffer
6
+ MAX_EVENTS = 100
7
+ MAX_METRICS = 100
8
+ FLUSH_INTERVAL = 5 # seconds
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @events = []
13
+ @metrics = []
14
+ @mutex = Mutex.new
15
+ @last_flush = Time.now
16
+
17
+ start_flush_thread
18
+ end
19
+
20
+ def add(type, data)
21
+ @mutex.synchronize do
22
+ case type
23
+ when :event
24
+ @events << data
25
+ when :metric
26
+ @metrics << data
27
+ end
28
+
29
+ flush_if_needed
30
+ end
31
+ end
32
+
33
+ def flush!
34
+ events_to_send = nil
35
+ metrics_to_send = nil
36
+
37
+ @mutex.synchronize do
38
+ events_to_send = @events.dup
39
+ metrics_to_send = @metrics.dup
40
+ @events.clear
41
+ @metrics.clear
42
+ @last_flush = Time.now
43
+ end
44
+
45
+ send_batch(events_to_send, metrics_to_send)
46
+ end
47
+
48
+ def size
49
+ @mutex.synchronize { @events.size + @metrics.size }
50
+ end
51
+
52
+ private
53
+
54
+ def flush_if_needed
55
+ should_flush = @events.size >= MAX_EVENTS ||
56
+ @metrics.size >= MAX_METRICS ||
57
+ Time.now - @last_flush >= FLUSH_INTERVAL
58
+
59
+ flush_async if should_flush
60
+ end
61
+
62
+ def flush_async
63
+ events_to_send = @events.dup
64
+ metrics_to_send = @metrics.dup
65
+ @events.clear
66
+ @metrics.clear
67
+ @last_flush = Time.now
68
+
69
+ Thread.new do
70
+ send_batch(events_to_send, metrics_to_send)
71
+ end
72
+ end
73
+
74
+ def send_batch(events, metrics)
75
+ return if events.empty? && metrics.empty?
76
+
77
+ @client.send_batch(events: events, metrics: metrics)
78
+ rescue StandardError => e
79
+ BrainzLab.debug("[Flux] Batch send failed: #{e.message}")
80
+ end
81
+
82
+ def start_flush_thread
83
+ Thread.new do
84
+ loop do
85
+ sleep FLUSH_INTERVAL
86
+ begin
87
+ flush! if size.positive?
88
+ rescue StandardError => e
89
+ BrainzLab.debug("[Flux] Flush thread error: #{e.message}")
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BrainzLab
8
+ module Flux
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def send_event(event)
15
+ post('/api/v1/events', event)
16
+ end
17
+
18
+ def send_events(events)
19
+ post('/api/v1/events/batch', { events: events })
20
+ end
21
+
22
+ def send_metric(metric)
23
+ post('/api/v1/metrics', metric)
24
+ end
25
+
26
+ def send_metrics(metrics)
27
+ post('/api/v1/metrics/batch', { metrics: metrics })
28
+ end
29
+
30
+ def send_batch(events:, metrics:)
31
+ post('/api/v1/flux/batch', { events: events, metrics: metrics })
32
+ end
33
+
34
+ private
35
+
36
+ def post(path, body)
37
+ uri = URI.parse("#{base_url}#{path}")
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = uri.scheme == 'https'
40
+ http.open_timeout = 5
41
+ http.read_timeout = 10
42
+
43
+ request = Net::HTTP::Post.new(uri.path)
44
+ request['Content-Type'] = 'application/json'
45
+ request['Authorization'] = "Bearer #{api_key}"
46
+ request['User-Agent'] = "brainzlab-sdk/#{BrainzLab::VERSION}"
47
+ request.body = body.to_json
48
+
49
+ response = http.request(request)
50
+
51
+ BrainzLab.debug("[Flux] Request failed: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
52
+
53
+ response
54
+ rescue StandardError => e
55
+ BrainzLab.debug("[Flux] Request error: #{e.message}")
56
+ nil
57
+ end
58
+
59
+ def base_url
60
+ @config.flux_url
61
+ end
62
+
63
+ def api_key
64
+ @config.flux_ingest_key || @config.flux_api_key
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BrainzLab
8
+ module Flux
9
+ class Provisioner
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def ensure_project!
15
+ return if (@config.flux_ingest_key && !@config.flux_ingest_key.to_s.empty?) ||
16
+ (@config.flux_api_key && !@config.flux_api_key.to_s.empty?)
17
+ return unless @config.flux_url && !@config.flux_url.to_s.empty?
18
+ return unless @config.secret_key && !@config.secret_key.to_s.empty?
19
+
20
+ BrainzLab.debug_log('[Flux] Auto-provisioning project...')
21
+ provision_project
22
+ end
23
+
24
+ private
25
+
26
+ def provision_project
27
+ uri = URI.parse("#{@config.flux_url}/api/v1/projects/provision")
28
+ http = Net::HTTP.new(uri.host, uri.port)
29
+ http.use_ssl = uri.scheme == 'https'
30
+ http.open_timeout = 10
31
+ http.read_timeout = 30
32
+
33
+ request = Net::HTTP::Post.new(uri.path)
34
+ request['Content-Type'] = 'application/json'
35
+ request['Authorization'] = "Bearer #{@config.secret_key}"
36
+ request['User-Agent'] = "brainzlab-sdk/#{BrainzLab::VERSION}"
37
+ request.body = {
38
+ name: @config.service || 'default',
39
+ environment: @config.environment
40
+ }.to_json
41
+
42
+ response = http.request(request)
43
+
44
+ if response.is_a?(Net::HTTPSuccess)
45
+ data = JSON.parse(response.body)
46
+ @config.flux_ingest_key = data['ingest_key']
47
+ @config.flux_api_key = data['api_key']
48
+ BrainzLab.debug_log('[Flux] Project provisioned successfully')
49
+ else
50
+ BrainzLab.debug_log("[Flux] Provisioning failed: #{response.code} - #{response.body}")
51
+ end
52
+ rescue StandardError => e
53
+ BrainzLab.debug_log("[Flux] Provisioning error: #{e.message}")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'flux/client'
4
+ require_relative 'flux/buffer'
5
+ require_relative 'flux/provisioner'
6
+
7
+ module BrainzLab
8
+ module Flux
9
+ class << self
10
+ # === EVENTS ===
11
+
12
+ # Track a custom event
13
+ # @param name [String] Event name (e.g., 'user.signup', 'order.completed')
14
+ # @param properties [Hash] Event properties
15
+ def track(name, properties = {})
16
+ return unless enabled?
17
+
18
+ ensure_provisioned!
19
+ return unless BrainzLab.configuration.flux_valid?
20
+
21
+ event = {
22
+ name: name,
23
+ timestamp: Time.now.utc.iso8601(3),
24
+ properties: properties.except(:user_id, :value, :tags, :session_id),
25
+ user_id: properties[:user_id],
26
+ session_id: properties[:session_id],
27
+ value: properties[:value],
28
+ tags: properties[:tags] || {},
29
+ environment: BrainzLab.configuration.environment,
30
+ service: BrainzLab.configuration.service
31
+ }
32
+
33
+ buffer.add(:event, event)
34
+ end
35
+
36
+ # Track event for a specific user
37
+ def track_for_user(user, name, properties = {})
38
+ user_id = user.respond_to?(:id) ? user.id.to_s : user.to_s
39
+ track(name, properties.merge(user_id: user_id))
40
+ end
41
+
42
+ # === METRICS ===
43
+
44
+ # Gauge: Current value (overwrites)
45
+ def gauge(name, value, tags: {})
46
+ return unless enabled?
47
+
48
+ ensure_provisioned!
49
+ return unless BrainzLab.configuration.flux_valid?
50
+
51
+ metric = {
52
+ type: 'gauge',
53
+ name: name,
54
+ value: value,
55
+ tags: tags,
56
+ timestamp: Time.now.utc.iso8601(3)
57
+ }
58
+
59
+ buffer.add(:metric, metric)
60
+ end
61
+
62
+ # Counter: Increment value
63
+ def increment(name, value = 1, tags: {})
64
+ return unless enabled?
65
+
66
+ ensure_provisioned!
67
+ return unless BrainzLab.configuration.flux_valid?
68
+
69
+ metric = {
70
+ type: 'counter',
71
+ name: name,
72
+ value: value,
73
+ tags: tags,
74
+ timestamp: Time.now.utc.iso8601(3)
75
+ }
76
+
77
+ buffer.add(:metric, metric)
78
+ end
79
+
80
+ # Counter: Decrement value
81
+ def decrement(name, value = 1, tags: {})
82
+ increment(name, -value, tags: tags)
83
+ end
84
+
85
+ # Distribution: Statistical aggregation
86
+ def distribution(name, value, tags: {})
87
+ return unless enabled?
88
+
89
+ ensure_provisioned!
90
+ return unless BrainzLab.configuration.flux_valid?
91
+
92
+ metric = {
93
+ type: 'distribution',
94
+ name: name,
95
+ value: value,
96
+ tags: tags,
97
+ timestamp: Time.now.utc.iso8601(3)
98
+ }
99
+
100
+ buffer.add(:metric, metric)
101
+ end
102
+
103
+ # Set: Unique count (cardinality)
104
+ def set(name, value, tags: {})
105
+ return unless enabled?
106
+
107
+ ensure_provisioned!
108
+ return unless BrainzLab.configuration.flux_valid?
109
+
110
+ metric = {
111
+ type: 'set',
112
+ name: name,
113
+ value: value.to_s,
114
+ tags: tags,
115
+ timestamp: Time.now.utc.iso8601(3)
116
+ }
117
+
118
+ buffer.add(:metric, metric)
119
+ end
120
+
121
+ # === CONVENIENCE METHODS ===
122
+
123
+ # Time a block and record as distribution
124
+ def measure(name, tags: {})
125
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
+ begin
127
+ yield
128
+ ensure
129
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
130
+ distribution(name, duration_ms, tags: tags.merge(unit: 'ms'))
131
+ end
132
+ end
133
+
134
+ # Flush any buffered data immediately
135
+ def flush!
136
+ buffer.flush!
137
+ end
138
+
139
+ # === INTERNAL ===
140
+
141
+ def ensure_provisioned!
142
+ return if @provisioned
143
+
144
+ @provisioned = true
145
+ provisioner.ensure_project!
146
+ end
147
+
148
+ def provisioner
149
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
150
+ end
151
+
152
+ def client
153
+ @client ||= Client.new(BrainzLab.configuration)
154
+ end
155
+
156
+ def buffer
157
+ @buffer ||= Buffer.new(client)
158
+ end
159
+
160
+ def reset!
161
+ @client = nil
162
+ @buffer = nil
163
+ @provisioner = nil
164
+ @provisioned = false
165
+ end
166
+
167
+ private
168
+
169
+ def enabled?
170
+ BrainzLab.configuration.flux_effectively_enabled?
171
+ end
172
+ end
173
+ end
174
+ end
@@ -11,19 +11,19 @@ module BrainzLab
11
11
  return if @installed
12
12
 
13
13
  # Subscribe to deliver notification
14
- ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
14
+ ActiveSupport::Notifications.subscribe('deliver.action_mailer') do |*args|
15
15
  event = ActiveSupport::Notifications::Event.new(*args)
16
16
  record_delivery(event)
17
17
  end
18
18
 
19
19
  # Subscribe to process notification (when mail is being prepared)
20
- ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args|
20
+ ActiveSupport::Notifications.subscribe('process.action_mailer') do |*args|
21
21
  event = ActiveSupport::Notifications::Event.new(*args)
22
22
  record_process(event)
23
23
  end
24
24
 
25
25
  @installed = true
26
- BrainzLab.debug_log("ActionMailer instrumentation installed")
26
+ BrainzLab.debug_log('ActionMailer instrumentation installed')
27
27
  end
28
28
 
29
29
  def installed?
@@ -46,13 +46,13 @@ module BrainzLab
46
46
  mail = payload[:mail]
47
47
  to = sanitize_recipients(mail&.to)
48
48
  subject = mail&.subject
49
- delivery_method = payload[:perform_deliveries] ? "delivered" : "skipped"
49
+ delivery_method = payload[:perform_deliveries] ? 'delivered' : 'skipped'
50
50
 
51
51
  # Add breadcrumb for Reflex
52
52
  if BrainzLab.configuration.reflex_enabled
53
53
  BrainzLab::Reflex.add_breadcrumb(
54
54
  "Mail #{delivery_method}: #{mailer}",
55
- category: "mailer.deliver",
55
+ category: 'mailer.deliver',
56
56
  level: :info,
57
57
  data: {
58
58
  mailer: mailer,
@@ -67,13 +67,13 @@ module BrainzLab
67
67
  # Record span for Pulse
68
68
  record_span(
69
69
  name: "Mail deliver #{mailer}",
70
- kind: "mailer",
70
+ kind: 'mailer',
71
71
  started_at: event.time,
72
72
  ended_at: event.end,
73
73
  duration_ms: duration_ms,
74
74
  data: {
75
75
  mailer: mailer,
76
- action: "deliver",
76
+ action: 'deliver',
77
77
  to: to,
78
78
  subject: truncate_subject(subject),
79
79
  message_id: message_id,
@@ -106,7 +106,7 @@ module BrainzLab
106
106
  if BrainzLab.configuration.reflex_enabled
107
107
  BrainzLab::Reflex.add_breadcrumb(
108
108
  "Mail process: #{mailer}##{action}",
109
- category: "mailer.process",
109
+ category: 'mailer.process',
110
110
  level: :info,
111
111
  data: {
112
112
  mailer: mailer,
@@ -119,7 +119,7 @@ module BrainzLab
119
119
  # Record span for Pulse
120
120
  record_span(
121
121
  name: "Mail process #{mailer}##{action}",
122
- kind: "mailer",
122
+ kind: 'mailer',
123
123
  started_at: event.time,
124
124
  ended_at: event.end,
125
125
  duration_ms: duration_ms,
@@ -152,27 +152,28 @@ module BrainzLab
152
152
 
153
153
  case recipients
154
154
  when Array
155
- recipients.map { |r| mask_email(r) }.join(", ")
155
+ recipients.map { |r| mask_email(r) }.join(', ')
156
156
  else
157
157
  mask_email(recipients.to_s)
158
158
  end
159
159
  end
160
160
 
161
161
  def mask_email(email)
162
- return email unless email.include?("@")
162
+ return email unless email.include?('@')
163
163
 
164
- local, domain = email.split("@", 2)
164
+ local, domain = email.split('@', 2)
165
165
  if local.length > 2
166
166
  "#{local[0..1]}***@#{domain}"
167
167
  else
168
168
  "***@#{domain}"
169
169
  end
170
170
  rescue StandardError
171
- "[email]"
171
+ '[email]'
172
172
  end
173
173
 
174
174
  def truncate_subject(subject)
175
175
  return nil unless subject
176
+
176
177
  subject.to_s[0, 100]
177
178
  end
178
179
  end
@@ -3,7 +3,7 @@
3
3
  module BrainzLab
4
4
  module Instrumentation
5
5
  class ActiveRecord
6
- SCHEMA_QUERIES = ["SCHEMA", "EXPLAIN"].freeze
6
+ SCHEMA_QUERIES = %w[SCHEMA EXPLAIN].freeze
7
7
  INTERNAL_TABLES = %w[pg_ information_schema sqlite_ mysql.].freeze
8
8
 
9
9
  class << self
@@ -11,7 +11,7 @@ module BrainzLab
11
11
  return unless defined?(::ActiveRecord)
12
12
  return if @installed
13
13
 
14
- ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
14
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
15
15
  event = ActiveSupport::Notifications::Event.new(*args)
16
16
  next if skip_query?(event.payload)
17
17
 
@@ -19,7 +19,7 @@ module BrainzLab
19
19
  end
20
20
 
21
21
  @installed = true
22
- BrainzLab.debug_log("ActiveRecord breadcrumbs installed")
22
+ BrainzLab.debug_log('ActiveRecord breadcrumbs installed')
23
23
  end
24
24
 
25
25
  def installed?
@@ -31,7 +31,7 @@ module BrainzLab
31
31
  def record_breadcrumb(event)
32
32
  payload = event.payload
33
33
  sql = payload[:sql]
34
- name = payload[:name] || "SQL"
34
+ name = payload[:name] || 'SQL'
35
35
  duration = event.duration.round(2)
36
36
 
37
37
  # Extract operation type from SQL
@@ -61,7 +61,7 @@ module BrainzLab
61
61
  sql: truncate_sql(sql),
62
62
  duration_ms: duration,
63
63
  cached: payload[:cached] || false,
64
- connection_name: payload[:connection]&.pool&.connection_class&.name
64
+ connection_name: extract_connection_name(payload[:connection])
65
65
  }.compact
66
66
  )
67
67
  rescue StandardError => e
@@ -69,15 +69,15 @@ module BrainzLab
69
69
  end
70
70
 
71
71
  def extract_operation(sql)
72
- return "query" unless sql
72
+ return 'query' unless sql
73
73
 
74
74
  case sql.to_s.strip.upcase
75
- when /\ASELECT/i then "select"
76
- when /\AINSERT/i then "insert"
77
- when /\AUPDATE/i then "update"
78
- when /\ADELETE/i then "delete"
79
- when /\ABEGIN/i, /\ACOMMIT/i, /\AROLLBACK/i then "transaction"
80
- else "query"
75
+ when /\ASELECT/i then 'select'
76
+ when /\AINSERT/i then 'insert'
77
+ when /\AUPDATE/i then 'update'
78
+ when /\ADELETE/i then 'delete'
79
+ when /\ABEGIN/i, /\ACOMMIT/i, /\AROLLBACK/i then 'transaction'
80
+ else 'query'
81
81
  end
82
82
  end
83
83
 
@@ -95,10 +95,25 @@ module BrainzLab
95
95
  false
96
96
  end
97
97
 
98
+ def extract_connection_name(connection)
99
+ return nil unless connection
100
+
101
+ # Rails 8.1+ uses db_config.name on the pool
102
+ # Older versions used connection_class but that's removed in Rails 8.1
103
+ if connection.respond_to?(:pool)
104
+ pool = connection.pool
105
+ pool.db_config.name if pool.respond_to?(:db_config) && pool.db_config.respond_to?(:name)
106
+ elsif connection.respond_to?(:db_config) && connection.db_config.respond_to?(:name)
107
+ connection.db_config.name
108
+ end
109
+ rescue StandardError
110
+ nil
111
+ end
112
+
98
113
  def truncate_sql(sql)
99
114
  return nil unless sql
100
115
 
101
- truncated = sql.to_s.gsub(/\s+/, " ").strip
116
+ truncated = sql.to_s.gsub(/\s+/, ' ').strip
102
117
  if truncated.length > 500
103
118
  "#{truncated[0, 497]}..."
104
119
  else