brainzlab 0.1.1 → 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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/lib/brainzlab/beacon/client.rb +209 -0
  4. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  5. data/lib/brainzlab/beacon.rb +215 -0
  6. data/lib/brainzlab/configuration.rb +341 -3
  7. data/lib/brainzlab/cortex/cache.rb +59 -0
  8. data/lib/brainzlab/cortex/client.rb +141 -0
  9. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  10. data/lib/brainzlab/cortex.rb +227 -0
  11. data/lib/brainzlab/dendrite/client.rb +232 -0
  12. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  13. data/lib/brainzlab/dendrite.rb +195 -0
  14. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  15. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  16. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  17. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  18. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  19. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  20. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  21. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  22. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  23. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  24. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  25. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  26. data/lib/brainzlab/devtools.rb +75 -0
  27. data/lib/brainzlab/flux/buffer.rb +96 -0
  28. data/lib/brainzlab/flux/client.rb +70 -0
  29. data/lib/brainzlab/flux/provisioner.rb +57 -0
  30. data/lib/brainzlab/flux.rb +174 -0
  31. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  32. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  33. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  34. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  35. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  36. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  37. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  38. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  39. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  40. data/lib/brainzlab/instrumentation.rb +72 -0
  41. data/lib/brainzlab/nerve/client.rb +217 -0
  42. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  43. data/lib/brainzlab/nerve.rb +219 -0
  44. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  45. data/lib/brainzlab/pulse/propagation.rb +1 -1
  46. data/lib/brainzlab/pulse/tracer.rb +1 -1
  47. data/lib/brainzlab/pulse.rb +1 -1
  48. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  49. data/lib/brainzlab/rails/railtie.rb +36 -3
  50. data/lib/brainzlab/recall/provisioner.rb +17 -0
  51. data/lib/brainzlab/recall.rb +6 -1
  52. data/lib/brainzlab/reflex.rb +2 -2
  53. data/lib/brainzlab/sentinel/client.rb +218 -0
  54. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  55. data/lib/brainzlab/sentinel.rb +165 -0
  56. data/lib/brainzlab/signal/client.rb +62 -0
  57. data/lib/brainzlab/signal/provisioner.rb +55 -0
  58. data/lib/brainzlab/signal.rb +136 -0
  59. data/lib/brainzlab/synapse/client.rb +290 -0
  60. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  61. data/lib/brainzlab/synapse.rb +270 -0
  62. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  63. data/lib/brainzlab/utilities/health_check.rb +296 -0
  64. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  65. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  66. data/lib/brainzlab/utilities.rb +17 -0
  67. data/lib/brainzlab/vault/cache.rb +80 -0
  68. data/lib/brainzlab/vault/client.rb +198 -0
  69. data/lib/brainzlab/vault/provisioner.rb +49 -0
  70. data/lib/brainzlab/vault.rb +268 -0
  71. data/lib/brainzlab/version.rb +1 -1
  72. data/lib/brainzlab/vision/client.rb +128 -0
  73. data/lib/brainzlab/vision/provisioner.rb +136 -0
  74. data/lib/brainzlab/vision.rb +157 -0
  75. data/lib/brainzlab.rb +101 -0
  76. metadata +60 -1
@@ -37,6 +37,70 @@ module BrainzLab
37
37
  :pulse_flush_interval,
38
38
  :pulse_sample_rate,
39
39
  :pulse_excluded_paths,
