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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- 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
|