fluyenta-ruby 0.1.14
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 +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -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 +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -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 +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Utilities
|
|
5
|
+
# Rate limiter with support for sliding window and token bucket algorithms
|
|
6
|
+
# Integrates with Flux for metrics tracking
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# limiter = BrainzLab::Utilities::RateLimiter.new(
|
|
10
|
+
# key: "api:user:123",
|
|
11
|
+
# limit: 100,
|
|
12
|
+
# window: 60 # seconds
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# if limiter.allow?
|
|
16
|
+
# # proceed with request
|
|
17
|
+
# else
|
|
18
|
+
# # rate limited
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example With block
|
|
22
|
+
# BrainzLab::Utilities::RateLimiter.throttle("api:user:#{user.id}", limit: 100, window: 60) do
|
|
23
|
+
# # this block runs only if not rate limited
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class RateLimiter
|
|
27
|
+
attr_reader :key, :limit, :window, :remaining, :reset_at
|
|
28
|
+
|
|
29
|
+
def initialize(key:, limit:, window:, store: nil)
|
|
30
|
+
@key = key
|
|
31
|
+
@limit = limit
|
|
32
|
+
@window = window
|
|
33
|
+
@store = store || default_store
|
|
34
|
+
@remaining = limit
|
|
35
|
+
@reset_at = Time.now + window
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if request is allowed (doesn't consume a token)
|
|
39
|
+
def allowed?
|
|
40
|
+
count, = get_current_count
|
|
41
|
+
count < @limit
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check and consume a token
|
|
45
|
+
def allow?
|
|
46
|
+
count, reset = increment
|
|
47
|
+
@remaining = [@limit - count, 0].max
|
|
48
|
+
@reset_at = reset
|
|
49
|
+
|
|
50
|
+
allowed = count <= @limit
|
|
51
|
+
|
|
52
|
+
# Track metrics
|
|
53
|
+
track_attempt(allowed)
|
|
54
|
+
|
|
55
|
+
allowed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Alias for allow?
|
|
59
|
+
def throttle?
|
|
60
|
+
!allow?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get current usage info
|
|
64
|
+
def status
|
|
65
|
+
count, reset = get_current_count
|
|
66
|
+
{
|
|
67
|
+
key: @key,
|
|
68
|
+
limit: @limit,
|
|
69
|
+
remaining: [@limit - count, 0].max,
|
|
70
|
+
reset_at: reset,
|
|
71
|
+
used: count
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reset the rate limit for this key
|
|
76
|
+
def reset!
|
|
77
|
+
@store.delete(@key)
|
|
78
|
+
@remaining = @limit
|
|
79
|
+
@reset_at = Time.now + @window
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Class method for quick throttling
|
|
83
|
+
def self.throttle(key, limit:, window:, store: nil)
|
|
84
|
+
limiter = new(key: key, limit: limit, window: window, store: store)
|
|
85
|
+
|
|
86
|
+
if limiter.allow?
|
|
87
|
+
yield if block_given?
|
|
88
|
+
true
|
|
89
|
+
else
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check rate limit without incrementing
|
|
95
|
+
def self.allowed?(key, limit:, window:, store: nil)
|
|
96
|
+
limiter = new(key: key, limit: limit, window: window, store: store)
|
|
97
|
+
limiter.allowed?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def default_store
|
|
103
|
+
@default_store ||= MemoryStore.new
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_current_count
|
|
107
|
+
current_bucket
|
|
108
|
+
data = @store.get(@key) || { buckets: {}, created_at: Time.now.to_i }
|
|
109
|
+
|
|
110
|
+
# Clean old buckets
|
|
111
|
+
cutoff = Time.now.to_i - @window
|
|
112
|
+
data[:buckets].delete_if { |k, _| k.to_i < cutoff }
|
|
113
|
+
|
|
114
|
+
count = data[:buckets].values.sum
|
|
115
|
+
reset = Time.at(Time.now.to_i + @window - (Time.now.to_i % @window))
|
|
116
|
+
|
|
117
|
+
[count, reset]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def increment
|
|
121
|
+
bucket = current_bucket
|
|
122
|
+
data = @store.get(@key) || { buckets: {}, created_at: Time.now.to_i }
|
|
123
|
+
|
|
124
|
+
# Clean old buckets
|
|
125
|
+
cutoff = Time.now.to_i - @window
|
|
126
|
+
data[:buckets].delete_if { |k, _| k.to_i < cutoff }
|
|
127
|
+
|
|
128
|
+
# Increment current bucket
|
|
129
|
+
data[:buckets][bucket] ||= 0
|
|
130
|
+
data[:buckets][bucket] += 1
|
|
131
|
+
|
|
132
|
+
# Store with TTL
|
|
133
|
+
@store.set(@key, data, ttl: @window * 2)
|
|
134
|
+
|
|
135
|
+
count = data[:buckets].values.sum
|
|
136
|
+
reset = Time.at(Time.now.to_i + @window - (Time.now.to_i % @window))
|
|
137
|
+
|
|
138
|
+
[count, reset]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def current_bucket
|
|
142
|
+
# Use 1-second buckets for sliding window
|
|
143
|
+
Time.now.to_i.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def track_attempt(allowed)
|
|
147
|
+
return unless BrainzLab.configuration.flux_effectively_enabled?
|
|
148
|
+
|
|
149
|
+
if allowed
|
|
150
|
+
BrainzLab::Flux.increment('rate_limiter.allowed', tags: { key: sanitize_key(@key) })
|
|
151
|
+
else
|
|
152
|
+
BrainzLab::Flux.increment('rate_limiter.denied', tags: { key: sanitize_key(@key) })
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def sanitize_key(key)
|
|
157
|
+
# Remove user-specific identifiers for aggregation
|
|
158
|
+
key.gsub(/:\d+/, ':*').gsub(/:[a-f0-9-]{36}/, ':*')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Simple in-memory store (for development/single-instance)
|
|
162
|
+
class MemoryStore
|
|
163
|
+
def initialize
|
|
164
|
+
@data = {}
|
|
165
|
+
@mutex = Mutex.new
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def get(key)
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
entry = @data[key]
|
|
171
|
+
return nil unless entry
|
|
172
|
+
return nil if entry[:expires_at] && Time.now > entry[:expires_at]
|
|
173
|
+
|
|
174
|
+
entry[:value]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def set(key, value, ttl: nil)
|
|
179
|
+
@mutex.synchronize do
|
|
180
|
+
@data[key] = {
|
|
181
|
+
value: value,
|
|
182
|
+
expires_at: ttl ? Time.now + ttl : nil
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def delete(key)
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
@data.delete(key)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def clear!
|
|
194
|
+
@mutex.synchronize do
|
|
195
|
+
@data.clear
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Redis store adapter
|
|
201
|
+
class RedisStore
|
|
202
|
+
def initialize(redis)
|
|
203
|
+
@redis = redis
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def get(key)
|
|
207
|
+
data = @redis.get("brainzlab:ratelimit:#{key}")
|
|
208
|
+
return nil unless data
|
|
209
|
+
|
|
210
|
+
JSON.parse(data, symbolize_names: true)
|
|
211
|
+
rescue StandardError
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def set(key, value, ttl: nil)
|
|
216
|
+
full_key = "brainzlab:ratelimit:#{key}"
|
|
217
|
+
if ttl
|
|
218
|
+
@redis.setex(full_key, ttl, value.to_json)
|
|
219
|
+
else
|
|
220
|
+
@redis.set(full_key, value.to_json)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def delete(key)
|
|
225
|
+
@redis.del("brainzlab:ratelimit:#{key}")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'utilities/rate_limiter'
|
|
4
|
+
require_relative 'utilities/circuit_breaker'
|
|
5
|
+
require_relative 'utilities/health_check'
|
|
6
|
+
require_relative 'utilities/log_formatter'
|
|
7
|
+
|
|
8
|
+
module BrainzLab
|
|
9
|
+
module Utilities
|
|
10
|
+
# All utilities are auto-loaded from their respective files
|
|
11
|
+
# Access them via:
|
|
12
|
+
# BrainzLab::Utilities::RateLimiter
|
|
13
|
+
# BrainzLab::Utilities::CircuitBreaker
|
|
14
|
+
# BrainzLab::Utilities::HealthCheck
|
|
15
|
+
# BrainzLab::Utilities::LogFormatter
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Vault
|
|
5
|
+
class Cache
|
|
6
|
+
def initialize(ttl = 300)
|
|
7
|
+
@ttl = ttl
|
|
8
|
+
@store = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def get(key)
|
|
13
|
+
@mutex.synchronize do
|
|
14
|
+
entry = @store[key]
|
|
15
|
+
return nil unless entry
|
|
16
|
+
return nil if expired?(entry)
|
|
17
|
+
|
|
18
|
+
entry[:value]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(key, value)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@store[key] = {
|
|
25
|
+
value: value,
|
|
26
|
+
expires_at: Time.now + @ttl
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def has?(key)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
entry = @store[key]
|
|
35
|
+
return false unless entry
|
|
36
|
+
return false if expired?(entry)
|
|
37
|
+
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def delete(key)
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@store.delete(key)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def delete_pattern(pattern)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
regex = Regexp.new(pattern.gsub('*', '.*'))
|
|
51
|
+
@store.delete_if { |k, _| k.match?(regex) }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear!
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@store.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def size
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
cleanup_expired!
|
|
64
|
+
@store.size
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def expired?(entry)
|
|
71
|
+
entry[:expires_at] < Time.now
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cleanup_expired!
|
|
75
|
+
now = Time.now
|
|
76
|
+
@store.delete_if { |_, entry| entry[:expires_at] < now }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Vault
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
@base_url = config.vault_url || 'https://vault.brainzlab.ai'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(key, environment:)
|
|
16
|
+
response = request(
|
|
17
|
+
:get,
|
|
18
|
+
"/api/v1/secrets/#{CGI.escape(key)}",
|
|
19
|
+
headers: { 'X-Vault-Environment' => environment }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
23
|
+
|
|
24
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
25
|
+
data[:value]
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
log_error('get', e)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set(key, value, environment:, description: nil, note: nil)
|
|
32
|
+
body = {
|
|
33
|
+
key: key,
|
|
34
|
+
value: value,
|
|
35
|
+
description: description,
|
|
36
|
+
note: note
|
|
37
|
+
}.compact
|
|
38
|
+
|
|
39
|
+
response = request(
|
|
40
|
+
:post,
|
|
41
|
+
'/api/v1/secrets',
|
|
42
|
+
headers: { 'X-Vault-Environment' => environment },
|
|
43
|
+
body: body
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
log_error('set', e)
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def list(environment:)
|
|
53
|
+
response = request(
|
|
54
|
+
:get,
|
|
55
|
+
'/api/v1/secrets',
|
|
56
|
+
headers: { 'X-Vault-Environment' => environment }
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
60
|
+
|
|
61
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
62
|
+
data[:secrets] || []
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
log_error('list', e)
|
|
65
|
+
[]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete(key)
|
|
69
|
+
response = request(:delete, "/api/v1/secrets/#{CGI.escape(key)}")
|
|
70
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
log_error('delete', e)
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def export(environment:, format:)
|
|
77
|
+
params = { format: format }
|
|
78
|
+
response = request(
|
|
79
|
+
:get,
|
|
80
|
+
'/api/v1/sync/export',
|
|
81
|
+
headers: { 'X-Vault-Environment' => environment },
|
|
82
|
+
params: params
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
86
|
+
|
|
87
|
+
case format
|
|
88
|
+
when :json
|
|
89
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
90
|
+
data[:secrets] || {}
|
|
91
|
+
else
|
|
92
|
+
response.body
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_error('export', e)
|
|
96
|
+
format == :json ? {} : ''
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def provision(project_id:, app_name:)
|
|
100
|
+
response = request(
|
|
101
|
+
:post,
|
|
102
|
+
'/api/v1/projects/provision',
|
|
103
|
+
body: { project_id: project_id, app_name: app_name },
|
|
104
|
+
use_service_key: true
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
log_error('provision', e)
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get all provider keys for the current project
|
|
114
|
+
# Returns a hash of provider => decrypted_key
|
|
115
|
+
def get_provider_keys
|
|
116
|
+
response = request(:get, '/api/v1/provider_keys/bulk')
|
|
117
|
+
|
|
118
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
|
|
120
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
121
|
+
# Convert to simple hash: { openai: "sk-...", anthropic: "sk-..." }
|
|
122
|
+
keys = {}
|
|
123
|
+
(data[:keys] || []).each do |key_data|
|
|
124
|
+
keys[key_data[:provider].to_sym] = key_data[:key]
|
|
125
|
+
end
|
|
126
|
+
keys
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
log_error('get_provider_keys', e)
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get a specific provider key
|
|
133
|
+
def get_provider_key(provider:, model_type: 'llm')
|
|
134
|
+
response = request(
|
|
135
|
+
:get,
|
|
136
|
+
'/api/v1/provider_keys/resolve',
|
|
137
|
+
params: { provider: provider, model_type: model_type }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
141
|
+
|
|
142
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
143
|
+
data[:key]
|
|
144
|
+
rescue StandardError => e
|
|
145
|
+
log_error('get_provider_key', e)
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
|
|
152
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
153
|
+
|
|
154
|
+
uri.query = URI.encode_www_form(params) if params
|
|
155
|
+
|
|
156
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
157
|
+
http.use_ssl = uri.scheme == 'https'
|
|
158
|
+
http.open_timeout = 10
|
|
159
|
+
http.read_timeout = 30
|
|
160
|
+
|
|
161
|
+
request = case method
|
|
162
|
+
when :get
|
|
163
|
+
Net::HTTP::Get.new(uri)
|
|
164
|
+
when :post
|
|
165
|
+
Net::HTTP::Post.new(uri)
|
|
166
|
+
when :put
|
|
167
|
+
Net::HTTP::Put.new(uri)
|
|
168
|
+
when :delete
|
|
169
|
+
Net::HTTP::Delete.new(uri)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Set headers
|
|
173
|
+
request['Content-Type'] = 'application/json'
|
|
174
|
+
request['Accept'] = 'application/json'
|
|
175
|
+
|
|
176
|
+
if use_service_key
|
|
177
|
+
request['X-Service-Key'] = @config.vault_master_key || @config.secret_key
|
|
178
|
+
else
|
|
179
|
+
auth_key = @config.vault_api_key || @config.secret_key
|
|
180
|
+
request['Authorization'] = "Bearer #{auth_key}" if auth_key
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
headers.each { |k, v| request[k] = v }
|
|
184
|
+
|
|
185
|
+
# Set body
|
|
186
|
+
request.body = body.to_json if body
|
|
187
|
+
|
|
188
|
+
http.request(request)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def log_error(operation, error)
|
|
192
|
+
structured_error = ErrorHandler.wrap(error, service: 'Vault', operation: operation)
|
|
193
|
+
BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{structured_error.message}")
|
|
194
|
+
|
|
195
|
+
# Call on_error callback if configured
|
|
196
|
+
if @config.on_error
|
|
197
|
+
@config.on_error.call(structured_error, { service: 'Vault', operation: operation })
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def handle_response_error(response, operation)
|
|
202
|
+
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
|
|
203
|
+
|
|
204
|
+
structured_error = ErrorHandler.from_response(response, service: 'Vault', operation: operation)
|
|
205
|
+
BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{structured_error.message}")
|
|
206
|
+
|
|
207
|
+
# Call on_error callback if configured
|
|
208
|
+
if @config.on_error
|
|
209
|
+
@config.on_error.call(structured_error, { service: 'Vault', operation: operation })
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
structured_error
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Vault
|
|
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.vault_auto_provision
|
|
14
|
+
return unless valid_auth?
|
|
15
|
+
|
|
16
|
+
@provisioned = true
|
|
17
|
+
|
|
18
|
+
# Try to provision with Platform project ID
|
|
19
|
+
project_id = detect_project_id
|
|
20
|
+
return unless project_id
|
|
21
|
+
|
|
22
|
+
client = Client.new(@config)
|
|
23
|
+
client.provision(
|
|
24
|
+
project_id: project_id,
|
|
25
|
+
app_name: @config.app_name || @config.service
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
BrainzLab.debug_log("[Vault::Provisioner] Project provisioned: #{project_id}")
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
BrainzLab.debug_log("[Vault::Provisioner] Provisioning failed: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def valid_auth?
|
|
36
|
+
key = @config.vault_api_key || @config.vault_master_key || @config.secret_key
|
|
37
|
+
!key.nil? && !key.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detect_project_id
|
|
41
|
+
# Try environment variable first
|
|
42
|
+
return ENV['BRAINZLAB_PROJECT_ID'] if ENV['BRAINZLAB_PROJECT_ID']
|
|
43
|
+
|
|
44
|
+
# Could also detect from Platform API if we have a secret key
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|