40
+ :flux_enabled,
41
+ :flux_url,
42
+ :flux_api_key,
43
+ :flux_ingest_key,
44
+ :flux_master_key,
45
+ :flux_auto_provision,
46
+ :flux_buffer_size,
47
+ :flux_flush_interval,
48
+ :signal_enabled,
49
+ :signal_url,
50
+ :signal_api_key,
51
+ :signal_master_key,
52
+ :signal_auto_provision,
53
+ :vault_enabled,
54
+ :vault_url,
55
+ :vault_api_key,
56
+ :vault_master_key,
57
+ :vault_auto_provision,
58
+ :vault_cache_enabled,
59
+ :vault_cache_ttl,
60
+ :vault_auto_load,
61
+ :vault_load_provider_keys,
62
+ :vision_enabled,
63
+ :vision_url,
64
+ :vision_api_key,
65
+ :vision_ingest_key,
66
+ :vision_master_key,
67
+ :vision_auto_provision,
68
+ :vision_default_model,
69
+ :vision_default_browser_provider,
70
+ :cortex_enabled,
71
+ :cortex_url,
72
+ :cortex_api_key,
73
+ :cortex_master_key,
74
+ :cortex_auto_provision,
75
+ :cortex_cache_enabled,
76
+ :cortex_cache_ttl,
77
+ :cortex_default_context,
78
+ :beacon_enabled,
79
+ :beacon_url,
80
+ :beacon_api_key,
81
+ :beacon_master_key,
82
+ :beacon_auto_provision,
83
+ :nerve_enabled,
84
+ :nerve_url,
85
+ :nerve_api_key,
86
+ :nerve_master_key,
87
+ :nerve_auto_provision,
88
+ :dendrite_enabled,
89
+ :dendrite_url,
90
+ :dendrite_api_key,
91
+ :dendrite_master_key,
92
+ :dendrite_auto_provision,
93
+ :sentinel_enabled,
94
+ :sentinel_url,
95
+ :sentinel_api_key,
96
+ :sentinel_agent_key,
97
+ :sentinel_master_key,
98
+ :sentinel_auto_provision,
99
+ :synapse_enabled,
100
+ :synapse_url,
101
+ :synapse_api_key,
102
+ :synapse_master_key,
103
+ :synapse_auto_provision,
40
104
  :scrub_fields,
41
105
  :logger,
42
106
  :instrument_http,
@@ -49,13 +113,39 @@ module BrainzLab
49
113
  :instrument_action_mailer,
50
114
  :instrument_delayed_job,
51
115
  :instrument_grape,
116
+ :instrument_solid_queue,
117
+ :instrument_good_job,
118
+ :instrument_resque,
119
+ :instrument_excon,
120
+ :instrument_typhoeus,
121
+ :instrument_dalli,
122
+ :instrument_aws,
123
+ :instrument_stripe,
52
124
  :http_ignore_hosts,
53
125
  :redis_ignore_commands,
54
126
  :log_formatter_enabled,
55
127
  :log_formatter_colors,
56
128
  :log_formatter_hide_assets,
57
129
  :log_formatter_compact_assets,
58
- :log_formatter_show_params
130
+ :log_formatter_show_params,
131
+ :disable_self_tracking,
132
+ :devtools_enabled,
133
+ :devtools_error_page_enabled,
134
+ :devtools_debug_panel_enabled,
135
+ :devtools_allowed_environments,
136
+ :devtools_allowed_ips,
137
+ :devtools_asset_path,
138
+ :devtools_panel_position,
139
+ :devtools_expand_by_default
140
+
141
+ # Services that should not track themselves to avoid circular dependencies
142
+ SELF_TRACKING_SERVICES = {
143
+ "recall" => :recall_enabled,
144
+ "reflex" => :reflex_enabled,
145
+ "pulse" => :pulse_enabled,
146
+ "flux" => :flux_enabled,
147
+ "signal" => :signal_enabled
148
+ }.freeze
59
149
 
60
150
  def initialize
61
151
  # Authentication
@@ -76,6 +166,10 @@ module BrainzLab
76
166
  # Debug mode - enables verbose logging
77
167
  @debug = ENV["BRAINZLAB_DEBUG"] == "true"
78
168
 
