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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +30 -0
- data/lib/brainzlab/beacon/client.rb +209 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +341 -3
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +141 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +227 -0
- data/lib/brainzlab/dendrite/client.rb +232 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
- data/lib/brainzlab/devtools/assets/devtools.js +322 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +70 -0
- data/lib/brainzlab/flux/provisioner.rb +57 -0
- data/lib/brainzlab/flux.rb +174 -0
- data/lib/brainzlab/instrumentation/active_record.rb +18 -1
- data/lib/brainzlab/instrumentation/aws.rb +179 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/resque.rb +115 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
- data/lib/brainzlab/instrumentation/stripe.rb +164 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
- data/lib/brainzlab/instrumentation.rb +72 -0
- data/lib/brainzlab/nerve/client.rb +217 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/instrumentation.rb +35 -2
- data/lib/brainzlab/pulse/propagation.rb +1 -1
- data/lib/brainzlab/pulse/tracer.rb +1 -1
- data/lib/brainzlab/pulse.rb +1 -1
- data/lib/brainzlab/rails/log_subscriber.rb +1 -2
- data/lib/brainzlab/rails/railtie.rb +36 -3
- data/lib/brainzlab/recall/provisioner.rb +17 -0
- data/lib/brainzlab/recall.rb +6 -1
- data/lib/brainzlab/reflex.rb +20 -5
- data/lib/brainzlab/sentinel/client.rb +218 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +62 -0
- data/lib/brainzlab/signal/provisioner.rb +55 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +290 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
- data/lib/brainzlab/utilities/health_check.rb +296 -0
- data/lib/brainzlab/utilities/log_formatter.rb +256 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +198 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +268 -0
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +128 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +157 -0
- data/lib/brainzlab.rb +101 -0
- 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
|