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,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Utilities
|
|
5
|
+
# Beautiful log formatter for Rails development
|
|
6
|
+
# Provides colorized, structured output with request timing
|
|
7
|
+
#
|
|
8
|
+
# @example Usage in Rails
|
|
9
|
+
# # config/environments/development.rb
|
|
10
|
+
# config.log_formatter = BrainzLab::Utilities::LogFormatter.new
|
|
11
|
+
#
|
|
12
|
+
# # Or use the Rails integration
|
|
13
|
+
# BrainzLab::Utilities::LogFormatter.install!
|
|
14
|
+
#
|
|
15
|
+
class LogFormatter < ::Logger::Formatter
|
|
16
|
+
COLORS = {
|
|
17
|
+
debug: "\e[36m", # Cyan
|
|
18
|
+
info: "\e[32m", # Green
|
|
19
|
+
warn: "\e[33m", # Yellow
|
|
20
|
+
error: "\e[31m", # Red
|
|
21
|
+
fatal: "\e[35m", # Magenta
|
|
22
|
+
reset: "\e[0m",
|
|
23
|
+
dim: "\e[2m",
|
|
24
|
+
bold: "\e[1m",
|
|
25
|
+
blue: "\e[34m",
|
|
26
|
+
gray: "\e[90m"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
SEVERITY_ICONS = {
|
|
30
|
+
"DEBUG" => "🔍",
|
|
31
|
+
"INFO" => "ℹ️ ",
|
|
32
|
+
"WARN" => "⚠️ ",
|
|
33
|
+
"ERROR" => "❌",
|
|
34
|
+
"FATAL" => "💀"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
HTTP_METHODS = {
|
|
38
|
+
"GET" => "\e[32m", # Green
|
|
39
|
+
"POST" => "\e[33m", # Yellow
|
|
40
|
+
"PUT" => "\e[34m", # Blue
|
|
41
|
+
"PATCH" => "\e[34m", # Blue
|
|
42
|
+
"DELETE" => "\e[31m", # Red
|
|
43
|
+
"HEAD" => "\e[36m", # Cyan
|
|
44
|
+
"OPTIONS" => "\e[36m" # Cyan
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
def initialize(colorize: nil, show_timestamp: true, show_severity: true, compact: false)
|
|
48
|
+
super()
|
|
49
|
+
@colorize = colorize.nil? ? $stdout.tty? : colorize
|
|
50
|
+
@show_timestamp = show_timestamp
|
|
51
|
+
@show_severity = show_severity
|
|
52
|
+
@compact = compact
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def call(severity, timestamp, progname, msg)
|
|
56
|
+
return "" if msg.nil? || msg.to_s.strip.empty?
|
|
57
|
+
|
|
58
|
+
message = format_message(msg)
|
|
59
|
+
return "" if skip_message?(message)
|
|
60
|
+
|
|
61
|
+
formatted = build_output(severity, timestamp, progname, message)
|
|
62
|
+
"#{formatted}\n"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Install as Rails logger formatter
|
|
66
|
+
def self.install!
|
|
67
|
+
return unless defined?(Rails)
|
|
68
|
+
|
|
69
|
+
Rails.application.configure do
|
|
70
|
+
config.log_formatter = BrainzLab::Utilities::LogFormatter.new(
|
|
71
|
+
colorize: BrainzLab.configuration.log_formatter_colors,
|
|
72
|
+
compact: BrainzLab.configuration.log_formatter_compact_assets
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Also hook into ActiveSupport::TaggedLogging if present
|
|
77
|
+
if defined?(ActiveSupport::TaggedLogging) && Rails.logger.respond_to?(:formatter=)
|
|
78
|
+
Rails.logger.formatter = new
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def format_message(msg)
|
|
85
|
+
case msg
|
|
86
|
+
when String
|
|
87
|
+
msg
|
|
88
|
+
when Exception
|
|
89
|
+
"#{msg.class}: #{msg.message}\n#{msg.backtrace&.first(10)&.join("\n")}"
|
|
90
|
+
else
|
|
91
|
+
msg.inspect
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def skip_message?(message)
|
|
96
|
+
return false unless BrainzLab.configuration.log_formatter_hide_assets
|
|
97
|
+
|
|
98
|
+
# Skip asset pipeline noise
|
|
99
|
+
message.include?("/assets/") ||
|
|
100
|
+
message.include?("Asset pipeline") ||
|
|
101
|
+
message.match?(/Started GET "\/assets\//)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_output(severity, timestamp, progname, message)
|
|
105
|
+
parts = []
|
|
106
|
+
|
|
107
|
+
if @show_timestamp
|
|
108
|
+
ts = colorize(timestamp.strftime("%H:%M:%S.%L"), :gray)
|
|
109
|
+
parts << ts
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if @show_severity
|
|
113
|
+
sev = format_severity(severity)
|
|
114
|
+
parts << sev
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
parts << format_content(message, severity)
|
|
118
|
+
|
|
119
|
+
parts.join(" ")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def format_severity(severity)
|
|
123
|
+
icon = SEVERITY_ICONS[severity] || ""
|
|
124
|
+
text = severity.ljust(5)
|
|
125
|
+
|
|
126
|
+
if @colorize
|
|
127
|
+
color = severity_color(severity)
|
|
128
|
+
"#{icon}#{color}#{text}#{COLORS[:reset]}"
|
|
129
|
+
else
|
|
130
|
+
"#{icon}[#{text}]"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def severity_color(severity)
|
|
135
|
+
case severity
|
|
136
|
+
when "DEBUG" then COLORS[:debug]
|
|
137
|
+
when "INFO" then COLORS[:info]
|
|
138
|
+
when "WARN" then COLORS[:warn]
|
|
139
|
+
when "ERROR" then COLORS[:error]
|
|
140
|
+
when "FATAL" then COLORS[:fatal]
|
|
141
|
+
else COLORS[:reset]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_content(message, severity)
|
|
146
|
+
# Handle Rails request log patterns
|
|
147
|
+
if (request_match = message.match(/Started (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) "([^"]+)"/))
|
|
148
|
+
format_request_started(request_match[1], request_match[2])
|
|
149
|
+
elsif (completed_match = message.match(/Completed (\d+) .+ in (\d+(?:\.\d+)?)ms/))
|
|
150
|
+
format_request_completed(completed_match[1].to_i, completed_match[2].to_f)
|
|
151
|
+
elsif message.include?("Processing by")
|
|
152
|
+
format_processing(message)
|
|
153
|
+
elsif message.include?("Parameters:")
|
|
154
|
+
format_parameters(message)
|
|
155
|
+
elsif message.include?("Rendering") || message.include?("Rendered")
|
|
156
|
+
format_rendering(message)
|
|
157
|
+
elsif severity == "ERROR" || severity == "FATAL"
|
|
158
|
+
format_error(message)
|
|
159
|
+
else
|
|
160
|
+
message
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def format_request_started(method, path)
|
|
165
|
+
method_color = HTTP_METHODS[method] || COLORS[:reset]
|
|
166
|
+
|
|
167
|
+
if @colorize
|
|
168
|
+
"#{COLORS[:bold]}→#{COLORS[:reset]} #{method_color}#{method}#{COLORS[:reset]} #{path}"
|
|
169
|
+
else
|
|
170
|
+
"→ #{method} #{path}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_request_completed(status, duration)
|
|
175
|
+
status_color = case status
|
|
176
|
+
when 200..299 then COLORS[:info]
|
|
177
|
+
when 300..399 then COLORS[:blue]
|
|
178
|
+
when 400..499 then COLORS[:warn]
|
|
179
|
+
when 500..599 then COLORS[:error]
|
|
180
|
+
else COLORS[:reset]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
duration_color = case duration
|
|
184
|
+
when 0..100 then COLORS[:info]
|
|
185
|
+
when 100..500 then COLORS[:warn]
|
|
186
|
+
else COLORS[:error]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if @colorize
|
|
190
|
+
"#{COLORS[:bold]}←#{COLORS[:reset]} #{status_color}#{status}#{COLORS[:reset]} #{duration_color}#{duration.round(1)}ms#{COLORS[:reset]}"
|
|
191
|
+
else
|
|
192
|
+
"← #{status} #{duration.round(1)}ms"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def format_processing(message)
|
|
197
|
+
if (match = message.match(/Processing by (\w+)#(\w+)/))
|
|
198
|
+
controller, action = match.captures
|
|
199
|
+
if @colorize
|
|
200
|
+
" #{COLORS[:dim]}#{controller}##{action}#{COLORS[:reset]}"
|
|
201
|
+
else
|
|
202
|
+
" #{controller}##{action}"
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
" #{message}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def format_parameters(message)
|
|
210
|
+
return message unless BrainzLab.configuration.log_formatter_show_params
|
|
211
|
+
|
|
212
|
+
if @colorize
|
|
213
|
+
" #{COLORS[:dim]}#{message}#{COLORS[:reset]}"
|
|
214
|
+
else
|
|
215
|
+
" #{message}"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def format_rendering(message)
|
|
220
|
+
if @compact
|
|
221
|
+
# Compact: just show the template name
|
|
222
|
+
if (match = message.match(/Render(?:ed|ing) ([^\s]+)/))
|
|
223
|
+
template = match[1].split("/").last
|
|
224
|
+
if @colorize
|
|
225
|
+
" #{COLORS[:gray]}#{template}#{COLORS[:reset]}"
|
|
226
|
+
else
|
|
227
|
+
" #{template}"
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
""
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
if @colorize
|
|
234
|
+
" #{COLORS[:dim]}#{message}#{COLORS[:reset]}"
|
|
235
|
+
else
|
|
236
|
+
" #{message}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def format_error(message)
|
|
242
|
+
if @colorize
|
|
243
|
+
"#{COLORS[:error]}#{message}#{COLORS[:reset]}"
|
|
244
|
+
else
|
|
245
|
+
message
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def colorize(text, color)
|
|
250
|
+
return text unless @colorize
|
|
251
|
+
|
|
252
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -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, reset = 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
|
+
bucket = 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
|