169
+ # Disable self-tracking - prevents services from tracking to themselves
170
+ # e.g., Recall won't log to itself, Reflex won't track errors to itself
171
+ @disable_self_tracking = ENV.fetch("BRAINZLAB_DISABLE_SELF_TRACKING", "true") == "true"
172
+
79
173
  # Recall settings
80
174
  @recall_enabled = true
81
175
  @recall_url = ENV["RECALL_URL"] || "https://recall.brainzlab.ai"
@@ -107,6 +201,90 @@ module BrainzLab
107
201
  @pulse_sample_rate = nil
108
202
  @pulse_excluded_paths = %w[/health /ping /up /assets]
109
203
 
204
+ # Flux settings
205
+ @flux_enabled = true
206
+ @flux_url = ENV["FLUX_URL"] || "https://flux.brainzlab.ai"
207
+ @flux_api_key = ENV["FLUX_API_KEY"]
208
+ @flux_ingest_key = ENV["FLUX_INGEST_KEY"]
209
+ @flux_master_key = ENV["FLUX_MASTER_KEY"]
210
+ @flux_auto_provision = true
211
+ @flux_buffer_size = 100
212
+ @flux_flush_interval = 5
213
+
214
+ # Signal settings
215
+ @signal_enabled = true
216
+ @signal_url = ENV["SIGNAL_URL"] || "https://signal.brainzlab.ai"
217
+ @signal_api_key = ENV["SIGNAL_API_KEY"]
218
+ @signal_master_key = ENV["SIGNAL_MASTER_KEY"]
219
+ @signal_auto_provision = true
220
+
221
+ # Vault settings
222
+ @vault_enabled = true
223
+ @vault_url = ENV["VAULT_URL"] || "https://vault.brainzlab.ai"
224
+ @vault_api_key = ENV["VAULT_API_KEY"]
225
+ @vault_master_key = ENV["VAULT_MASTER_KEY"]
226
+ @vault_auto_provision = true
227
+ @vault_cache_enabled = true
228
+ @vault_cache_ttl = 300 # 5 minutes
229
+ @vault_auto_load = ENV.fetch("VAULT_AUTO_LOAD", "false") == "true" # Auto-load secrets into ENV
230
+ @vault_load_provider_keys = true # Also load provider keys (OpenAI, etc.)
231
+
232
+ # Vision settings (AI browser automation)
233
+ @vision_enabled = true
234
+ @vision_url = ENV["VISION_URL"] || "https://vision.brainzlab.ai"
235
+ @vision_api_key = ENV["VISION_API_KEY"]
236
+ @vision_ingest_key = ENV["VISION_INGEST_KEY"]
237
+ @vision_master_key = ENV["VISION_MASTER_KEY"]
238
+ @vision_auto_provision = true
239
+ @vision_default_model = ENV["VISION_DEFAULT_MODEL"] || "claude-sonnet-4"
240
+ @vision_default_browser_provider = ENV["VISION_DEFAULT_BROWSER_PROVIDER"] || "local"
241
+
242
+ # Cortex settings (feature flags)
243
+ @cortex_enabled = true
244
+ @cortex_url = ENV["CORTEX_URL"] || "https://cortex.brainzlab.ai"
245
+ @cortex_api_key = ENV["CORTEX_API_KEY"]
246
+ @cortex_master_key = ENV["CORTEX_MASTER_KEY"]
247
+ @cortex_auto_provision = true
248
+ @cortex_cache_enabled = true
249
+ @cortex_cache_ttl = 60 # 1 minute
250
+ @cortex_default_context = {}
251
+
252
+ # Beacon settings (uptime monitoring)
253
+ @beacon_enabled = true
254
+ @beacon_url = ENV["BEACON_URL"] || "https://beacon.brainzlab.ai"
255
+ @beacon_api_key = ENV["BEACON_API_KEY"]
256
+ @beacon_master_key = ENV["BEACON_MASTER_KEY"]
257
+ @beacon_auto_provision = true
258
+
259
+ # Nerve settings (job monitoring)
260
+ @nerve_enabled = true
261
+ @nerve_url = ENV["NERVE_URL"] || "https://nerve.brainzlab.ai"
262
+ @nerve_api_key = ENV["NERVE_API_KEY"]
263
+ @nerve_master_key = ENV["NERVE_MASTER_KEY"]
264
+ @nerve_auto_provision = true
265
+
266
+ # Dendrite settings (AI documentation)
267
+ @dendrite_enabled = true
268
+ @dendrite_url = ENV["DENDRITE_URL"] || "https://dendrite.brainzlab.ai"
269
+ @dendrite_api_key = ENV["DENDRITE_API_KEY"]
270
+ @dendrite_master_key = ENV["DENDRITE_MASTER_KEY"]
271
+ @dendrite_auto_provision = true
272
+
273
+ # Sentinel settings (host monitoring)
274
+ @sentinel_enabled = true
275
+ @sentinel_url = ENV["SENTINEL_URL"] || "https://sentinel.brainzlab.ai"
276
+ @sentinel_api_key = ENV["SENTINEL_API_KEY"]
277
+ @sentinel_agent_key = ENV["SENTINEL_AGENT_KEY"]
278
+ @sentinel_master_key = ENV["SENTINEL_MASTER_KEY"]
279
+ @sentinel_auto_provision = true
280
+
281
+ # Synapse settings (AI development orchestration)
282
+ @synapse_enabled = true
283
+ @synapse_url = ENV["SYNAPSE_URL"] || "https://synapse.brainzlab.ai"
284
+ @synapse_api_key = ENV["SYNAPSE_API_KEY"]
285
+ @synapse_master_key = ENV["SYNAPSE_MASTER_KEY"]
286
+ @synapse_auto_provision = true
287
+
110
288
  # Filtering
