brainzlab 0.1.0 → 0.1.2

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +30 -0
  4. data/lib/brainzlab/beacon/client.rb +209 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +341 -3
  8. data/lib/brainzlab/cortex/cache.rb +59 -0
  9. data/lib/brainzlab/cortex/client.rb +141 -0
  10. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  11. data/lib/brainzlab/cortex.rb +227 -0
  12. data/lib/brainzlab/dendrite/client.rb +232 -0
  13. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  14. data/lib/brainzlab/dendrite.rb +195 -0
  15. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  16. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  17. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  18. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  19. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  20. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  21. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  22. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  23. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  24. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  25. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  26. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  27. data/lib/brainzlab/devtools.rb +75 -0
  28. data/lib/brainzlab/flux/buffer.rb +96 -0
  29. data/lib/brainzlab/flux/client.rb +70 -0
  30. data/lib/brainzlab/flux/provisioner.rb +57 -0
  31. data/lib/brainzlab/flux.rb +174 -0
  32. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  33. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  34. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  35. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  36. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  37. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  38. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  39. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  40. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  41. data/lib/brainzlab/instrumentation.rb +72 -0
  42. data/lib/brainzlab/nerve/client.rb +217 -0
  43. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  44. data/lib/brainzlab/nerve.rb +219 -0
  45. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  46. data/lib/brainzlab/pulse/propagation.rb +1 -1
  47. data/lib/brainzlab/pulse/tracer.rb +1 -1
  48. data/lib/brainzlab/pulse.rb +1 -1
  49. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  50. data/lib/brainzlab/rails/railtie.rb +36 -3
  51. data/lib/brainzlab/recall/provisioner.rb +17 -0
  52. data/lib/brainzlab/recall.rb +6 -1
  53. data/lib/brainzlab/reflex.rb +20 -5
  54. data/lib/brainzlab/sentinel/client.rb +218 -0
  55. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  56. data/lib/brainzlab/sentinel.rb +165 -0
  57. data/lib/brainzlab/signal/client.rb +62 -0
  58. data/lib/brainzlab/signal/provisioner.rb +55 -0
  59. data/lib/brainzlab/signal.rb +136 -0
  60. data/lib/brainzlab/synapse/client.rb +290 -0
  61. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  62. data/lib/brainzlab/synapse.rb +270 -0
  63. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  64. data/lib/brainzlab/utilities/health_check.rb +296 -0
  65. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  66. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  67. data/lib/brainzlab/utilities.rb +17 -0
  68. data/lib/brainzlab/vault/cache.rb +80 -0
  69. data/lib/brainzlab/vault/client.rb +198 -0
  70. data/lib/brainzlab/vault/provisioner.rb +49 -0
  71. data/lib/brainzlab/vault.rb +268 -0
  72. data/lib/brainzlab/version.rb +1 -1
  73. data/lib/brainzlab/vision/client.rb +128 -0
  74. data/lib/brainzlab/vision/provisioner.rb +136 -0
  75. data/lib/brainzlab/vision.rb +157 -0
  76. data/lib/brainzlab.rb +101 -0
  77. metadata +62 -2
