brainzlab 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +30 -0
  4. data/lib/brainzlab/beacon/client.rb +209 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +341 -3
  8. data/lib/brainzlab/cortex/cache.rb +59 -0
  9. data/lib/brainzlab/cortex/client.rb +141 -0
  10. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  11. data/lib/brainzlab/cortex.rb +227 -0
  12. data/lib/brainzlab/dendrite/client.rb +232 -0
  13. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  14. data/lib/brainzlab/dendrite.rb +195 -0
  15. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  16. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  17. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  18. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  19. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  20. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  21. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  22. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  23. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  24. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  25. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  26. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  27. data/lib/brainzlab/devtools.rb +75 -0
  28. data/lib/brainzlab/flux/buffer.rb +96 -0
  29. data/lib/brainzlab/flux/client.rb +70 -0
  30. data/lib/brainzlab/flux/provisioner.rb +57 -0
  31. data/lib/brainzlab/flux.rb +174 -0
  32. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  33. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  34. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  35. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  36. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  37. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  38. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  39. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  40. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  41. data/lib/brainzlab/instrumentation.rb +72 -0
  42. data/lib/brainzlab/nerve/client.rb +217 -0
  43. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  44. data/lib/brainzlab/nerve.rb +219 -0
  45. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  46. data/lib/brainzlab/pulse/propagation.rb +1 -1
  47. data/lib/brainzlab/pulse/tracer.rb +1 -1
  48. data/lib/brainzlab/pulse.rb +1 -1
  49. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  50. data/lib/brainzlab/rails/railtie.rb +36 -3
  51. data/lib/brainzlab/recall/provisioner.rb +17 -0
  52. data/lib/brainzlab/recall.rb +6 -1
  53. data/lib/brainzlab/reflex.rb +20 -5
  54. data/lib/brainzlab/sentinel/client.rb +218 -0
  55. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  56. data/lib/brainzlab/sentinel.rb +165 -0
  57. data/lib/brainzlab/signal/client.rb +62 -0
  58. data/lib/brainzlab/signal/provisioner.rb +55 -0
  59. data/lib/brainzlab/signal.rb +136 -0
  60. data/lib/brainzlab/synapse/client.rb +290 -0
  61. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  62. data/lib/brainzlab/synapse.rb +270 -0
  63. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  64. data/lib/brainzlab/utilities/health_check.rb +296 -0
  65. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  66. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  67. data/lib/brainzlab/utilities.rb +17 -0
  68. data/lib/brainzlab/vault/cache.rb +80 -0
  69. data/lib/brainzlab/vault/client.rb +198 -0
  70. data/lib/brainzlab/vault/provisioner.rb +49 -0
  71. data/lib/brainzlab/vault.rb +268 -0
  72. data/lib/brainzlab/version.rb +1 -1
  73. data/lib/brainzlab/vision/client.rb +128 -0
  74. data/lib/brainzlab/vision/provisioner.rb +136 -0
  75. data/lib/brainzlab/vision.rb +157 -0
  76. data/lib/brainzlab.rb +101 -0
  77. metadata +62 -2
@@ -0,0 +1,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