111
289
  @scrub_fields = %i[password password_confirmation token api_key secret]
112
290
 
@@ -124,6 +302,14 @@ module BrainzLab
124
302
  @instrument_action_mailer = true # ActionMailer instrumentation
125
303
  @instrument_delayed_job = true # Delayed::Job instrumentation
126
304
  @instrument_grape = true # Grape API instrumentation
305
+ @instrument_solid_queue = true # Solid Queue job instrumentation
306
+ @instrument_good_job = true # GoodJob instrumentation
307
+ @instrument_resque = true # Resque instrumentation
308
+ @instrument_excon = true # Excon HTTP client instrumentation
309
+ @instrument_typhoeus = true # Typhoeus HTTP client instrumentation
310
+ @instrument_dalli = true # Dalli/Memcached instrumentation
311
+ @instrument_aws = true # AWS SDK instrumentation
312
+ @instrument_stripe = true # Stripe API instrumentation
127
313
  @http_ignore_hosts = %w[localhost 127.0.0.1]
128
314
  @redis_ignore_commands = %w[ping info] # Commands to skip tracking
129
315
 
@@ -133,6 +319,16 @@ module BrainzLab
133
319
  @log_formatter_hide_assets = false
134
320
  @log_formatter_compact_assets = true
135
321
  @log_formatter_show_params = true
322
+
323
+ # DevTools settings (development error page and debug panel)
324
+ @devtools_enabled = true
325
+ @devtools_error_page_enabled = true
326
+ @devtools_debug_panel_enabled = true
327
+ @devtools_allowed_environments = %w[development test]
328
+ @devtools_allowed_ips = ["127.0.0.1", "::1", "172.16.0.0/12", "192.168.0.0/16", "10.0.0.0/8"]
329
+ @devtools_asset_path = "/__brainzlab__"
330
+ @devtools_panel_position = "bottom-right"
331
+ @devtools_expand_by_default = false
136
332
  end
137
333
 
138
334
  def recall_min_level=(level)
@@ -168,10 +364,150 @@ module BrainzLab
168
364
  pulse_api_key || secret_key
169
365
  end