@@ -0,0 +1,164 @@
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
+ if BrainzLab.configuration.flux_effectively_enabled?
92
+ tags = { method: method, resource: resource }
93
+ BrainzLab::Flux.distribution("stripe.duration_ms", duration_ms, tags: tags) if duration_ms
94
+ BrainzLab::Flux.increment("stripe.requests", tags: tags)
95
+
96
+ if event[:error]
97
+ BrainzLab::Flux.increment("stripe.errors", tags: tags.merge(error_type: event[:error_type]))
98
+ end
99
+ end
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
+ if BrainzLab.configuration.flux_effectively_enabled?
119
+ tags = { method: method.to_s.upcase, resource: resource }
120
+ BrainzLab::Flux.distribution("stripe.duration_ms", duration_ms, tags: tags)
121
+ BrainzLab::Flux.increment("stripe.requests", tags: tags)
122
+ end
123
+ end
124
+
125
+ def self.track_error(method, resource, path, started_at, error)
126
+ duration_ms = ((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
+ if BrainzLab.configuration.reflex_effectively_enabled?
156
+ BrainzLab::Reflex.capture(error,
157
+ tags: { source: "stripe", resource: resource },
158
+ extra: { error_type: error_type }
159
+ )
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ module TyphoeusInstrumentation
6
+ class << self
7
+ def install!
8
+ return unless defined?(::Typhoeus)
9
+
10
+ install_callbacks!
11
+
12
+ BrainzLab.debug_log("[Instrumentation] Typhoeus instrumentation installed")
13
+ end
14
+
15
+ private
16
+
17
+ def install_callbacks!
18
+ ::Typhoeus.on_complete do |response|
19
+ track_request(response)
20
+ end
21
+ end
22
+
23
+ def track_request(response)
24
+ request = response.request
25
+ return unless request
26
+
27
+ uri = URI.parse(request.base_url) rescue nil
28
+ return unless uri
29
+
30
+ host = uri.host
31
+ return if skip_host?(host)
32
+
33
+ method = (request.options[:method] || :get).to_s.upcase
34
+ path = uri.path.empty? ? "/" : uri.path
35
+ status = response.response_code
36
+ duration_ms = (response.total_time * 1000).round(2)
37
+
38
+ # Add breadcrumb
39
+ BrainzLab::Reflex.add_breadcrumb(
40
+ "HTTP #{method} #{host}#{path} -> #{status}",
41
+ category: "http",
42
+ level: response.success? ? :info : :error,
43
+ data: {
44
+ method: method,
45
+ host: host,
46
+ path: path,
47
+ status: status,
48
+ duration_ms: duration_ms
49
+ }
50
+ )
51
+
52
+ # Track with Flux
53
+ if BrainzLab.configuration.flux_effectively_enabled?
54
+ tags = { host: host, method: method, status: status.to_s }
55
+ BrainzLab::Flux.distribution("http.typhoeus.duration_ms", duration_ms, tags: tags)
56
+ BrainzLab::Flux.increment("http.typhoeus.requests", tags: tags)
57
+
58
+ unless response.success?
59
+ BrainzLab::Flux.increment("http.typhoeus.errors", tags: tags)
60
+ end
61
+
62
+ if response.timed_out?
63
+ BrainzLab::Flux.increment("http.typhoeus.timeouts", tags: { host: host })
64
+ end
65
+ end
66
+ end
67
+
68
+ def skip_host?(host)
69
+ return true unless host
70
+
71
+ ignore_hosts = BrainzLab.configuration.http_ignore_hosts || []
72
+ ignore_hosts.any? { |h| host.include?(h) }
73
+ end
74
+ end
75
+
76
+ # Hydra instrumentation for parallel requests
77
+ module HydraInstrumentation
78
+ def self.install!
79
+ return unless defined?(::Typhoeus::Hydra)
80
+
81
+ ::Typhoeus::Hydra.class_eval do
82
+ alias_method :original_run, :run
83
+
84
+ def run
85
+ started_at = Time.now
86
+ request_count = queued_requests.size
87
+
88
+ result = original_run
89
+
90
+ duration_ms = ((Time.now - started_at) * 1000).round(2)
91
+
92
+ if BrainzLab.configuration.flux_effectively_enabled?
93
+ BrainzLab::Flux.distribution("http.typhoeus.hydra.duration_ms", duration_ms)
94
+ BrainzLab::Flux.distribution("http.typhoeus.hydra.request_count", request_count)
95
+ end
96
+
97
+ result
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -39,6 +39,22 @@ module BrainzLab
39
39
 
40
40
  # Grape API instrumentation
41
41
  install_grape! if config.instrument_grape
42
+
43
+ # Modern job queue instrumentation
44
+ install_solid_queue! if config.instrument_solid_queue
45
+ install_good_job! if config.instrument_good_job
46
+ install_resque! if config.instrument_resque
47
+
48
+ # Additional HTTP clients
49
+ install_excon! if config.instrument_excon
50
+ install_typhoeus! if config.instrument_typhoeus
51
+
52
+ # Caching
53
+ install_dalli! if config.instrument_dalli
54
+
55
+ # Cloud & Payment
56
+ install_aws! if config.instrument_aws
57
+ install_stripe! if config.instrument_stripe
42
58
  end
43
59
 
44
60
  def install_net_http!
@@ -121,6 +137,62 @@ module BrainzLab
121
137
  GrapeInstrumentation.install!
122
138
  end
123
139
 
140
+ def install_solid_queue!
141
+ return unless defined?(::SolidQueue)
142
+
143
+ require_relative "instrumentation/solid_queue"
144
+ SolidQueueInstrumentation.install!
145
+ end
146
+
147
+ def install_good_job!
148
+ return unless defined?(::GoodJob)
149
+
150
+ require_relative "instrumentation/good_job"
151
+ GoodJobInstrumentation.install!
152
+ end
153
+
154
+ def install_resque!
155
+ return unless defined?(::Resque)
156
+
157
+ require_relative "instrumentation/resque"
158
+ ResqueInstrumentation.install!
159
+ end
160
+
161
+ def install_excon!
162
+ return unless defined?(::Excon)
163
+
164
+ require_relative "instrumentation/excon"
165
+ ExconInstrumentation.install!
166
+ end
167
+
168
+ def install_typhoeus!
169
+ return unless defined?(::Typhoeus)
170
+
171
+ require_relative "instrumentation/typhoeus"
172
+ TyphoeusInstrumentation.install!
173
+ end
174
+
175
+ def install_dalli!
176
+ return unless defined?(::Dalli::Client)
177
+
178
+ require_relative "instrumentation/dalli"
179
+ DalliInstrumentation.install!
180
+ end
181
+
182
+ def install_aws!
183
+ return unless defined?(::Aws)
184
+
185
+ require_relative "instrumentation/aws"
186
+ AWSInstrumentation.install!
187
+ end
188
+
189
+ def install_stripe!
190
+ return unless defined?(::Stripe)
191
+
192
+ require_relative "instrumentation/stripe"
193
+ StripeInstrumentation.install!
194
+ end
195
+
124
196
  # Manual installation methods for lazy-loaded libraries
125
197
  def install_http!
126
198
  install_net_http!
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module BrainzLab
8
+ module Nerve
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ @base_url = config.nerve_url || "https://nerve.brainzlab.ai"
13
+ end
14
+
15
+ # Report a job execution
16
+ def report_job(job_class:, job_id:, queue:, status:, started_at:, ended_at:, **attributes)
17
+ response = request(
18
+ :post,
19
+ "/api/v1/jobs",
20
+ body: {
21
+ job_class: job_class,
22
+ job_id: job_id,
23
+ queue: queue,
24
+ status: status,
25
+ started_at: started_at.iso8601(3),
26
+ ended_at: ended_at.iso8601(3),
27
+ duration_ms: ((ended_at - started_at) * 1000).round(2),
28
+ **attributes
29
+ }
30
+ )
31
+
32
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
33
+ rescue StandardError => e
34
+ log_error("report_job", e)
35
+ false
36
+ end
37
+
38
+ # Report a job failure
39
+ def report_failure(job_class:, job_id:, queue:, error_class:, error_message:, backtrace: nil, **attributes)
40
+ response = request(
41
+ :post,
42
+ "/api/v1/jobs/failures",
43
+ body: {
44
+ job_class: job_class,
45
+ job_id: job_id,
46
+ queue: queue,
47
+ error_class: error_class,
48
+ error_message: error_message,
49
+ backtrace: backtrace&.first(20),
50
+ failed_at: Time.now.utc.iso8601(3),
51
+ **attributes
52
+ }
53
+ )
54
+
55
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
56
+ rescue StandardError => e
57
+ log_error("report_failure", e)
58
+ false
59
+ end
60
+
61
+ # Get job statistics
62
+ def stats(queue: nil, job_class: nil, period: "1h")
63
+ params = { period: period }
64
+ params[:queue] = queue if queue
65
+ params[:job_class] = job_class if job_class
66
+
67
+ response = request(:get, "/api/v1/stats", params: params)
68
+
69
+ return nil unless response.is_a?(Net::HTTPSuccess)
70
+
71
+ JSON.parse(response.body, symbolize_names: true)
72
+ rescue StandardError => e
73
+ log_error("stats", e)
74
+ nil
75
+ end
76
+
77
+ # List recent jobs
78
+ def list_jobs(queue: nil, status: nil, limit: 100)
79
+ params = { limit: limit }
80
+ params[:queue] = queue if queue
81
+ params[:status] = status if status
82
+
83
+ response = request(:get, "/api/v1/jobs", params: params)
84
+
85
+ return [] unless response.is_a?(Net::HTTPSuccess)
86
+
87
+ data = JSON.parse(response.body, symbolize_names: true)
88
+ data[:jobs] || []
89
+ rescue StandardError => e
90
+ log_error("list_jobs", e)
91
+ []
92
+ end
93
+
94
+ # List queues
95
+ def list_queues
96
+ response = request(:get, "/api/v1/queues")
97
+
98
+ return [] unless response.is_a?(Net::HTTPSuccess)
99
+
100
+ data = JSON.parse(response.body, symbolize_names: true)
101
+ data[:queues] || []
102
+ rescue StandardError => e
103
+ log_error("list_queues", e)
104
+ []
105
+ end
106
+
107
+ # Get queue details
108
+ def get_queue(name)
109
+ response = request(:get, "/api/v1/queues/#{CGI.escape(name)}")
110
+
111
+ return nil unless response.is_a?(Net::HTTPSuccess)
112
+
113
+ JSON.parse(response.body, symbolize_names: true)
114
+ rescue StandardError => e
115
+ log_error("get_queue", e)
116
+ nil
117
+ end
118
+
119
+ # Retry a failed job
120
+ def retry_job(job_id)
121
+ response = request(:post, "/api/v1/jobs/#{job_id}/retry")
122
+ response.is_a?(Net::HTTPSuccess)
123
+ rescue StandardError => e
124
+ log_error("retry_job", e)
125
+ false
126
+ end
127
+
128
+ # Delete a job
129
+ def delete_job(job_id)
130
+ response = request(:delete, "/api/v1/jobs/#{job_id}")
131
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
132
+ rescue StandardError => e
133
+ log_error("delete_job", e)
134
+ false
135
+ end
136
+
137
+ # Report queue metrics
138
+ def report_metrics(queue:, size:, latency_ms: nil, workers: nil)
139
+ response = request(
140
+ :post,
141
+ "/api/v1/metrics",
142
+ body: {
143
+ queue: queue,
144
+ size: size,
145
+ latency_ms: latency_ms,
146
+ workers: workers,
147
+ timestamp: Time.now.utc.iso8601(3)
148
+ }
149
+ )
150
+
151
+ response.is_a?(Net::HTTPSuccess)
152
+ rescue StandardError => e
153
+ log_error("report_metrics", e)
154
+ false
155
+ end
156
+
157
+ def provision(project_id:, app_name:)
158
+ response = request(
159
+ :post,
160
+ "/api/v1/projects/provision",
161
+ body: { project_id: project_id, app_name: app_name },
162
+ use_service_key: true
163
+ )
164
+
165
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
166
+ rescue StandardError => e
167
+ log_error("provision", e)
168
+ false
169
+ end
170
+
171
+ private
172
+
173
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
174
+ uri = URI.parse("#{@base_url}#{path}")
175
+
176
+ if params
177
+ uri.query = URI.encode_www_form(params)
178
+ end
179
+
180
+ http = Net::HTTP.new(uri.host, uri.port)
181
+ http.use_ssl = uri.scheme == "https"
182
+ http.open_timeout = 5
183
+ http.read_timeout = 10
184
+
185
+ request = case method
186
+ when :get
187
+ Net::HTTP::Get.new(uri)
188
+ when :post
189
+ Net::HTTP::Post.new(uri)
190
+ when :put
191
+ Net::HTTP::Put.new(uri)
192
+ when :delete
193
+ Net::HTTP::Delete.new(uri)
194
+ end
195
+
196
+ request["Content-Type"] = "application/json"
197
+ request["Accept"] = "application/json"
198
+
199
+ if use_service_key
200
+ request["X-Service-Key"] = @config.nerve_master_key || @config.secret_key
201
+ else
202
+ auth_key = @config.nerve_api_key || @config.secret_key
203
+ request["Authorization"] = "Bearer #{auth_key}" if auth_key
204
+ end
205
+
206
+ headers.each { |k, v| request[k] = v }
207
+ request.body = body.to_json if body
208
+
209
+ http.request(request)
210
+ end
211
+
212
+ def log_error(operation, error)
213
+ BrainzLab.debug_log("[Nerve::Client] #{operation} failed: #{error.message}")
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Nerve
5
+ class Provisioner
6
+ def initialize(config)
7
+ @config = config
8
+ @provisioned = false
9
+ end
10
+
11
+ def ensure_project!
12
+ return if @provisioned
13
+ return unless @config.nerve_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ project_id = detect_project_id
19
+ return unless project_id
20
+
21
+ client = Client.new(@config)
22
+ client.provision(
23
+ project_id: project_id,
24
+ app_name: @config.app_name || @config.service
25
+ )
26
+
27
+ BrainzLab.debug_log("[Nerve::Provisioner] Project provisioned: #{project_id}")
28
+ rescue StandardError => e
29
+ BrainzLab.debug_log("[Nerve::Provisioner] Provisioning failed: #{e.message}")
30
+ end
31
+
32
+ private
33
+
34
+ def valid_auth?
35
+ key = @config.nerve_api_key || @config.nerve_master_key || @config.secret_key
36
+ !key.nil? && !key.empty?
37
+ end
38
+
39
+ def detect_project_id
40
+ ENV["BRAINZLAB_PROJECT_ID"]
41
+ end
42
+ end
43
+ end
44
+ end