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,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'testing/event_store'
4
+ require_relative 'testing/helpers'
5
+ require_relative 'testing/matchers'
6
+
7
+ module BrainzLab
8
+ # Testing utilities for BrainzLab SDK
9
+ #
10
+ # Provides helpers for stubbing SDK calls, capturing events,
11
+ # and custom matchers for RSpec/Minitest.
12
+ #
13
+ # @example Usage in RSpec
14
+ # # spec/rails_helper.rb or spec/spec_helper.rb
15
+ # require 'brainzlab/testing'
16
+ #
17
+ # RSpec.configure do |config|
18
+ # config.include BrainzLab::Testing::Helpers
19
+ # end
20
+ #
21
+ # @example Usage in Minitest
22
+ # # test/test_helper.rb
23
+ # require 'brainzlab/testing'
24
+ #
25
+ # class ActiveSupport::TestCase
26
+ # include BrainzLab::Testing::Helpers
27
+ # end
28
+ #
29
+ module Testing
30
+ class << self
31
+ # Global event store for capturing events during tests
32
+ def event_store
33
+ @event_store ||= EventStore.new
34
+ end
35
+
36
+ # Reset the event store (called between tests)
37
+ def reset!
38
+ @event_store = EventStore.new
39
+ end
40
+
41
+ # Check if testing mode is active
42
+ def enabled?
43
+ @enabled == true
44
+ end
45
+
46
+ # Enable testing mode (stubs all SDK calls)
47
+ def enable!
48
+ return if @enabled
49
+
50
+ @enabled = true
51
+ install_stubs!
52
+ end
53
+
54
+ # Disable testing mode
55
+ def disable!
56
+ return unless @enabled
57
+
58
+ @enabled = false
59
+ remove_stubs!
60
+ end
61
+
62
+ private
63
+
64
+ def install_stubs!
65
+ # Stub Flux (events/metrics)
66
+ stub_flux!
67
+
68
+ # Stub Recall (logging)
69
+ stub_recall!
70
+
71
+ # Stub Reflex (error tracking)
72
+ stub_reflex!
73
+
74
+ # Stub Pulse (APM/tracing)
75
+ stub_pulse!
76
+
77
+ # Stub Signal (alerts/notifications)
78
+ stub_signal!
79
+
80
+ # Stub other modules
81
+ stub_beacon!
82
+ stub_nerve!
83
+ stub_dendrite!
84
+ stub_sentinel!
85
+ stub_synapse!
86
+ stub_cortex!
87
+ stub_vault!
88
+ stub_vision!
89
+ end
90
+
91
+ def remove_stubs!
92
+ # Reset all modules to restore original behavior
93
+ BrainzLab.reset_configuration!
94
+ end
95
+
96
+ def stub_flux!
97
+ # Store original methods
98
+ @original_flux_track = BrainzLab::Flux.method(:track)
99
+
100
+ # Replace with capturing versions
101
+ BrainzLab::Flux.define_singleton_method(:track) do |name, properties = {}|
102
+ BrainzLab::Testing.event_store.record_event(name, properties)
103
+ end
104
+
105
+ BrainzLab::Flux.define_singleton_method(:track_for_user) do |user, name, properties = {}|
106
+ user_id = user.respond_to?(:id) ? user.id.to_s : user.to_s
107
+ BrainzLab::Testing.event_store.record_event(name, properties.merge(user_id: user_id))
108
+ end
109
+
110
+ # Stub metrics
111
+ %i[gauge increment decrement distribution histogram timing set].each do |method|
112
+ BrainzLab::Flux.define_singleton_method(method) do |name, value = nil, **opts|
113
+ BrainzLab::Testing.event_store.record_metric(method, name, value, opts)
114
+ end
115
+ end
116
+
117
+ BrainzLab::Flux.define_singleton_method(:measure) do |name, **opts, &block|
118
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
119
+ result = block.call
120
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
121
+ BrainzLab::Testing.event_store.record_metric(:distribution, name, duration_ms, opts.merge(unit: 'ms'))
122
+ result
123
+ end
124
+
125
+ BrainzLab::Flux.define_singleton_method(:flush!) { true }
126
+ end
127
+
128
+ def stub_recall!
129
+ %i[debug info warn error fatal].each do |level|
130
+ BrainzLab::Recall.define_singleton_method(level) do |message, **data|
131
+ BrainzLab::Testing.event_store.record_log(level, message, data)
132
+ end
133
+ end
134
+
135
+ BrainzLab::Recall.define_singleton_method(:log) do |level, message, **data|
136
+ BrainzLab::Testing.event_store.record_log(level, message, data)
137
+ end
138
+
139
+ BrainzLab::Recall.define_singleton_method(:time) do |label, **data, &block|
140
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
141
+ result = block.call
142
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
143
+ BrainzLab::Testing.event_store.record_log(:info, "#{label} (#{duration_ms}ms)", data.merge(duration_ms: duration_ms))
144
+ result
145
+ end
146
+
147
+ BrainzLab::Recall.define_singleton_method(:flush) { true }
148
+ end
149
+
150
+ def stub_reflex!
151
+ BrainzLab::Reflex.define_singleton_method(:capture) do |exception, **context|
152
+ BrainzLab::Testing.event_store.record_error(exception, context)
153
+ end
154
+
155
+ BrainzLab::Reflex.define_singleton_method(:capture_message) do |message, level: :error, **context|
156
+ BrainzLab::Testing.event_store.record_error_message(message, level, context)
157
+ end
158
+ end
159
+
160
+ def stub_pulse!
161
+ # Stub tracing methods
162
+ BrainzLab::Pulse.define_singleton_method(:start_trace) do |name, **opts|
163
+ BrainzLab::Testing.event_store.record_trace(name, opts.merge(action: :start))
164
+ { trace_id: 'test-trace-id-12345', name: name }
165
+ end
166
+
167
+ BrainzLab::Pulse.define_singleton_method(:finish_trace) do |**opts|
168
+ BrainzLab::Testing.event_store.record_trace('finish', opts.merge(action: :finish))
169
+ true
170
+ end
171
+
172
+ BrainzLab::Pulse.define_singleton_method(:span) do |name, **opts, &block|
173
+ BrainzLab::Testing.event_store.record_trace(name, opts.merge(type: :span))
174
+ block&.call
175
+ end
176
+
177
+ BrainzLab::Pulse.define_singleton_method(:record_trace) do |name, **opts|
178
+ BrainzLab::Testing.event_store.record_trace(name, opts)
179
+ true
180
+ end
181
+
182
+ BrainzLab::Pulse.define_singleton_method(:record_span) do |**opts|
183
+ BrainzLab::Testing.event_store.record_trace(opts[:name], opts)
184
+ true
185
+ end
186
+
187
+ BrainzLab::Pulse.define_singleton_method(:record_metric) do |name, **opts|
188
+ BrainzLab::Testing.event_store.record_metric(opts[:kind] || :gauge, name, opts[:value], tags: opts[:tags] || {})
189
+ true
190
+ end
191
+
192
+ # Stub metric convenience methods
193
+ %i[gauge counter histogram].each do |method|
194
+ BrainzLab::Pulse.define_singleton_method(method) do |name, value, tags: {}|
195
+ BrainzLab::Testing.event_store.record_metric(method, name, value, tags: tags)
196
+ true
197
+ end
198
+ end
199
+
200
+ # Stub distributed tracing methods
201
+ BrainzLab::Pulse.define_singleton_method(:inject) do |headers, **opts|
202
+ headers['traceparent'] = '00-test-trace-id-12345-test-span-id-67890-01'
203
+ headers
204
+ end
205
+
206
+ BrainzLab::Pulse.define_singleton_method(:extract) do |headers|
207
+ BrainzLab::Pulse::Propagation::Context.new(
208
+ trace_id: 'test-trace-id-12345',
209
+ span_id: 'test-span-id-67890'
210
+ ) if headers['traceparent']
211
+ end
212
+
213
+ BrainzLab::Pulse.define_singleton_method(:extract!) do |headers|
214
+ BrainzLab::Pulse.extract(headers)
215
+ end
216
+
217
+ BrainzLab::Pulse.define_singleton_method(:propagation_context) do
218
+ BrainzLab::Pulse::Propagation::Context.new(
219
+ trace_id: 'test-trace-id-12345',
220
+ span_id: 'test-span-id-67890'
221
+ )
222
+ end
223
+
224
+ BrainzLab::Pulse.define_singleton_method(:child_context) do
225
+ BrainzLab::Pulse::Propagation::Context.new(
226
+ trace_id: 'test-trace-id-12345',
227
+ span_id: SecureRandom.hex(8)
228
+ )
229
+ end
230
+ end
231
+
232
+ def stub_signal!
233
+ BrainzLab::Signal.define_singleton_method(:alert) do |name, message, severity: :warning, channels: nil, data: {}|
234
+ BrainzLab::Testing.event_store.record_alert(name, message, severity, channels, data)
235
+ end
236
+
237
+ BrainzLab::Signal.define_singleton_method(:notify) do |channel, message, title: nil, data: {}|
238
+ BrainzLab::Testing.event_store.record_notification(channel, message, title, data)
239
+ end
240
+
241
+ BrainzLab::Signal.define_singleton_method(:trigger) do |rule_name, context = {}|
242
+ BrainzLab::Testing.event_store.record_trigger(rule_name, context)
243
+ end
244
+
245
+ BrainzLab::Signal.define_singleton_method(:test!) { true }
246
+ end
247
+
248
+ def stub_beacon!
249
+ return unless defined?(BrainzLab::Beacon)
250
+
251
+ BrainzLab::Beacon.define_singleton_method(:create_http_monitor) { |*| { id: 'test-monitor-1', status: 'created' } }
252
+ BrainzLab::Beacon.define_singleton_method(:create_ssl_monitor) { |*| { id: 'test-monitor-2', status: 'created' } }
253
+ BrainzLab::Beacon.define_singleton_method(:create_tcp_monitor) { |*| { id: 'test-monitor-3', status: 'created' } }
254
+ BrainzLab::Beacon.define_singleton_method(:create_dns_monitor) { |*| { id: 'test-monitor-4', status: 'created' } }
255
+ BrainzLab::Beacon.define_singleton_method(:list) { [] }
256
+ BrainzLab::Beacon.define_singleton_method(:get) { |_id| nil }
257
+ BrainzLab::Beacon.define_singleton_method(:update) { |*| true }
258
+ BrainzLab::Beacon.define_singleton_method(:delete) { |_id| true }
259
+ BrainzLab::Beacon.define_singleton_method(:pause) { |_id| true }
260
+ BrainzLab::Beacon.define_singleton_method(:resume) { |_id| true }
261
+ BrainzLab::Beacon.define_singleton_method(:history) { |*| [] }
262
+ BrainzLab::Beacon.define_singleton_method(:status) { { status: 'up', monitors: 0 } }
263
+ BrainzLab::Beacon.define_singleton_method(:all_up?) { true }
264
+ BrainzLab::Beacon.define_singleton_method(:incidents) { [] }
265
+ end
266
+
267
+ def stub_nerve!
268
+ return unless defined?(BrainzLab::Nerve)
269
+
270
+ BrainzLab::Nerve.define_singleton_method(:flag) { |*| false }
271
+ BrainzLab::Nerve.define_singleton_method(:enabled?) { |*| false }
272
+ BrainzLab::Nerve.define_singleton_method(:disabled?) { |*| true }
273
+ BrainzLab::Nerve.define_singleton_method(:variation) { |*| nil }
274
+ BrainzLab::Nerve.define_singleton_method(:all_flags) { {} }
275
+ end
276
+
277
+ def stub_dendrite!
278
+ return unless defined?(BrainzLab::Dendrite)
279
+
280
+ BrainzLab::Dendrite.define_singleton_method(:get) { |*| nil }
281
+ BrainzLab::Dendrite.define_singleton_method(:set) { |*| true }
282
+ BrainzLab::Dendrite.define_singleton_method(:delete) { |*| true }
283
+ BrainzLab::Dendrite.define_singleton_method(:all) { {} }
284
+ end
285
+
286
+ def stub_sentinel!
287
+ return unless defined?(BrainzLab::Sentinel)
288
+
289
+ BrainzLab::Sentinel.define_singleton_method(:check) { |*| { allowed: true } }
290
+ BrainzLab::Sentinel.define_singleton_method(:allowed?) { |*| true }
291
+ BrainzLab::Sentinel.define_singleton_method(:denied?) { |*| false }
292
+ end
293
+
294
+ def stub_synapse!
295
+ return unless defined?(BrainzLab::Synapse)
296
+
297
+ BrainzLab::Synapse.define_singleton_method(:publish) { |*| true }
298
+ BrainzLab::Synapse.define_singleton_method(:subscribe) { |*| true }
299
+ end
300
+
301
+ def stub_cortex!
302
+ return unless defined?(BrainzLab::Cortex)
303
+
304
+ BrainzLab::Cortex.define_singleton_method(:read) { |*| nil }
305
+ BrainzLab::Cortex.define_singleton_method(:write) { |*| true }
306
+ BrainzLab::Cortex.define_singleton_method(:delete) { |*| true }
307
+ BrainzLab::Cortex.define_singleton_method(:fetch) { |key, **opts, &block| block&.call }
308
+ end
309
+
310
+ def stub_vault!
311
+ return unless defined?(BrainzLab::Vault)
312
+
313
+ BrainzLab::Vault.define_singleton_method(:get) { |*| nil }
314
+ BrainzLab::Vault.define_singleton_method(:set) { |*| true }
315
+ BrainzLab::Vault.define_singleton_method(:delete) { |*| true }
316
+ end
317
+
318
+ def stub_vision!
319
+ return unless defined?(BrainzLab::Vision)
320
+
321
+ BrainzLab::Vision.define_singleton_method(:track_pageview) { |*| true }
322
+ BrainzLab::Vision.define_singleton_method(:track_event) { |*| true }
323
+ BrainzLab::Vision.define_singleton_method(:identify) { |*| true }
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Utilities
5
+ # Circuit breaker pattern implementation for resilient external calls
6
+ # Integrates with Flux for metrics and Reflex for error tracking
7
+ #
8
+ # States:
9
+ # - :closed - Normal operation, requests pass through
10
+ # - :open - Failing, requests are rejected immediately
11
+ # - :half_open - Testing, limited requests allowed to check recovery
12
+ #
13
+ # @example Basic usage
14
+ # breaker = BrainzLab::Utilities::CircuitBreaker.new(
15
+ # name: "external_api",
16
+ # failure_threshold: 5,
17
+ # recovery_timeout: 30
18
+ # )
19
+ #
20
+ # breaker.call do
21
+ # external_api.request
22
+ # end
23
+ #
24
+ # @example With fallback
25
+ # breaker.call(fallback: -> { cached_value }) do
26
+ # external_api.request
27
+ # end
28
+ #
29
+ class CircuitBreaker
30
+ STATES = %i[closed open half_open].freeze
31
+
32
+ attr_reader :name, :state, :failure_count, :success_count, :last_failure_at
33
+
34
+ def initialize(name:, failure_threshold: 5, success_threshold: 2, recovery_timeout: 30, timeout: nil,
35
+ exclude_exceptions: [])
36
+ @name = name
37
+ @failure_threshold = failure_threshold
38
+ @success_threshold = success_threshold
39
+ @recovery_timeout = recovery_timeout
40
+ @timeout = timeout
41
+ @exclude_exceptions = exclude_exceptions
42
+
43
+ @state = :closed
44
+ @failure_count = 0
45
+ @success_count = 0
46
+ @last_failure_at = nil
47
+ @mutex = Mutex.new
48
+ end
49
+
50
+ # Execute a block with circuit breaker protection
51
+ def call(fallback: nil, &)
52
+ check_state_transition!
53
+
54
+ case @state
55
+ when :open
56
+ track_rejected
57
+ raise CircuitOpenError.new(@name, failure_count: @failure_count, last_failure_at: @last_failure_at) unless fallback
58
+
59
+ fallback.respond_to?(:call) ? fallback.call : fallback
60
+
61
+ when :closed, :half_open
62
+ execute_with_protection(fallback, &)
63
+ end
64
+ end
65
+
66
+ # Force the circuit to a specific state
67
+ def force_state!(new_state)
68
+ unless STATES.include?(new_state)
69
+ raise BrainzLab::ValidationError.new(
70
+ "Invalid circuit breaker state: #{new_state}",
71
+ hint: "Valid states are: #{STATES.join(', ')}",
72
+ code: 'invalid_circuit_state',
73
+ field: 'state',
74
+ context: { provided: new_state, valid_values: STATES }
75
+ )
76
+ end
77
+
78
+ @mutex.synchronize do
79
+ @state = new_state
80
+ @failure_count = 0 if new_state == :closed
81
+ @success_count = 0 if new_state == :half_open
82
+ end
83
+
84
+ track_state_change(new_state)
85
+ end
86
+
87
+ # Reset the circuit breaker
88
+ def reset!
89
+ force_state!(:closed)
90
+ @last_failure_at = nil
91
+ end
92
+
93
+ # Get circuit status
94
+ def status
95
+ {
96
+ name: @name,
97
+ state: @state,
98
+ failure_count: @failure_count,
99
+ success_count: @success_count,
100
+ failure_threshold: @failure_threshold,
101
+ success_threshold: @success_threshold,
102
+ last_failure_at: @last_failure_at,
103
+ recovery_timeout: @recovery_timeout
104
+ }
105
+ end
106
+
107
+ # Check if circuit is allowing requests
108
+ def available?
109
+ check_state_transition!
110
+ @state != :open
111
+ end
112
+
113
+ # Class-level registry of circuit breakers
114
+ class << self
115
+ def registry
116
+ @registry ||= {}
117
+ end
118
+
119
+ def get(name)
120
+ registry[name.to_s]
121
+ end
122
+
123
+ def register(name, **)
124
+ registry[name.to_s] = new(name: name, **)
125
+ end
126
+
127
+ def call(name, **options, &)
128
+ breaker = get(name) || register(name, **options)
129
+ breaker.call(**options.slice(:fallback), &)
130
+ end
131
+
132
+ def reset_all!
133
+ registry.each_value(&:reset!)
134
+ end
135
+
136
+ def status_all
137
+ registry.transform_values(&:status)
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def execute_with_protection(fallback, &)
144
+ result = if @timeout
145
+ Timeout.timeout(@timeout, &)
146
+ else
147
+ yield
148
+ end
149
+
150
+ record_success
151
+ result
152
+ rescue *excluded_exceptions
153
+ # Don't count excluded exceptions as failures
154
+ raise
155
+ rescue StandardError => e
156
+ record_failure(e)
157
+
158
+ raise unless fallback
159
+
160
+ fallback.respond_to?(:call) ? fallback.call : fallback
161
+ end
162
+
163
+ def record_success
164
+ @mutex.synchronize do
165
+ if @state == :half_open
166
+ @success_count += 1
167
+ transition_to(:closed) if @success_count >= @success_threshold
168
+ else
169
+ @failure_count = 0
170
+ end
171
+ end
172
+
173
+ track_success
174
+ end
175
+
176
+ def record_failure(error)
177
+ @mutex.synchronize do
178
+ @failure_count += 1
179
+ @last_failure_at = Time.now
180
+
181
+ if @state == :half_open
182
+ transition_to(:open)
183
+ elsif @failure_count >= @failure_threshold
184
+ transition_to(:open)
185
+ end
186
+ end
187
+
188
+ track_failure(error)
189
+ end
190
+
191
+ def check_state_transition!
192
+ return unless @state == :open && @last_failure_at
193
+
194
+ return unless Time.now - @last_failure_at >= @recovery_timeout
195
+
196
+ @mutex.synchronize do
197
+ transition_to(:half_open) if @state == :open
198
+ end
199
+ end
200
+
201
+ def transition_to(new_state)
202
+ old_state = @state
203
+ @state = new_state
204
+
205
+ case new_state
206
+ when :closed
207
+ @failure_count = 0
208
+ @success_count = 0
209
+ when :half_open
210
+ @success_count = 0
211
+ when :open
212
+ # Keep failure count for debugging
213
+ end
214
+
215
+ track_state_change(new_state, old_state)
216
+ end
217
+
218
+ def excluded_exceptions
219
+ @exclude_exceptions.empty? ? [] : @exclude_exceptions
220
+ end
221
+
222
+ # Metrics tracking
223
+
224
+ def track_success
225
+ return unless BrainzLab.configuration.flux_effectively_enabled?
226
+
227
+ BrainzLab::Flux.increment('circuit_breaker.success', tags: { name: @name, state: @state.to_s })
228
+ end
229
+
230
+ def track_failure(error)
231
+ return unless BrainzLab.configuration.flux_effectively_enabled?
232
+
233
+ BrainzLab::Flux.increment('circuit_breaker.failure', tags: {
234
+ name: @name,
235
+ state: @state.to_s,
236
+ error_class: error.class.name
237
+ })
238
+ end
239
+
240
+ def track_rejected
241
+ return unless BrainzLab.configuration.flux_effectively_enabled?
242
+
243
+ BrainzLab::Flux.increment('circuit_breaker.rejected', tags: { name: @name })
244
+ end
245
+
246
+ def track_state_change(new_state, old_state = nil)
247
+ return unless BrainzLab.configuration.flux_effectively_enabled?
248
+
249
+ BrainzLab::Flux.track('circuit_breaker.state_change', {
250
+ name: @name,
251
+ new_state: new_state.to_s,
252
+ old_state: old_state&.to_s,
253
+ failure_count: @failure_count
254
+ })
255
+
256
+ # Also add breadcrumb for debugging
257
+ BrainzLab::Reflex.add_breadcrumb(
258
+ "Circuit '#{@name}' transitioned to #{new_state}",
259
+ category: 'circuit_breaker',
260
+ level: new_state == :open ? :warning : :info,
261
+ data: { name: @name, old_state: old_state, new_state: new_state }
262
+ )
263
+ end
264
+
265
+ # Error raised when circuit is open
266
+ # Inherits from BrainzLab::ServiceUnavailableError for structured error handling
267
+ class CircuitOpenError < BrainzLab::ServiceUnavailableError
268
+ attr_reader :circuit_name, :failure_count, :last_failure_at
269
+
270
+ def initialize(circuit_name, failure_count: nil, last_failure_at: nil)
271
+ @circuit_name = circuit_name
272
+ @failure_count = failure_count
273
+ @last_failure_at = last_failure_at
274
+
275
+ super(
276
+ "Circuit '#{circuit_name}' is open",
277
+ hint: 'The circuit breaker has tripped due to repeated failures. The service will be retried automatically after the recovery timeout.',
278
+ code: 'circuit_open',
279
+ service_name: circuit_name,
280
+ context: {
281
+ circuit_name: circuit_name,
282
+ failure_count: failure_count,
283
+ last_failure_at: last_failure_at&.iso8601
284
+ }.compact
285
+ )
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end