fluyenta-ruby 0.1.14

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,265 @@
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:,
134
+ error:)
135
+ ended_at = Time.now.utc
136
+ duration_ms = ((ended_at - started_at) * 1000).round(2)
137
+
138
+ # Collect spans
139
+ spans = Thread.current[:brainzlab_pulse_spans] || []
140
+ breakdown = Thread.current[:brainzlab_pulse_breakdown] || {}
141
+
142
+ formatted_spans = spans.map do |span|
143
+ {
144
+ span_id: span[:span_id],
145
+ name: span[:name],
146
+ kind: span[:kind],
147
+ started_at: format_timestamp(span[:started_at]),
148
+ ended_at: format_timestamp(span[:ended_at]),
149
+ duration_ms: span[:duration_ms],
150
+ data: span[:data],
151
+ error: span[:error],
152
+ error_class: span[:error_class],
153
+ error_message: span[:error_message]
154
+ }.compact
155
+ end
156
+
157
+ payload = {
158
+ trace_id: SecureRandom.uuid,
159
+ name: job_class,
160
+ kind: 'job',
161
+ started_at: started_at.utc.iso8601(3),
162
+ ended_at: ended_at.utc.iso8601(3),
163
+ duration_ms: duration_ms,
164
+ job_class: job_class,
165
+ job_id: job_id,
166
+ queue: queue,
167
+ queue_wait_ms: queue_wait_ms,
168
+ executions: retry_count + 1,
169
+ db_ms: breakdown[:db_ms],
170
+ error: error.present?,
171
+ error_class: error&.class&.name,
172
+ error_message: error&.message&.slice(0, 1000),
173
+ spans: formatted_spans,
174
+ environment: BrainzLab.configuration.environment,
175
+ commit: BrainzLab.configuration.commit,
176
+ host: BrainzLab.configuration.host
177
+ }
178
+
179
+ # Add parent trace info for distributed tracing
180
+ if parent_context&.valid?
181
+ payload[:parent_trace_id] = parent_context.trace_id
182
+ payload[:parent_span_id] = parent_context.span_id
183
+ end
184
+
185
+ BrainzLab::Pulse.client.send_trace(payload.compact)
186
+ rescue StandardError => e
187
+ BrainzLab.debug_log("Sidekiq trace recording failed: #{e.message}")
188
+ end
189
+
190
+ def format_timestamp(ts)
191
+ return nil unless ts
192
+
193
+ case ts
194
+ when Time, DateTime then ts.utc.iso8601(3)
195
+ when Float, Integer then Time.at(ts).utc.iso8601(3)
196
+ when String then ts
197
+ else ts.to_s
198
+ end
199
+ end
200
+ end
201
+
202
+ # Client middleware - runs when jobs are enqueued
203
+ class ClientMiddleware
204
+ def call(_worker_class, job, queue, _redis_pool)
205
+ # Inject trace context for distributed tracing
206
+ inject_trace_context(job)
207
+
208
+ # Add breadcrumb for job enqueue
209
+ if BrainzLab.configuration.reflex_enabled
210
+ BrainzLab::Reflex.add_breadcrumb(
211
+ "Enqueue #{job['class']}",
212
+ category: 'job.sidekiq.enqueue',
213
+ level: :info,
214
+ data: { queue: queue, job_id: job['jid'] }
215
+ )
216
+ end
217
+
218
+ # Record span for Pulse
219
+ record_enqueue_span(job, queue)
220
+
221
+ yield
222
+ end
223
+
224
+ private
225
+
226
+ def inject_trace_context(job)
227
+ return unless BrainzLab.configuration.pulse_enabled
228
+
229
+ # Get or create propagation context
230
+ ctx = BrainzLab::Pulse::Propagation.current
231
+ ctx ||= BrainzLab::Pulse.send(:create_propagation_context)
232
+
233
+ return unless ctx&.valid?
234
+
235
+ job['_brainzlab_trace'] = {
236
+ 'trace_id' => ctx.trace_id,
237
+ 'span_id' => ctx.span_id,
238
+ 'sampled' => ctx.sampled
239
+ }
240
+ rescue StandardError => e
241
+ BrainzLab.debug_log("Failed to inject Sidekiq trace context: #{e.message}")
242
+ end
243
+
244
+ def record_enqueue_span(job, queue)
245
+ spans = Thread.current[:brainzlab_pulse_spans]
246
+ return unless spans
247
+
248
+ spans << {
249
+ span_id: SecureRandom.uuid,
250
+ name: "Enqueue #{job['class']}",
251
+ kind: 'job',
252
+ started_at: Time.now.utc,
253
+ ended_at: Time.now.utc,
254
+ duration_ms: 0,
255
+ data: {
256
+ job_class: job['class'],
257
+ job_id: job['jid'],
258
+ queue: queue
259
+ }
260
+ }
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module SolidQueueInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::SolidQueue)
9
+
10
+ install_job_instrumentation!
11
+ install_worker_instrumentation!
12
+
13
+ BrainzLab.debug_log('[Instrumentation] SolidQueue instrumentation installed')
14
+ end
15
+
16
+ private
17
+
18
+ def install_job_instrumentation!
19
+ return unless defined?(::ActiveJob::Base)
20
+
21
+ ::ActiveJob::Base.class_eval do
22
+ around_perform do |job, block|
23
+ BrainzLab::Instrumentation::SolidQueueInstrumentation.around_perform(job, &block)
24
+ end
25
+
26
+ around_enqueue do |job, block|
27
+ BrainzLab::Instrumentation::SolidQueueInstrumentation.around_enqueue(job, &block)
28
+ end
29
+ end
30
+ end
31
+
32
+ def install_worker_instrumentation!
33
+ # Subscribe to ActiveSupport notifications for SolidQueue
34
+ return unless defined?(::ActiveSupport::Notifications)
35
+
36
+ ::ActiveSupport::Notifications.subscribe(/solid_queue/) do |name, start, finish, _id, payload|
37
+ handle_notification(name, start, finish, payload)
38
+ end
39
+ end
40
+
41
+ def handle_notification(name, start, finish, payload)
42
+ duration_ms = ((finish - start) * 1000).round(2)
43
+
44
+ case name
45
+ when 'perform.solid_queue'
46
+ track_job_perform(payload, duration_ms)
47
+ when 'enqueue.solid_queue'
48
+ track_job_enqueue(payload)
49
+ when 'discard.solid_queue'
50
+ track_job_discard(payload)
51
+ when 'retry.solid_queue'
52
+ track_job_retry(payload)
53
+ end
54
+ end
55
+
56
+ def track_job_perform(payload, duration_ms)
57
+ job_class = payload[:job]&.class&.name || payload[:job_class]
58
+ queue = payload[:queue] || 'default'
59
+
60
+ # Track with Pulse
61
+ if BrainzLab.configuration.pulse_effectively_enabled?
62
+ BrainzLab::Pulse.record_trace(
63
+ "job.#{job_class}",
64
+ kind: 'job',
65
+ started_at: Time.now - (duration_ms / 1000.0),
66
+ ended_at: Time.now,
67
+ job_class: job_class,
68
+ job_id: payload[:job_id],
69
+ queue: queue,
70
+ executions: payload[:executions] || 1,
71
+ error: payload[:error].present?,
72
+ error_class: payload[:error]&.class&.name,
73
+ error_message: payload[:error]&.message
74
+ )
75
+ end
76
+
77
+ # Track with Flux
78
+ if BrainzLab.configuration.flux_effectively_enabled?
79
+ tags = { job_class: job_class, queue: queue }
80
+ BrainzLab::Flux.distribution('solid_queue.job.duration_ms', duration_ms, tags: tags)
81
+ BrainzLab::Flux.increment('solid_queue.job.processed', tags: tags)
82
+
83
+ BrainzLab::Flux.increment('solid_queue.job.failed', tags: tags) if payload[:error]
84
+ end
85
+
86
+ # Add breadcrumb for Reflex
87
+ BrainzLab::Reflex.add_breadcrumb(
88
+ "Job #{job_class} completed in #{duration_ms}ms",
89
+ category: 'job',
90
+ level: payload[:error] ? :error : :info,
91
+ data: { queue: queue, job_id: payload[:job_id], duration_ms: duration_ms }
92
+ )
93
+ end
94
+
95
+ def track_job_enqueue(payload)
96
+ job_class = payload[:job]&.class&.name || payload[:job_class]
97
+ queue = payload[:queue] || 'default'
98
+
99
+ return unless BrainzLab.configuration.flux_effectively_enabled?
100
+
101
+ BrainzLab::Flux.increment('solid_queue.job.enqueued', tags: { job_class: job_class, queue: queue })
102
+ end
103
+
104
+ def track_job_discard(payload)
105
+ job_class = payload[:job]&.class&.name || payload[:job_class]
106
+
107
+ return unless BrainzLab.configuration.flux_effectively_enabled?
108
+
109
+ BrainzLab::Flux.increment('solid_queue.job.discarded', tags: { job_class: job_class })
110
+ end
111
+
112
+ def track_job_retry(payload)
113
+ job_class = payload[:job]&.class&.name || payload[:job_class]
114
+
115
+ return unless BrainzLab.configuration.flux_effectively_enabled?
116
+
117
+ BrainzLab::Flux.increment('solid_queue.job.retried', tags: { job_class: job_class })
118
+ end
119
+ end
120
+
121
+ def self.around_perform(job)
122
+ job_class = job.class.name
123
+ queue = job.queue_name || 'default'
124
+ started_at = Time.now
125
+
126
+ # Set context for the job
127
+ BrainzLab::Context.current.set_context(
128
+ job_class: job_class,
129
+ job_id: job.job_id,
130
+ queue: queue
131
+ )
132
+
133
+ begin
134
+ yield
135
+ rescue StandardError => e
136
+ # Capture error with Reflex
137
+ if BrainzLab.configuration.reflex_effectively_enabled?
138
+ BrainzLab::Reflex.capture(e,
139
+ tags: { job_class: job_class, queue: queue },
140
+ extra: {
141
+ job_id: job.job_id,
142
+ arguments: safe_arguments(job),
143
+ executions: job.executions
144
+ })
145
+ end
146
+ raise
147
+ ensure
148
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
149
+
150
+ # Record trace
151
+ if BrainzLab.configuration.pulse_effectively_enabled?
152
+ BrainzLab::Pulse.record_trace(
153
+ "job.#{job_class}",
154
+ kind: 'job',
155
+ started_at: started_at,
156
+ ended_at: Time.now,
157
+ job_class: job_class,
158
+ job_id: job.job_id,
159
+ queue: queue,
160
+ executions: job.executions
161
+ )
162
+ end
163
+
164
+ # Record metrics
165
+ if BrainzLab.configuration.flux_effectively_enabled?
166
+ tags = { job_class: job_class, queue: queue }
167
+ BrainzLab::Flux.distribution('solid_queue.job.duration_ms', duration_ms, tags: tags)
168
+ end
169
+
170
+ # Clear context
171
+ BrainzLab.clear_context!
172
+ end
173
+ end
174
+
175
+ def self.around_enqueue(job)
176
+ yield
177
+ rescue StandardError => e
178
+ if BrainzLab.configuration.reflex_effectively_enabled?
179
+ BrainzLab::Reflex.capture(e,
180
+ tags: { job_class: job.class.name, queue: job.queue_name },
181
+ extra: { job_id: job.job_id, arguments: safe_arguments(job) })
182
+ end
183
+ raise
184
+ end
185
+
186
+ def self.safe_arguments(job)
187
+ args = job.arguments
188
+ BrainzLab::Reflex.send(:filter_params, args) if args
189
+ rescue StandardError
190
+ '[Unable to serialize]'
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module StripeInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::Stripe)
9
+
10
+ install_instrumentation!
11
+
12
+ BrainzLab.debug_log('[Instrumentation] Stripe instrumentation installed')
13
+ end
14
+
15
+ private
16
+
17
+ def install_instrumentation!
18
+ # Stripe uses a request callback system
19
+ if ::Stripe.respond_to?(:add_instrumentation)
20
+ ::Stripe.add_instrumentation do |event|
21
+ track_event(event)
22
+ end
23
+ else
24
+ # Fallback: monkey-patch the API resource
25
+ install_api_resource_patch!
26
+ end
27
+ end
28
+
29
+ def install_api_resource_patch!
30
+ return unless defined?(::Stripe::StripeClient)
31
+ return unless ::Stripe::StripeClient.respond_to?(:execute_request)
32
+
33
+ ::Stripe::StripeClient.class_eval do
34
+ class << self
35
+ alias_method :original_execute_request, :execute_request
36
+
37
+ def execute_request(method, path, api_base: nil, api_key: nil, headers: {}, params: {}, usage: [])
38
+ started_at = Time.now
39
+ resource = extract_resource(path)
40
+
41
+ begin
42
+ response = original_execute_request(
43
+ method, path,
44
+ api_base: api_base,
45
+ api_key: api_key,
46
+ headers: headers,
47
+ params: params,
48
+ usage: usage
49
+ )
50
+
51
+ BrainzLab::Instrumentation::StripeInstrumentation.track_success(
52
+ method, resource, path, started_at, response
53
+ )
54
+
55
+ response
56
+ rescue StandardError => e
57
+ BrainzLab::Instrumentation::StripeInstrumentation.track_error(
58
+ method, resource, path, started_at, e
59
+ )
60
+ raise
61
+ end
62
+ end
63
+
64
+ def extract_resource(path)
65
+ # /v1/customers/cus_xxx -> customers
66
+ parts = path.to_s.split('/').reject(&:empty?)
67
+ parts[1] || 'unknown'
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def track_event(event)
74
+ duration_ms = (event[:duration] * 1000).round(2) if event[:duration]
75
+ method = event[:method].to_s.upcase
76
+ resource = event[:path].to_s.split('/')[2] || 'unknown'
77
+
78
+ BrainzLab::Reflex.add_breadcrumb(
79
+ "Stripe #{method} #{resource}",
80
+ category: 'payment',
81
+ level: event[:error] ? :error : :info,
82
+ data: {
83
+ method: method,
84
+ resource: resource,
85
+ status: event[:http_status],
86
+ duration_ms: duration_ms,
87
+ request_id: event[:request_id]
88
+ }
89
+ )
90
+
91
+ return unless BrainzLab.configuration.flux_effectively_enabled?
92
+
93
+ tags = { method: method, resource: resource }
94
+ BrainzLab::Flux.distribution('stripe.duration_ms', duration_ms, tags: tags) if duration_ms
95
+ BrainzLab::Flux.increment('stripe.requests', tags: tags)
96
+
97
+ return unless event[:error]
98
+
99
+ BrainzLab::Flux.increment('stripe.errors', tags: tags.merge(error_type: event[:error_type]))
100
+ end
101
+ end
102
+
103
+ def self.track_success(method, resource, path, started_at, _response)
104
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
105
+
106
+ BrainzLab::Reflex.add_breadcrumb(
107
+ "Stripe #{method.to_s.upcase} #{resource}",
108
+ category: 'payment',
109
+ level: :info,
110
+ data: {
111
+ method: method.to_s.upcase,
112
+ resource: resource,
113
+ path: path,
114
+ duration_ms: duration_ms
115
+ }
116
+ )
117
+
118
+ return unless BrainzLab.configuration.flux_effectively_enabled?
119
+
120
+ tags = { method: method.to_s.upcase, resource: resource }
121
+ BrainzLab::Flux.distribution('stripe.duration_ms', duration_ms, tags: tags)
122
+ BrainzLab::Flux.increment('stripe.requests', tags: tags)
123
+ end
124
+
125
+ def self.track_error(method, resource, _path, started_at, error)
126
+ ((Time.now - started_at) * 1000).round(2)
127
+ error_type = case error
128
+ when Stripe::CardError then 'card_error'
129
+ when Stripe::RateLimitError then 'rate_limit'
130
+ when Stripe::InvalidRequestError then 'invalid_request'
131
+ when Stripe::AuthenticationError then 'authentication'
132
+ when Stripe::APIConnectionError then 'connection'
133
+ when Stripe::StripeError then 'stripe_error'
134
+ else 'unknown'
135
+ end
136
+
137
+ BrainzLab::Reflex.add_breadcrumb(
138
+ "Stripe #{method.to_s.upcase} #{resource} failed: #{error.message}",
139
+ category: 'payment',
140
+ level: :error,
141
+ data: {
142
+ method: method.to_s.upcase,
143
+ resource: resource,
144
+ error_type: error_type,
145
+ error: error.class.name
146
+ }
147
+ )
148
+
149
+ if BrainzLab.configuration.flux_effectively_enabled?
150
+ tags = { method: method.to_s.upcase, resource: resource, error_type: error_type }
151
+ BrainzLab::Flux.increment('stripe.errors', tags: tags)
152
+ end
153
+
154
+ # Capture with Reflex (but filter sensitive data)
155
+ return unless BrainzLab.configuration.reflex_effectively_enabled?
156
+
157
+ BrainzLab::Reflex.capture(error,
158
+ tags: { source: 'stripe', resource: resource },
159
+ extra: { error_type: error_type })
160
+ end
161
+ end
162
+ end
163
+ end