170
366
 
367
+ def flux_valid?
368
+ key = flux_ingest_key || flux_api_key || secret_key
369
+ !key.nil? && !key.empty?
370
+ end
371
+
372
+ def flux_auth_key
373
+ flux_ingest_key || flux_api_key || secret_key
374
+ end
375
+
376
+ def signal_valid?
377
+ key = signal_api_key || secret_key
378
+ !key.nil? && !key.empty?
379
+ end
380
+
381
+ def signal_auth_key
382
+ signal_api_key || secret_key
383
+ end
384
+
385
+ def vault_valid?
386
+ key = vault_api_key || secret_key
387
+ !key.nil? && !key.empty?
388
+ end
389
+
390
+ def vault_auth_key
391
+ vault_api_key || secret_key
392
+ end
393
+
394
+ def vision_valid?
395
+ key = vision_ingest_key || vision_api_key || secret_key
396
+ !key.nil? && !key.empty?
397
+ end
398
+
399
+ def vision_auth_key
400
+ vision_ingest_key || vision_api_key || secret_key
401
+ end
402
+
403
+ def beacon_valid?
404
+ key = beacon_api_key || secret_key
405
+ !key.nil? && !key.empty?
406
+ end
407
+
408
+ def beacon_auth_key
409
+ beacon_api_key || secret_key
410
+ end
411
+
412
+ def nerve_valid?
413
+ key = nerve_api_key || secret_key
414
+ !key.nil? && !key.empty?
415
+ end
416
+
417
+ def nerve_auth_key
418
+ nerve_api_key || secret_key
419
+ end
420
+
421
+ def cortex_valid?
422
+ key = cortex_api_key || secret_key
423
+ !key.nil? && !key.empty?
424
+ end
425
+
426
+ def cortex_auth_key
427
+ cortex_api_key || secret_key
428
+ end
429
+
430
+ def dendrite_valid?
431
+ key = dendrite_api_key || secret_key
432
+ !key.nil? && !key.empty?
433
+ end
434
+
435
+ def dendrite_auth_key
436
+ dendrite_api_key || secret_key
437
+ end
438
+
439
+ def sentinel_valid?
440
+ key = sentinel_api_key || secret_key
441
+ !key.nil? && !key.empty?
442
+ end
443
+
444
+ def sentinel_auth_key
445
+ sentinel_api_key || secret_key
446
+ end
447
+
448
+ def synapse_valid?
449
+ key = synapse_api_key || secret_key
450
+ !key.nil? && !key.empty?
451
+ end
452
+
453
+ def synapse_auth_key
454
+ synapse_api_key || secret_key
455
+ end
456
+
171
457
  def debug?
172
458
  @debug == true
173
459
  end
174
460
 
461
+ # Check if recall is effectively enabled (considering self-tracking)
462
+ def recall_effectively_enabled?
463
+ return false unless @recall_enabled
464
+ return true unless @disable_self_tracking
465
+
466
+ # Disable if this is the Recall service itself
467
+ normalized_app_name = @app_name.to_s.downcase.strip
468
+ normalized_app_name != "recall"
469
+ end
470
+
471
+ # Check if reflex is effectively enabled (considering self-tracking)
472
+ def reflex_effectively_enabled?
473
+ return false unless @reflex_enabled
474
+ return true unless @disable_self_tracking
475
+
476
+ # Disable if this is the Reflex service itself
477
+ normalized_app_name = @app_name.to_s.downcase.strip
478
+ normalized_app_name != "reflex"
479
+ end
480
+
481
+ # Check if pulse is effectively enabled (considering self-tracking)
482
+ def pulse_effectively_enabled?
483
+ return false unless @pulse_enabled
484
+ return true unless @disable_self_tracking
485
+
486
+ # Disable if this is the Pulse service itself
487
+ normalized_app_name = @app_name.to_s.downcase.strip
488
+ normalized_app_name != "pulse"
489
+ end
490
+
491
+ # Check if flux is effectively enabled (considering self-tracking)
492
+ def flux_effectively_enabled?
493
+ return false unless @flux_enabled
494
+ return true unless @disable_self_tracking
495
+
496
+ # Disable if this is the Flux service itself
497
+ normalized_app_name = @app_name.to_s.downcase.strip
498
+ normalized_app_name != "flux"
499
+ end
500
+
501
+ # Check if signal is effectively enabled (considering self-tracking)
502
+ def signal_effectively_enabled?
503
+ return false unless @signal_enabled
504
+ return true unless @disable_self_tracking
505
+
506
+ # Disable if this is the Signal service itself
507
+ normalized_app_name = @app_name.to_s.downcase.strip
508
+ normalized_app_name != "signal"
509
+ end
510
+
175
511
  def debug_log(message)
