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
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nerve/client"
4
+ require_relative "nerve/provisioner"
5
+
6
+ module BrainzLab
7
+ module Nerve
8
+ class << self
9
+ # Report a completed job
10
+ # @param job_class [String] Job class name
11
+ # @param job_id [String] Job ID
12
+ # @param queue [String] Queue name
13
+ # @param started_at [Time] When job started
14
+ # @param ended_at [Time] When job ended (defaults to now)
15
+ # @param attributes [Hash] Additional attributes
16
+ #
17
+ # @example
18
+ # BrainzLab::Nerve.report_success(
19
+ # job_class: "ProcessOrderJob",
20
+ # job_id: "abc-123",
21
+ # queue: "default",
22
+ # started_at: 1.minute.ago
23
+ # )
24
+ #
25
+ def report_success(job_class:, job_id:, queue:, started_at:, ended_at: Time.now, **attributes)
26
+ return false unless enabled?
27
+
28
+ ensure_provisioned!
29
+ return false unless BrainzLab.configuration.nerve_valid?
30
+
31
+ client.report_job(
32
+ job_class: job_class,
33
+ job_id: job_id,
34
+ queue: queue,
35
+ status: "completed",
36
+ started_at: started_at,
37
+ ended_at: ended_at,
38
+ **attributes
39
+ )
40
+ end
41
+
42
+ # Report a failed job
43
+ def report_failure(job_class:, job_id:, queue:, error:, started_at: nil, **attributes)
44
+ return false unless enabled?
45
+
46
+ ensure_provisioned!
47
+ return false unless BrainzLab.configuration.nerve_valid?
48
+
49
+ client.report_failure(
50
+ job_class: job_class,
51
+ job_id: job_id,
52
+ queue: queue,
53
+ error_class: error.class.name,
54
+ error_message: error.message,
55
+ backtrace: error.backtrace,
56
+ started_at: started_at,
57
+ **attributes
58
+ )
59
+ end
60
+
61
+ # Report a job that's currently running
62
+ def report_started(job_class:, job_id:, queue:, **attributes)
63
+ return false unless enabled?
64
+
65
+ ensure_provisioned!
66
+ return false unless BrainzLab.configuration.nerve_valid?
67
+
68
+ client.report_job(
69
+ job_class: job_class,
70
+ job_id: job_id,
71
+ queue: queue,
72
+ status: "running",
73
+ started_at: Time.now,
74
+ ended_at: Time.now,
75
+ **attributes
76
+ )
77
+ end
78
+
79
+ # Get job statistics
80
+ # @param queue [String] Filter by queue (optional)
81
+ # @param job_class [String] Filter by job class (optional)
82
+ # @param period [String] Time period: "1h", "24h", "7d", "30d"
83
+ def stats(queue: nil, job_class: nil, period: "1h")
84
+ return nil unless enabled?
85
+
86
+ ensure_provisioned!
87
+ return nil unless BrainzLab.configuration.nerve_valid?
88
+
89
+ client.stats(queue: queue, job_class: job_class, period: period)
90
+ end
91
+
92
+ # List recent jobs
93
+ def jobs(queue: nil, status: nil, limit: 100)
94
+ return [] unless enabled?
95
+
96
+ ensure_provisioned!
97
+ return [] unless BrainzLab.configuration.nerve_valid?
98
+
99
+ client.list_jobs(queue: queue, status: status, limit: limit)
100
+ end
101
+
102
+ # List all queues
103
+ def queues
104
+ return [] unless enabled?
105
+
106
+ ensure_provisioned!
107
+ return [] unless BrainzLab.configuration.nerve_valid?
108
+
109
+ client.list_queues
110
+ end
111
+
112
+ # Get queue details
113
+ def queue(name)
114
+ return nil unless enabled?
115
+
116
+ ensure_provisioned!
117
+ return nil unless BrainzLab.configuration.nerve_valid?
118
+
119
+ client.get_queue(name)
120
+ end
121
+
122
+ # Retry a failed job
123
+ def retry(job_id)
124
+ return false unless enabled?
125
+
126
+ ensure_provisioned!
127
+ return false unless BrainzLab.configuration.nerve_valid?
128
+
129
+ client.retry_job(job_id)
130
+ end
131
+
132
+ # Delete a job
133
+ def delete(job_id)
134
+ return false unless enabled?
135
+
136
+ ensure_provisioned!
137
+ return false unless BrainzLab.configuration.nerve_valid?
138
+
139
+ client.delete_job(job_id)
140
+ end
141
+
142
+ # Report queue metrics (for custom job backends)
143
+ def report_metrics(queue:, size:, latency_ms: nil, workers: nil)
144
+ return false unless enabled?
145
+
146
+ ensure_provisioned!
147
+ return false unless BrainzLab.configuration.nerve_valid?
148
+
149
+ client.report_metrics(
150
+ queue: queue,
151
+ size: size,
152
+ latency_ms: latency_ms,
153
+ workers: workers
154
+ )
155
+ end
156
+
157
+ # Track a job execution (block helper)
158
+ # @example
159
+ # BrainzLab::Nerve.track(job_class: "MyJob", job_id: "123", queue: "default") do
160
+ # # job work
161
+ # end
162
+ #
163
+ def track(job_class:, job_id:, queue: "default", **attributes)
164
+ started_at = Time.now
165
+
166
+ begin
167
+ result = yield
168
+ report_success(
169
+ job_class: job_class,
170
+ job_id: job_id,
171
+ queue: queue,
172
+ started_at: started_at,
173
+ **attributes
174
+ )
175
+ result
176
+ rescue StandardError => e
177
+ report_failure(
178
+ job_class: job_class,
179
+ job_id: job_id,
180
+ queue: queue,
181
+ error: e,
182
+ started_at: started_at,
183
+ **attributes
184
+ )
185
+ raise
186
+ end
187
+ end
188
+
189
+ # === INTERNAL ===
190
+
191
+ def ensure_provisioned!
192
+ return if @provisioned
193
+
194
+ @provisioned = true
195
+ provisioner.ensure_project!
196
+ end
197
+
198
+ def provisioner
199
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
200
+ end
201
+
202
+ def client
203
+ @client ||= Client.new(BrainzLab.configuration)
204
+ end
205
+
206
+ def reset!
207
+ @client = nil
208
+ @provisioner = nil
209
+ @provisioned = false
210
+ end
211
+
212
+ private
213
+
214
+ def enabled?
215
+ BrainzLab.configuration.nerve_enabled
216
+ end
217
+ end
218
+ end
219
+ end
@@ -26,6 +26,7 @@ module BrainzLab
26
26
  event = ActiveSupport::Notifications::Event.new(*args)
