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