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,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
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Sentinel
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.sentinel_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("[Sentinel::Provisioner] Project provisioned: #{project_id}")
28
+ rescue StandardError => e
29
+ BrainzLab.debug_log("[Sentinel::Provisioner] Provisioning failed: #{e.message}")
30
+ end
31
+
32
+ private
33
+
34
+ def valid_auth?
35
+ key = @config.sentinel_api_key || @config.sentinel_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
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sentinel/client"
4
+ require_relative "sentinel/provisioner"
5
+
6
+ module BrainzLab
7
+ module Sentinel
8
+ class << self
9
+ # List all registered hosts
10
+ # @param status [String] Filter by status (online, offline, warning, critical)
11
+ # @param page [Integer] Page number
12
+ # @param per_page [Integer] Results per page
13
+ # @return [Array<Hash>] List of hosts
14
+ #
15
+ # @example
16
+ # hosts = BrainzLab::Sentinel.hosts(status: "online")
17
+ #
18
+ def hosts(status: nil, page: 1, per_page: 50)
19
+ return [] unless enabled?
20
+
21
+ ensure_provisioned!
22
+ return [] unless BrainzLab.configuration.sentinel_valid?
23
+
24
+ client.list_hosts(status: status, page: page, per_page: per_page)
25
+ end
26
+
27
+ # Get host details
28
+ # @param host_id [String] Host ID
29
+ # @return [Hash, nil] Host details
30
+ def host(host_id)
31
+ return nil unless enabled?
32
+
33
+ ensure_provisioned!
34
+ return nil unless BrainzLab.configuration.sentinel_valid?
35
+
36
+ client.get_host(host_id)
37
+ end
38
+
39
+ # Get metrics for a host
40
+ # @param host_id [String] Host ID
41
+ # @param period [String] Time period (1h, 6h, 24h, 7d, 30d)
42
+ # @param metrics [Array<String>] Specific metrics (cpu, memory, disk, network)
43
+ # @return [Hash, nil] Metrics data
44
+ #
45
+ # @example
46
+ # metrics = BrainzLab::Sentinel.metrics(host_id, period: "24h", metrics: ["cpu", "memory"])
47
+ #
48
+ def metrics(host_id, period: "1h", metrics: nil)
49
+ return nil unless enabled?
50
+
51
+ ensure_provisioned!
52
+ return nil unless BrainzLab.configuration.sentinel_valid?
53
+
54
+ client.get_metrics(host_id, period: period, metrics: metrics)
55
+ end
56
+
57
+ # Get top processes for a host
58
+ # @param host_id [String] Host ID
59
+ # @param sort_by [String] Sort by (cpu, memory, time)
60
+ # @param limit [Integer] Max results
61
+ # @return [Array<Hash>] Process list
62
+ #
63
+ # @example
64
+ # procs = BrainzLab::Sentinel.processes(host_id, sort_by: "memory", limit: 10)
65
+ #
66
+ def processes(host_id, sort_by: "cpu", limit: 20)
67
+ return [] unless enabled?
68
+
69
+ ensure_provisioned!
70
+ return [] unless BrainzLab.configuration.sentinel_valid?
71
+
72
+ client.get_processes(host_id, sort_by: sort_by, limit: limit)
73
+ end
74
+
75
+ # List all containers
76
+ # @param host_id [String] Optional filter by host
77
+ # @param status [String] Filter by status (running, stopped, paused)
78
+ # @return [Array<Hash>] Container list
79
+ #
80
+ # @example
81
+ # containers = BrainzLab::Sentinel.containers(host_id: "host_123", status: "running")
82
+ #
83
+ def containers(host_id: nil, status: nil)
84
+ return [] unless enabled?
85
+
86
+ ensure_provisioned!
87
+ return [] unless BrainzLab.configuration.sentinel_valid?
88
+
89
+ client.list_containers(host_id: host_id, status: status)
90
+ end
91
+
92
+ # Get container details
93
+ # @param container_id [String] Container ID
94
+ # @return [Hash, nil] Container details
95
+ def container(container_id)
96
+ return nil unless enabled?
97
+
98
+ ensure_provisioned!
99
+ return nil unless BrainzLab.configuration.sentinel_valid?
100
+
101
+ client.get_container(container_id)
102
+ end
103
+
104
+ # Get container metrics
105
+ # @param container_id [String] Container ID
106
+ # @param period [String] Time period
107
+ # @return [Hash, nil] Container metrics
108
+ def container_metrics(container_id, period: "1h")
109
+ return nil unless enabled?
110
+
111
+ ensure_provisioned!
112
+ return nil unless BrainzLab.configuration.sentinel_valid?
113
+
114
+ client.get_container_metrics(container_id, period: period)
115
+ end
116
+
117
+ # Get alerts
118
+ # @param host_id [String] Optional filter by host
119
+ # @param status [String] Filter by status (active, acknowledged, resolved)
120
+ # @param severity [String] Filter by severity (info, warning, critical)
121
+ # @return [Array<Hash>] Alert list
122
+ #
123
+ # @example
124
+ # alerts = BrainzLab::Sentinel.alerts(severity: "critical", status: "active")
125
+ #
126
+ def alerts(host_id: nil, status: nil, severity: nil)
127
+ return [] unless enabled?
128
+
129
+ ensure_provisioned!
130
+ return [] unless BrainzLab.configuration.sentinel_valid?
131
+
132
+ client.get_alerts(host_id: host_id, status: status, severity: severity)
133
+ end
134
+
135
+ # === INTERNAL ===
136
+
137
+ def ensure_provisioned!
138
+ return if @provisioned
139
+
140
+ @provisioned = true
141
+ provisioner.ensure_project!
142
+ end
143
+
144
+ def provisioner
145
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
146
+ end
147
+
148
+ def client
149
+ @client ||= Client.new(BrainzLab.configuration)
150
+ end
151
+
152
+ def reset!
153
+ @client = nil
154
+ @provisioner = nil
155
+ @provisioned = false
156
+ end
157
+
158
+ private
159
+
160
+ def enabled?
161
+ BrainzLab.configuration.sentinel_enabled
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module BrainzLab
8
+ module Signal
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def send_alert(alert)
15
+ post("/api/v1/alerts", alert)
16
+ end
17
+
18
+ def send_notification(notification)
19
+ post("/api/v1/notifications", notification)
20
+ end
21
+
22
+ def trigger_rule(payload)
23
+ post("/api/v1/rules/trigger", payload)
24
+ end
25
+
26
+ private
27
+
28
+ def post(path, body)
29
+ uri = URI.parse("#{base_url}#{path}")
30
+ http = Net::HTTP.new(uri.host, uri.port)
31
+ http.use_ssl = uri.scheme == "https"
32
+ http.open_timeout = 5
33
+ http.read_timeout = 10
34
+
35
+ request = Net::HTTP::Post.new(uri.path)
36
+ request["Content-Type"] = "application/json"
37
+ request["Authorization"] = "Bearer #{api_key}"
38
+ request["User-Agent"] = "brainzlab-sdk/#{BrainzLab::VERSION}"
39
+ request.body = body.to_json
40
+
41
+ response = http.request(request)
42
+
43
+ unless response.is_a?(Net::HTTPSuccess)
44
+ BrainzLab.debug_log("[Signal] Request failed: #{response.code} - #{response.body}")
45
+ end
46
+
47
+ response
48
+ rescue => e
49
+ BrainzLab.debug_log("[Signal] Request error: #{e.message}")
50
+ nil
51
+ end
52
+
53
+ def base_url
54
+ @config.signal_url
55
+ end
56
+
57
+ def api_key
58
+ @config.signal_auth_key
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module BrainzLab
8
+ module Signal
9
+ class Provisioner
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def ensure_project!
15
+ return if @config.signal_api_key && !@config.signal_api_key.to_s.empty?
16
+ return unless @config.signal_url && !@config.signal_url.to_s.empty?
17
+ return unless @config.secret_key && !@config.secret_key.to_s.empty?
18
+
19
+ BrainzLab.debug_log("[Signal] Auto-provisioning project...")
20
+ provision_project
21
+ end
22
+
23
+ private
24
+
25
+ def provision_project
26
+ uri = URI.parse("#{@config.signal_url}/api/v1/projects/provision")
27
+ http = Net::HTTP.new(uri.host, uri.port)
28
+ http.use_ssl = uri.scheme == "https"
29
+ http.open_timeout = 10
30
+ http.read_timeout = 30
31
+
32
+ request = Net::HTTP::Post.new(uri.path)
33
+ request["Content-Type"] = "application/json"
34
+ request["Authorization"] = "Bearer #{@config.secret_key}"
35
+ request["User-Agent"] = "brainzlab-sdk/#{BrainzLab::VERSION}"
36
+ request.body = {
37
+ name: @config.service || "default",
38
+ environment: @config.environment
39
+ }.to_json
40
+
41
+ response = http.request(request)
42
+
43
+ if response.is_a?(Net::HTTPSuccess)
44
+ data = JSON.parse(response.body)
45
+ @config.signal_api_key = data["api_key"]
46
+ BrainzLab.debug_log("[Signal] Project provisioned successfully")
47
+ else
48
+ BrainzLab.debug_log("[Signal] Provisioning failed: #{response.code} - #{response.body}")
49
+ end
50
+ rescue => e
51
+ BrainzLab.debug_log("[Signal] Provisioning error: #{e.message}")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "signal/client"
4
+ require_relative "signal/provisioner"
5
+
6
+ module BrainzLab
7
+ module Signal
8
+ SEVERITIES = %i[info warning error critical].freeze
9
+
10
+ class << self
11
+ # Send an alert with a message
12
+ # @param name [String] Alert name (e.g., 'high_error_rate', 'low_disk_space')
13
+ # @param message [String] Alert message
14
+ # @param severity [Symbol] Alert severity (:info, :warning, :error, :critical)
15
+ # @param channels [Array<String>] Channels to send alert to (e.g., ['slack', 'email'])
16
+ # @param data [Hash] Additional data to include with the alert
17
+ def alert(name, message, severity: :warning, channels: nil, data: {})
18
+ return unless enabled?
19
+
20
+ ensure_provisioned!
21
+ return unless BrainzLab.configuration.signal_valid?
22
+
23
+ payload = {
24
+ type: "alert",
25
+ name: name,
26
+ message: message,
27
+ severity: severity.to_s,
28
+ channels: channels,
29
+ data: data,
30
+ timestamp: Time.now.utc.iso8601(3),
31
+ environment: BrainzLab.configuration.environment,
32
+ service: BrainzLab.configuration.service,
33
+ host: BrainzLab.configuration.host,
34
+ context: context_data
35
+ }
36
+
37
+ client.send_alert(payload)
38
+ end
39
+
40
+ # Send a notification to specific channels
41
+ # @param channel [String, Array<String>] Channel(s) to send to ('slack', 'email', 'webhook')
42
+ # @param message [String] Notification message
43
+ # @param title [String] Optional notification title
44
+ # @param data [Hash] Additional data
45
+ def notify(channel, message, title: nil, data: {})
46
+ return unless enabled?
47
+
48
+ ensure_provisioned!
49
+ return unless BrainzLab.configuration.signal_valid?
50
+
51
+ channels = Array(channel)
52
+ payload = {
53
+ type: "notification",
54
+ channels: channels,
55
+ message: message,
56
+ title: title,
57
+ data: data,
58
+ timestamp: Time.now.utc.iso8601(3),
59
+ environment: BrainzLab.configuration.environment,
60
+ service: BrainzLab.configuration.service
61
+ }
62
+
63
+ client.send_notification(payload)
64
+ end
65
+
66
+ # Trigger a predefined alert rule
67
+ # @param rule_name [String] Name of the alert rule to trigger
68
+ # @param context [Hash] Context data to pass to the rule
69
+ def trigger(rule_name, context = {})
70
+ return unless enabled?
71
+
72
+ ensure_provisioned!
73
+ return unless BrainzLab.configuration.signal_valid?
74
+
75
+ payload = {
76
+ type: "trigger",
77
+ rule: rule_name,
78
+ context: context,
79
+ timestamp: Time.now.utc.iso8601(3),
80
+ environment: BrainzLab.configuration.environment,
81
+ service: BrainzLab.configuration.service
82
+ }
83
+
84
+ client.trigger_rule(payload)
85
+ end
86
+
87
+ # Send a test alert to verify configuration
88
+ def test!
89
+ alert(
90
+ "test_alert",
91
+ "This is a test alert from BrainzLab Signal SDK",
92
+ severity: :info,
93
+ data: { test: true, sdk_version: BrainzLab::VERSION }
94
+ )
95
+ end
96
+
97
+ # === INTERNAL ===
98
+
99
+ def ensure_provisioned!
100
+ return if @provisioned
101
+
102
+ @provisioned = true
103
+ provisioner.ensure_project!
104
+ end
105
+
106
+ def provisioner
107
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
108
+ end
109
+
110
+ def client
111
+ @client ||= Client.new(BrainzLab.configuration)
112
+ end
113
+
114
+ def reset!
115
+ @client = nil
116
+ @provisioner = nil
117
+ @provisioned = false
118
+ end
119
+
120
+ private
121
+
122
+ def enabled?
123
+ BrainzLab.configuration.signal_effectively_enabled?
124
+ end
125
+
126
+ def context_data
127
+ ctx = BrainzLab::Context.current
128
+ {
129
+ user: ctx.user,
130
+ tags: ctx.tags,
131
+ extra: ctx.extra
132
+ }
133
+ end
134
+ end
135
+ end
136
+ end