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,261 @@
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, "Circuit '#{@name}' is open" 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
+ raise ArgumentError, "Invalid state: #{new_state}" unless STATES.include?(new_state)
69
+
70
+ @mutex.synchronize do
71
+ @state = new_state
72
+ @failure_count = 0 if new_state == :closed
73
+ @success_count = 0 if new_state == :half_open
74
+ end
75
+
76
+ track_state_change(new_state)
77
+ end
78
+
79
+ # Reset the circuit breaker
80
+ def reset!
81
+ force_state!(:closed)
82
+ @last_failure_at = nil
83
+ end
84
+
85
+ # Get circuit status
86
+ def status
87
+ {
88
+ name: @name,
89
+ state: @state,
90
+ failure_count: @failure_count,
91
+ success_count: @success_count,
92
+ failure_threshold: @failure_threshold,
93
+ success_threshold: @success_threshold,
94
+ last_failure_at: @last_failure_at,
95
+ recovery_timeout: @recovery_timeout
96
+ }
97
+ end
98
+
99
+ # Check if circuit is allowing requests
100
+ def available?
101
+ check_state_transition!
102
+ @state != :open
103
+ end
104
+
105
+ # Class-level registry of circuit breakers
106
+ class << self
107
+ def registry
108
+ @registry ||= {}
109
+ end
110
+
111
+ def get(name)
112
+ registry[name.to_s]
113
+ end
114
+
115
+ def register(name, **)
116
+ registry[name.to_s] = new(name: name, **)
117
+ end
118
+
119
+ def call(name, **options, &)
120
+ breaker = get(name) || register(name, **options)
121
+ breaker.call(**options.slice(:fallback), &)
122
+ end
123
+
124
+ def reset_all!
125
+ registry.each_value(&:reset!)
126
+ end
127
+
128
+ def status_all
129
+ registry.transform_values(&:status)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def execute_with_protection(fallback, &)
136
+ result = if @timeout
137
+ Timeout.timeout(@timeout, &)
138
+ else
139
+ yield
140
+ end
141
+
142
+ record_success
143
+ result
144
+ rescue *excluded_exceptions
145
+ # Don't count excluded exceptions as failures
146
+ raise
147
+ rescue StandardError => e
148
+ record_failure(e)
149
+
150
+ raise unless fallback
151
+
152
+ fallback.respond_to?(:call) ? fallback.call : fallback
153
+ end
154
+
155
+ def record_success
156
+ @mutex.synchronize do
157
+ if @state == :half_open
158
+ @success_count += 1
159
+ transition_to(:closed) if @success_count >= @success_threshold
160
+ else
161
+ @failure_count = 0
162
+ end
163
+ end
164
+
165
+ track_success
166
+ end
167
+
168
+ def record_failure(error)
169
+ @mutex.synchronize do
170
+ @failure_count += 1
171
+ @last_failure_at = Time.now
172
+
173
+ if @state == :half_open
174
+ transition_to(:open)
175
+ elsif @failure_count >= @failure_threshold
176
+ transition_to(:open)
177
+ end
178
+ end
179
+
180
+ track_failure(error)
181
+ end
182
+
183
+ def check_state_transition!
184
+ return unless @state == :open && @last_failure_at
185
+
186
+ return unless Time.now - @last_failure_at >= @recovery_timeout
187
+
188
+ @mutex.synchronize do
189
+ transition_to(:half_open) if @state == :open
190
+ end
191
+ end
192
+
193
+ def transition_to(new_state)
194
+ old_state = @state
195
+ @state = new_state
196
+
197
+ case new_state
198
+ when :closed
199
+ @failure_count = 0
200
+ @success_count = 0
201
+ when :half_open
202
+ @success_count = 0
203
+ when :open
204
+ # Keep failure count for debugging
205
+ end
206
+
207
+ track_state_change(new_state, old_state)
208
+ end
209
+
210
+ def excluded_exceptions
211
+ @exclude_exceptions.empty? ? [] : @exclude_exceptions
212
+ end
213
+
214
+ # Metrics tracking
215
+
216
+ def track_success
217
+ return unless BrainzLab.configuration.flux_effectively_enabled?
218
+
219
+ BrainzLab::Flux.increment('circuit_breaker.success', tags: { name: @name, state: @state.to_s })
220
+ end
221
+
222
+ def track_failure(error)
223
+ return unless BrainzLab.configuration.flux_effectively_enabled?
224
+
225
+ BrainzLab::Flux.increment('circuit_breaker.failure', tags: {
226
+ name: @name,
227
+ state: @state.to_s,
228
+ error_class: error.class.name
229
+ })
230
+ end
231
+
232
+ def track_rejected
233
+ return unless BrainzLab.configuration.flux_effectively_enabled?
234
+
235
+ BrainzLab::Flux.increment('circuit_breaker.rejected', tags: { name: @name })
236
+ end
237
+
238
+ def track_state_change(new_state, old_state = nil)
239
+ return unless BrainzLab.configuration.flux_effectively_enabled?
240
+
241
+ BrainzLab::Flux.track('circuit_breaker.state_change', {
242
+ name: @name,
243
+ new_state: new_state.to_s,
244
+ old_state: old_state&.to_s,
245
+ failure_count: @failure_count
246
+ })
247
+
248
+ # Also add breadcrumb for debugging
249
+ BrainzLab::Reflex.add_breadcrumb(
250
+ "Circuit '#{@name}' transitioned to #{new_state}",
251
+ category: 'circuit_breaker',
252
+ level: new_state == :open ? :warning : :info,
253
+ data: { name: @name, old_state: old_state, new_state: new_state }
254
+ )
255
+ end
256
+
257
+ # Error raised when circuit is open
258
+ class CircuitOpenError < StandardError; end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Utilities
5
+ # Health check utility for application health endpoints
6
+ # Provides checks for database, cache, queues, and external services
7
+ #
8
+ # @example Basic usage in Rails routes
9
+ # # config/routes.rb
10
+ # mount BrainzLab::Utilities::HealthCheck::Engine => "/health"
11
+ #
12
+ # @example Manual usage
13
+ # result = BrainzLab::Utilities::HealthCheck.run
14
+ # result[:status] # => "healthy" or "unhealthy"
15
+ # result[:checks] # => { database: { status: "ok", latency_ms: 5 }, ... }
16
+ #
17
+ class HealthCheck
18
+ CHECKS = %i[database redis cache queue memory disk].freeze
19
+
20
+ class << self
21
+ # Run all configured health checks
22
+ def run(checks: nil)
23
+ checks_to_run = checks || CHECKS
24
+ results = {}
25
+ overall_healthy = true
26
+
27
+ checks_to_run.each do |check|
28
+ result = send("check_#{check}")
29
+ results[check] = result
30
+ overall_healthy = false if result[:status] != 'ok'
31
+ rescue StandardError => e
32
+ results[check] = { status: 'error', message: e.message }
33
+ overall_healthy = false
34
+ end
35
+
36
+ {
37
+ status: overall_healthy ? 'healthy' : 'unhealthy',
38
+ timestamp: Time.now.utc.iso8601,
39
+ checks: results
40
+ }
41
+ end
42
+
43
+ # Quick check - just returns status
44
+ def healthy?
45
+ result = run
46
+ result[:status] == 'healthy'
47
+ end
48
+
49
+ # Database connectivity check
50
+ def check_database
51
+ return { status: 'skip', message: 'ActiveRecord not loaded' } unless defined?(ActiveRecord::Base)
52
+
53
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ ActiveRecord::Base.connection.execute('SELECT 1')
55
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
56
+
57
+ { status: 'ok', latency_ms: latency }
58
+ rescue StandardError => e
59
+ { status: 'error', message: e.message }
60
+ end
61
+
62
+ # Redis connectivity check
63
+ def check_redis
64
+ return { status: 'skip', message: 'Redis not configured' } unless defined?(Redis)
65
+
66
+ redis = find_redis_connection
67
+ return { status: 'skip', message: 'No Redis connection found' } unless redis
68
+
69
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
70
+ redis.ping
71
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
72
+
73
+ { status: 'ok', latency_ms: latency }
74
+ rescue StandardError => e
75
+ { status: 'error', message: e.message }
76
+ end
77
+
78
+ # Rails cache check
79
+ def check_cache
80
+ return { status: 'skip', message: 'Rails not loaded' } unless defined?(Rails)
81
+
82
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
83
+ key = "brainzlab_health_check_#{SecureRandom.hex(4)}"
84
+ Rails.cache.write(key, 'ok', expires_in: 10.seconds)
85
+ value = Rails.cache.read(key)
86
+ Rails.cache.delete(key)
87
+ latency = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
88
+
89
+ if value == 'ok'
90
+ { status: 'ok', latency_ms: latency }
91
+ else
92
+ { status: 'error', message: 'Cache read/write failed' }
93
+ end
94
+ rescue StandardError => e
95
+ { status: 'error', message: e.message }
96
+ end
97
+
98
+ # Queue system check
99
+ def check_queue
100
+ if defined?(SolidQueue)
101
+ check_solid_queue
102
+ elsif defined?(Sidekiq)
103
+ check_sidekiq
104
+ elsif defined?(GoodJob)
105
+ check_good_job
106
+ else
107
+ { status: 'skip', message: 'No queue system detected' }
108
+ end
109
+ end
110
+
111
+ # Memory usage check
112
+ def check_memory
113
+ mem_info = memory_usage
114
+
115
+ status = if mem_info[:percentage] > 90
116
+ 'warning'
117
+ elsif mem_info[:percentage] > 95
118
+ 'error'
119
+ else
120
+ 'ok'
121
+ end
122
+
123
+ {
124
+ status: status,
125
+ used_mb: mem_info[:used_mb],
126
+ percentage: mem_info[:percentage]
127
+ }
128
+ rescue StandardError => e
129
+ { status: 'error', message: e.message }
130
+ end
131
+
132
+ # Disk space check
133
+ def check_disk
134
+ disk_info = disk_usage
135
+
136
+ status = if disk_info[:percentage] > 90
137
+ 'warning'
138
+ elsif disk_info[:percentage] > 95
139
+ 'error'
140
+ else
141
+ 'ok'
142
+ end
143
+
144
+ {
145
+ status: status,
146
+ used_gb: disk_info[:used_gb],
147
+ available_gb: disk_info[:available_gb],
148
+ percentage: disk_info[:percentage]
149
+ }
150
+ rescue StandardError => e
151
+ { status: 'error', message: e.message }
152
+ end
153
+
154
+ # Register a custom health check
155
+ def register(name, &block)
156
+ custom_checks[name.to_sym] = block
157
+ end
158
+
159
+ def custom_checks
160
+ @custom_checks ||= {}
161
+ end
162
+
163
+ private
164
+
165
+ def find_redis_connection
166
+ # Try common Redis connection sources
167
+ if defined?(Redis.current) && Redis.current
168
+ Redis.current
169
+ elsif defined?(Sidekiq) && Sidekiq.respond_to?(:redis)
170
+ Sidekiq.redis { |conn| return conn }
171
+ elsif defined?(Rails) && Rails.application.config.respond_to?(:redis)
172
+ Rails.application.config.redis
173
+ end
174
+ rescue StandardError
175
+ nil
176
+ end
177
+
178
+ def check_solid_queue
179
+ return { status: 'skip', message: 'SolidQueue not loaded' } unless defined?(SolidQueue)
180
+
181
+ # Check if processes are running
182
+ if defined?(SolidQueue::Process)
183
+ process_count = SolidQueue::Process.where('last_heartbeat_at > ?', 5.minutes.ago).count
184
+ {
185
+ status: process_count.positive? ? 'ok' : 'warning',
186
+ processes: process_count
187
+ }
188
+ else
189
+ { status: 'ok', message: 'SolidQueue configured' }
190
+ end
191
+ rescue StandardError => e
192
+ { status: 'error', message: e.message }
193
+ end
194
+
195
+ def check_sidekiq
196
+ return { status: 'skip', message: 'Sidekiq not loaded' } unless defined?(Sidekiq)
197
+
198
+ stats = Sidekiq::Stats.new
199
+ {
200
+ status: 'ok',
201
+ processed: stats.processed,
202
+ failed: stats.failed,
203
+ queues: stats.queues,
204
+ workers: stats.workers_size
205
+ }
206
+ rescue StandardError => e
207
+ { status: 'error', message: e.message }
208
+ end
209
+
210
+ def check_good_job
211
+ return { status: 'skip', message: 'GoodJob not loaded' } unless defined?(GoodJob)
212
+
213
+ {
214
+ status: 'ok',
215
+ pending: GoodJob::Job.where(performed_at: nil).count,
216
+ running: GoodJob::Job.running.count
217
+ }
218
+ rescue StandardError => e
219
+ { status: 'error', message: e.message }
220
+ end
221
+
222
+ def memory_usage
223
+ # Use /proc/self/status on Linux, ps on macOS
224
+ if File.exist?('/proc/self/status')
225
+ status = File.read('/proc/self/status')
226
+ vm_rss = status.match(/VmRSS:\s+(\d+)\s+kB/)&.captures&.first.to_i
227
+ used_mb = (vm_rss / 1024.0).round(2)
228
+ else
229
+ # macOS fallback
230
+ pid = Process.pid
231
+ output = `ps -o rss= -p #{pid}`.strip
232
+ used_mb = (output.to_i / 1024.0).round(2)
233
+ end
234
+
235
+ # Estimate percentage (based on typical container memory)
236
+ max_mb = ENV.fetch('MEMORY_LIMIT_MB', 512).to_i
237
+ percentage = ((used_mb / max_mb) * 100).round(2)
238
+
239
+ { used_mb: used_mb, percentage: percentage }
240
+ end
241
+
242
+ def disk_usage
243
+ output = `df -k /`.split("\n").last.split
244
+ total = output[1].to_i / 1024 / 1024.0
245
+ used = output[2].to_i / 1024 / 1024.0
246
+ available = output[3].to_i / 1024 / 1024.0
247
+ percentage = ((used / total) * 100).round(2)
248
+
249
+ {
250
+ used_gb: used.round(2),
251
+ available_gb: available.round(2),
252
+ percentage: percentage
253
+ }
254
+ end
255
+ end
256
+
257
+ # Rails Engine for mounting health endpoints
258
+ if defined?(::Rails::Engine)
259
+ class Engine < ::Rails::Engine
260
+ isolate_namespace BrainzLab::Utilities::HealthCheck
261
+
262
+ routes.draw do
263
+ get '/', to: 'health#show'
264
+ get '/live', to: 'health#live'
265
+ get '/ready', to: 'health#ready'
266
+ end
267
+ end
268
+ end
269
+
270
+ # Controller for health endpoints
271
+ if defined?(ActionController::API)
272
+ class HealthController < ActionController::API
273
+ def show
274
+ result = HealthCheck.run
275
+ status = result[:status] == 'healthy' ? :ok : :service_unavailable
276
+ render json: result, status: status
277
+ end
278
+
279
+ def live
280
+ # Liveness probe - just check if the app is running
281
+ render json: { status: 'ok', timestamp: Time.now.utc.iso8601 }
282
+ end
283
+
284
+ def ready
285
+ # Readiness probe - check critical dependencies
286
+ result = HealthCheck.run(checks: %i[database redis])
287
+ status = result[:status] == 'healthy' ? :ok : :service_unavailable
288
+ render json: result, status: status
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
294
+ end