176
512
  return unless debug?
177
513
 
@@ -201,13 +537,15 @@ module BrainzLab
201
537
  end
202
538
 
203
539
  def detect_git_commit
204
- `git rev-parse HEAD 2>/dev/null`.strip.presence
540
+ result = `git rev-parse HEAD 2>/dev/null`.strip
541
+ result.empty? ? nil : result
205
542
  rescue StandardError
206
543
  nil
207
544
  end
208
545
 
209
546
  def detect_git_branch
210
- `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip.presence
547
+ result = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
548
+ result.empty? ? nil : result
211
549
  rescue StandardError
212
550
  nil
213
551
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Cortex
5
+ class Cache
6
+ def initialize(ttl = 60)
7
+ @ttl = ttl
8
+ @store = {}
9
+ @timestamps = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def get(key)
14
+ @mutex.synchronize do
15
+ return nil unless @store.key?(key)
16
+ return nil if expired?(key)
17
+
18
+ @store[key]
19
+ end
20
+ end
21
+
22
+ def set(key, value)
23
+ @mutex.synchronize do
24
+ @store[key] = value
25
+ @timestamps[key] = Time.now
26
+ end
27
+ end
28
+
29
+ def has?(key)
30
+ @mutex.synchronize do
31
+ @store.key?(key) && !expired?(key)
32
+ end
33
+ end
34
+
35
+ def delete(key)
36
+ @mutex.synchronize do
37
+ @store.delete(key)
38
+ @timestamps.delete(key)
39
+ end
40
+ end
41
+
42
+ def clear!
43
+ @mutex.synchronize do
44
+ @store.clear
45
+ @timestamps.clear
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def expired?(key)
52
+ timestamp = @timestamps[key]
53
+ return true unless timestamp
54
+
55
+ Time.now - timestamp > @ttl
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "cgi"
7
+
8
+ module BrainzLab
9
+ module Cortex
10
+ class Client
11
+ def initialize(config)
12
+ @config = config
13
+ @base_url = config.cortex_url || "https://cortex.brainzlab.ai"
14
+ end
15
+
16
+ # Evaluate a single flag
17
+ def evaluate(flag_name, context: {})
18
+ response = request(
19
+ :post,
20
+ "/api/v1/evaluate",
21
+ body: {
22
+ flag: flag_name,
23
+ context: context
24
+ }
25
+ )
26
+
27
+ return nil unless response.is_a?(Net::HTTPSuccess)
28
+
29
+ data = JSON.parse(response.body, symbolize_names: true)
30
+ data[:result]
31
+ rescue StandardError => e
32
+ log_error("evaluate", e)
33
+ nil
34
+ end
35
+
36
+ # Evaluate multiple flags at once
37
+ def evaluate_all(context: {})
38
+ response = request(
39
+ :post,
40
+ "/api/v1/evaluate/batch",
41
+ body: { context: context }
42
+ )
43
+
44
+ return {} unless response.is_a?(Net::HTTPSuccess)
45
+
46
+ data = JSON.parse(response.body, symbolize_names: true)
47
+ data[:flags] || {}
48
+ rescue StandardError => e
49
+ log_error("evaluate_all", e)
50
+ {}
51
+ end
52
+
53
+ # List all flags
54
+ def list
55
+ response = request(:get, "/api/v1/flags")
56
+
57
+ return [] unless response.is_a?(Net::HTTPSuccess)
58
+
59
+ data = JSON.parse(response.body, symbolize_names: true)
60
+ data[:flags] || []
61
+ rescue StandardError => e
62
+ log_error("list", e)
63
+ []
64
+ end
65
+
66
+ # Get flag details
67
+ def get_flag(flag_name)
68
+ response = request(:get, "/api/v1/flags/#{CGI.escape(flag_name.to_s)}")
69
+
70
+ return nil unless response.is_a?(Net::HTTPSuccess)
71
+
72
+ JSON.parse(response.body, symbolize_names: true)
73
+ rescue StandardError => e
74
+ log_error("get_flag", e)
75
+ nil
76
+ end
77
+
78
+ def provision(project_id:, app_name:)
79
+ response = request(
80
+ :post,
81
+ "/api/v1/projects/provision",
82
+ body: { project_id: project_id, app_name: app_name },
83
+ use_service_key: true
84
+ )
85
+
86
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
87
+ rescue StandardError => e
88
+ log_error("provision", e)
89
+ false
90
+ end
91
+
92
+ private
93
+
94
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
95
+ uri = URI.parse("#{@base_url}#{path}")
96
+
97
+ if params
98
+ uri.query = URI.encode_www_form(params)
99
+ end
100
+
101
+ http = Net::HTTP.new(uri.host, uri.port)
102
+ http.use_ssl = uri.scheme == "https"
103
+ http.open_timeout = 5
104
+ http.read_timeout = 10
105
+
106
+ request = case method
107
+ when :get
108
+ Net::HTTP::Get.new(uri)
109
+ when :post
110
+ Net::HTTP::Post.new(uri)
111
+ when :put
112
+ Net::HTTP::Put.new(uri)
113
+ when :delete
114
+ Net::HTTP::Delete.new(uri)
115
+ end
116
+
117
+ # Set headers
118
+ request["Content-Type"] = "application/json"
119
+ request["Accept"] = "application/json"
120
+
121
+ if use_service_key
122
+ request["X-Service-Key"] = @config.cortex_master_key || @config.secret_key
123
+ else
124
+ auth_key = @config.cortex_api_key || @config.secret_key
125
+ request["Authorization"] = "Bearer #{auth_key}" if auth_key
126
+ end
127
+
128
+ headers.each { |k, v| request[k] = v }
129
+
130
+ # Set body
131
+ request.body = body.to_json if body
132
+
133
+ http.request(request)
134
+ end
135
+
136
+ def log_error(operation, error)
137
+ BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{error.message}")
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Cortex
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.cortex_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ # Try to provision with Platform project ID
19
+ project_id = detect_project_id
20
+ return unless project_id
21
+
22
+ client = Client.new(@config)
23
+ client.provision(
24
+ project_id: project_id,
25
+ app_name: @config.app_name || @config.service
26
+ )
27
+
28
+ BrainzLab.debug_log("[Cortex::Provisioner] Project provisioned: #{project_id}")
29
+ rescue StandardError => e
30
+ BrainzLab.debug_log("[Cortex::Provisioner] Provisioning failed: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def valid_auth?
36
+ key = @config.cortex_api_key || @config.cortex_master_key || @config.secret_key
37
+ !key.nil? && !key.empty?
38
+ end
39
+
40
+ def detect_project_id
41
+ # Try environment variable first
42
+ return ENV["BRAINZLAB_PROJECT_ID"] if ENV["BRAINZLAB_PROJECT_ID"]
43
+
44
+ # Could also detect from Platform API if we have a secret key
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end