27
27
  next if skip_query?(event.payload)
28
28
 
29
+ sql = event.payload[:sql]
29
30
  record_span(
30
31
  name: event.payload[:name] || "SQL",
31
32
  kind: "db",
@@ -33,9 +34,11 @@ module BrainzLab
33
34
  ended_at: event.end,
34
35
  duration_ms: event.duration,
35
36
  data: {
36
- sql: truncate_sql(event.payload[:sql]),
37
+ sql: truncate_sql(sql),
37
38
  name: event.payload[:name],
38
- cached: event.payload[:cached] || false
39
+ cached: event.payload[:cached] || false,
40
+ table: extract_table(sql),
41
+ operation: extract_operation(sql)
39
42
  }
40
43
  )
41
44
  end
@@ -358,6 +361,36 @@ module BrainzLab
358
361
  return nil unless path
359
362
  path.to_s.split("/").last(2).join("/")
360
363
  end
364
+
365
+ def extract_table(sql)
366
+ return nil unless sql
367
+
368
+ # Match FROM "table" or FROM table patterns
369
+ # Also handles INSERT INTO, UPDATE, DELETE FROM
370
+ case sql.to_s
371
+ when /\bFROM\s+["'`]?(\w+)["'`]?/i
372
+ Regexp.last_match(1)
373
+ when /\bINTO\s+["'`]?(\w+)["'`]?/i
374
+ Regexp.last_match(1)
375
+ when /\bUPDATE\s+["'`]?(\w+)["'`]?/i
376
+ Regexp.last_match(1)
377
+ when /\bJOIN\s+["'`]?(\w+)["'`]?/i
378
+ Regexp.last_match(1)
379
+ end
380
+ end
381
+
382
+ def extract_operation(sql)
383
+ return nil unless sql
384
+
385
+ case sql.to_s.strip.upcase
386
+ when /\ASELECT/i then "SELECT"
387
+ when /\AINSERT/i then "INSERT"
388
+ when /\AUPDATE/i then "UPDATE"
389
+ when /\ADELETE/i then "DELETE"
390
+ when /\ABEGIN/i, /\ACOMMIT/i, /\AROLLBACK/i then "TRANSACTION"
391
+ else "QUERY"
392
+ end
393
+ end
361
394
  end
362
395
  end
363
396
  end
@@ -31,7 +31,7 @@ module BrainzLab
31
31
  end
32
32
 
33
33
  def valid?
34
- @trace_id && @span_id
34
+ !trace_id.nil? && !span_id.nil?
35
35
  end
36
36
 
37
37
  def to_h
@@ -52,7 +52,7 @@ module BrainzLab
52
52
  ).compact
53
53
 
54
54
  # Add request context if available
55
- ctx = Context.current
55
+ ctx = BrainzLab::Context.current
56
56
  payload[:request_id] ||= ctx.request_id
57
57
  payload[:user_id] ||= ctx.user&.dig(:id)&.to_s
58
58
 
@@ -160,7 +160,7 @@ module BrainzLab
160
160
  end
161
161
 
162
162
  def enabled?
163
- BrainzLab.configuration.pulse_enabled
163
+ BrainzLab.configuration.pulse_effectively_enabled?
164
164
  end
165
165
 
166
166
  def build_trace_payload(name, kind, started_at, ended_at, attributes)
@@ -149,8 +149,7 @@ module BrainzLab
149
149
 
150
150
  sql
151
151
  .gsub(/\b\d+\b/, "?") # Replace numbers
152
- .gsub(/'[^']*'/, "?") # Replace strings
153
- .gsub(/"[^"]*"/, "?") # Replace double-quoted strings
152
+ .gsub(/'[^']*'/, "?") # Replace single-quoted strings
154
153
  .gsub(/\$\d+/, "?") # Replace positional params
155
154
  .gsub(/\/\*.*?\*\//, "") # Remove comments
156
155
  .gsub(/\s+/, " ") # Normalize whitespace
@@ -7,6 +7,17 @@ module BrainzLab
7
7
  require "generators/brainzlab/install/install_generator"
8
8
  end
9
9
 
10
+ # Load Vault secrets early, before configuration
11
+ # This allows secrets to be used in config files
12
+ initializer "brainzlab.load_vault_secrets", before: :load_environment_config do
13
+ if BrainzLab.configuration.vault_enabled && BrainzLab.configuration.vault_auto_load
14
+ BrainzLab.debug_log("[Vault] Auto-loading secrets into ENV...")
15
+ BrainzLab::Vault.load!(
16
+ provider_keys: BrainzLab.configuration.vault_load_provider_keys
17
+ )
18
+ end
19
+ end
20
+
10
21
  initializer "brainzlab.configure_rails_initialization" do |app|
11
22
  # Set defaults from Rails
12
23
  BrainzLab.configure do |config|
@@ -20,11 +31,33 @@ module BrainzLab
20
31
 
21
32
  # Add request context middleware (runs early)
22
33
  app.middleware.insert_after ActionDispatch::RequestId, BrainzLab::Rails::Middleware
34
+
35
+ # Add DevTools middlewares if enabled
36
+ if BrainzLab.configuration.devtools_enabled
37
+ require_relative "../devtools"
38
+
39
+ # Asset server (handles /__brainzlab__/* requests)
40
+ app.middleware.insert_before ActionDispatch::Static, BrainzLab::DevTools::Middleware::AssetServer
41
+
42
+ # Database handler (handles /_brainzlab/devtools/database POST requests)
43
+ # Allows running migrations from the error page
44
+ app.middleware.insert_before ActionDispatch::Static, BrainzLab::DevTools::Middleware::DatabaseHandler
45
+
46
+ # Error page (catches exceptions and renders branded error page)
47
+ # Insert BEFORE DebugExceptions so we can intercept the HTML error page
48
+ # that DebugExceptions renders and replace it with our own
49
+ if defined?(ActionDispatch::DebugExceptions)
50
+ app.middleware.insert_before ActionDispatch::DebugExceptions, BrainzLab::DevTools::Middleware::ErrorPage
51
+ end
52
+
53
+ # Debug panel (injects panel into HTML responses)
54
+ app.middleware.use BrainzLab::DevTools::Middleware::DebugPanel
55
+ end
23
56
  end
24
57
 
25
58
  config.after_initialize do
26
59
  # Set up custom log formatter
27
- setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
60
+ BrainzLab::Rails::Railtie.setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
28
61
 
29
62
  # Install instrumentation (HTTP tracking, etc.)
30
63
  BrainzLab::Instrumentation.install!
@@ -569,9 +602,9 @@ module BrainzLab
569
602
  end
570
603
  end
571
604
 
572
- # Sidekiq error handler
605
+ # Sidekiq error handler - Sidekiq 7.x+ requires 3 arguments
573
606
  class SidekiqErrorHandler
574
- def call(exception, context)
607
+ def call(exception, context, _config = nil)
575
608
  BrainzLab::Reflex.capture(exception,
576
609
  tags: { type: "sidekiq" },
577
610
  extra: {
@@ -37,14 +37,31 @@ module BrainzLab
37
37
  private
38
38
 
39
39
  def should_provision?
40
+ if @config.debug
41
+ log_debug("Checking provision conditions:")
42
+ log_debug(" recall_auto_provision: #{@config.recall_auto_provision}")
43
+ log_debug(" app_name: '#{@config.app_name}'")
44
+ log_debug(" secret_key set: #{@config.secret_key.to_s.strip.length > 0}")
45
+ log_debug(" recall_master_key set: #{@config.recall_master_key.to_s.strip.length > 0}")
46
+ end
47
+
40
48
  return false unless @config.recall_auto_provision
41
49
  return false unless @config.app_name.to_s.strip.length > 0
42
50
  return false if @config.secret_key.to_s.strip.length > 0
43
51
  return false unless @config.recall_master_key.to_s.strip.length > 0
44
52
 
53
+ log_debug("Will provision Recall project") if @config.debug
45
54
  true
46
55
  end
47
56
 
57
+ def log_debug(message)
58
+ if @config.logger
59
+ @config.logger.info("[BrainzLab::Debug] #{message}")
60
+ else
61
+ puts "[BrainzLab::Debug] #{message}"
62
+ end
63
+ end
64
+
48
65
  def provision_project
49
66
  uri = URI.parse("#{@config.recall_url}/api/v1/projects/provision")
50
67
  request = Net::HTTP::Post.new(uri)
@@ -30,7 +30,7 @@ module BrainzLab
30
30
 
31
31
  def log(level, message, **data)
32
32
  config = BrainzLab.configuration
33
- return unless config.recall_enabled
33
+ return unless config.recall_effectively_enabled?
34
34
 
35
35
  # Auto-provision project on first log if app_name is configured
36
36
  ensure_provisioned!
@@ -43,6 +43,11 @@ module BrainzLab
43
43
  end
44
44
 
45
45
  def ensure_provisioned!
46
+ config = BrainzLab.configuration
47
+ if config.debug
48
+ puts "[BrainzLab::Debug] Recall.ensure_provisioned! called, @provisioned=#{@provisioned}"
49
+ end
50
+
46
51
  return if @provisioned
47
52
 
48
53
  @provisioned = true
@@ -77,7 +77,7 @@ module BrainzLab
77
77
  private
78
78
 
79
79
  def enabled?
80
- BrainzLab.configuration.reflex_enabled
80
+ BrainzLab.configuration.reflex_effectively_enabled?
81
81
  end
82
82
 
83
83
  def capture_disabled?
@@ -245,7 +245,7 @@ module BrainzLab
245
245
  # - "path/to/file.rb:42:in `method_name'" (backtick + single quote)
246
246
  # - "path/to/file.rb:42:in 'method_name'" (single quotes)
247
247
  # - "path/to/file.rb:42" (no method)
248
- if line =~ /\A(.+):(\d+):in [`'](.+)'?\z/
248
+ if line =~ /\A(.+):(\d+):in [`']([^']+)'?\z/
249
249
  {
250
250
  file: $1,
251
251
  line: $2.to_i,
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module BrainzLab
8
+ module Sentinel
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ @base_url = config.sentinel_url || "https://sentinel.brainzlab.ai"
13
+ end
14
+
15
+ # List all registered hosts
16
+ def list_hosts(status: nil, page: 1, per_page: 50)
17
+ params = { page: page, per_page: per_page }
18
+ params[:status] = status if status
19
+
20
+ response = request(:get, "/api/v1/hosts", params: params)
21
+
22
+ return [] unless response.is_a?(Net::HTTPSuccess)
23
+
24
+ data = JSON.parse(response.body, symbolize_names: true)
25
+ data[:hosts] || []
26
+ rescue StandardError => e
27
+ log_error("list_hosts", e)
28
+ []
29
+ end
30
+
31
+ # Get host details
32
+ def get_host(host_id)
33
+ response = request(:get, "/api/v1/hosts/#{host_id}")
34
+
35
+ return nil unless response.is_a?(Net::HTTPSuccess)
36
+
37
+ JSON.parse(response.body, symbolize_names: true)
38
+ rescue StandardError => e
39
+ log_error("get_host", e)
40
+ nil
41
+ end
42
+
43
+ # Get host metrics
44
+ # @param host_id [String] Host ID
45
+ # @param period [String] Time period (1h, 6h, 24h, 7d, 30d)
46
+ # @param metrics [Array<String>] Specific metrics to fetch (cpu, memory, disk, network)
47
+ def get_metrics(host_id, period: "1h", metrics: nil)
48
+ params = { period: period }
49
+ params[:metrics] = metrics.join(",") if metrics
50
+
51
+ response = request(:get, "/api/v1/hosts/#{host_id}/metrics", params: params)
52
+
53
+ return nil unless response.is_a?(Net::HTTPSuccess)
54
+
55
+ JSON.parse(response.body, symbolize_names: true)
56
+ rescue StandardError => e
57
+ log_error("get_metrics", e)
58
+ nil
59
+ end
60
+
61
+ # Get top processes for a host
62
+ def get_processes(host_id, sort_by: "cpu", limit: 20)
63
+ params = { sort_by: sort_by, limit: limit }
64
+
65
+ response = request(:get, "/api/v1/hosts/#{host_id}/processes", params: params)
66
+
67
+ return [] unless response.is_a?(Net::HTTPSuccess)
68
+
69
+ data = JSON.parse(response.body, symbolize_names: true)
70
+ data[:processes] || []
71
+ rescue StandardError => e
72
+ log_error("get_processes", e)
73
+ []
74
+ end
75
+
76
+ # List all containers
77
+ def list_containers(host_id: nil, status: nil)
78
+ params = {}
79
+ params[:host_id] = host_id if host_id
80
+ params[:status] = status if status
81
+
82
+ response = request(:get, "/api/v1/containers", params: params)
83
+
84
+ return [] unless response.is_a?(Net::HTTPSuccess)
85
+
86
+ data = JSON.parse(response.body, symbolize_names: true)
87
+ data[:containers] || []
88
+ rescue StandardError => e
89
+ log_error("list_containers", e)
90
+ []
91
+ end
92
+
93
+ # Get container details
94
+ def get_container(container_id)
95
+ response = request(:get, "/api/v1/containers/#{container_id}")
96
+
97
+ return nil unless response.is_a?(Net::HTTPSuccess)
98
+
99
+ JSON.parse(response.body, symbolize_names: true)
100
+ rescue StandardError => e
101
+ log_error("get_container", e)
102
+ nil
103
+ end
104
+
105
+ # Get container metrics
106
+ def get_container_metrics(container_id, period: "1h")
107
+ params = { period: period }
108
+
109
+ response = request(:get, "/api/v1/containers/#{container_id}/metrics", params: params)
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_container_metrics", e)
116
+ nil
117
+ end
118
+
119
+ # Get alerts for a host
120
+ def get_alerts(host_id: nil, status: nil, severity: nil)
121
+ params = {}
122
+ params[:host_id] = host_id if host_id
123
+ params[:status] = status if status
124
+ params[:severity] = severity if severity
125
+
126
+ response = request(:get, "/api/v1/alerts", params: params)
127
+
128
+ return [] unless response.is_a?(Net::HTTPSuccess)
129
+
130
+ data = JSON.parse(response.body, symbolize_names: true)
131
+ data[:alerts] || []
132
+ rescue StandardError => e
133
+ log_error("get_alerts", e)
134
+ []
135
+ end
136
+
137
+ # Report metrics from agent (internal use)
138
+ def report_metrics(host_id:, metrics:, timestamp: nil)
139
+ response = request(
140
+ :post,
141
+ "/internal/agent/report",
142
+ body: {
143
+ host_id: host_id,
144
+ metrics: metrics,
145
+ timestamp: timestamp || Time.now.utc.iso8601
146
+ },
147
+ use_agent_key: true
148
+ )
149
+
150
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
151
+ rescue StandardError => e
152
+ log_error("report_metrics", e)
153
+ false
154
+ end
155
+
156
+ def provision(project_id:, app_name:)
157
+ response = request(
158
+ :post,
159
+ "/api/v1/projects/provision",
160
+ body: { project_id: project_id, app_name: app_name },
161
+ use_service_key: true
162
+ )
163
+
164
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
165
+ rescue StandardError => e
166
+ log_error("provision", e)
167
+ false
168
+ end
169
+
170
+ private
171
+
172
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false, use_agent_key: false)
173
+ uri = URI.parse("#{@base_url}#{path}")
174
+
175
+ if params
176
+ uri.query = URI.encode_www_form(params)
177
+ end
178
+
179
+ http = Net::HTTP.new(uri.host, uri.port)
180
+ http.use_ssl = uri.scheme == "https"
181
+ http.open_timeout = 10
182
+ http.read_timeout = 30
183
+
184
+ request = case method
185
+ when :get
186
+ Net::HTTP::Get.new(uri)
187
+ when :post
188
+ Net::HTTP::Post.new(uri)
189
+ when :put
190
+ Net::HTTP::Put.new(uri)
191
+ when :delete
192
+ Net::HTTP::Delete.new(uri)
193
+ end
194
+
195
+ request["Content-Type"] = "application/json"
196
+ request["Accept"] = "application/json"
197
+
198
+ if use_service_key
199
+ request["X-Service-Key"] = @config.sentinel_master_key || @config.secret_key
200
+ elsif use_agent_key
201
+ request["X-Agent-Key"] = @config.sentinel_agent_key || @config.sentinel_api_key || @config.secret_key
202
+ else
203
+ auth_key = @config.sentinel_api_key || @config.secret_key
204
+ request["Authorization"] = "Bearer #{auth_key}" if auth_key
205
+ end
206
+
207
+ headers.each { |k, v| request[k] = v }
208
+ request.body = body.to_json if body
209
+
210
+ http.request(request)
211
+ end
212
+
213
+ def log_error(operation, error)
214
+ BrainzLab.debug_log("[Sentinel::Client] #{operation} failed: #{error.message}")
215
+ end
216
+ end
217
+ end
218
+ end