brainzlab 0.1.11 → 0.1.12
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/README.md +210 -3
- data/lib/brainzlab/beacon/client.rb +21 -1
- data/lib/brainzlab/configuration.rb +51 -2
- data/lib/brainzlab/cortex/client.rb +21 -1
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +21 -1
- 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 +245 -109
- data/lib/brainzlab/devtools/assets/devtools.js +40 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/nerve/client.rb +21 -1
- data/lib/brainzlab/pulse/client.rb +66 -5
- data/lib/brainzlab/pulse.rb +17 -4
- data/lib/brainzlab/recall/client.rb +74 -6
- data/lib/brainzlab/recall.rb +19 -2
- data/lib/brainzlab/reflex/client.rb +66 -5
- data/lib/brainzlab/reflex.rb +40 -8
- data/lib/brainzlab/sentinel/client.rb +21 -1
- data/lib/brainzlab/synapse/client.rb +21 -1
- 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 +32 -3
- data/lib/brainzlab/vault/client.rb +21 -1
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +53 -6
- data/lib/brainzlab.rb +42 -0
- metadata +24 -1
|
@@ -223,7 +223,27 @@ module BrainzLab
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
def log_error(operation, error)
|
|
226
|
-
|
|
226
|
+
structured_error = ErrorHandler.wrap(error, service: 'Dendrite', operation: operation)
|
|
227
|
+
BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
|
|
228
|
+
|
|
229
|
+
# Call on_error callback if configured
|
|
230
|
+
if @config.on_error
|
|
231
|
+
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_response_error(response, operation)
|
|
236
|
+
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent) || response.is_a?(Net::HTTPAccepted)
|
|
237
|
+
|
|
238
|
+
structured_error = ErrorHandler.from_response(response, service: 'Dendrite', operation: operation)
|
|
239
|
+
BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
|
|
240
|
+
|
|
241
|
+
# Call on_error callback if configured
|
|
242
|
+
if @config.on_error
|
|
243
|
+
@config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
structured_error
|
|
227
247
|
end
|
|
228
248
|
end
|
|
229
249
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BrainzLab
|
|
6
|
+
module Development
|
|
7
|
+
# Pretty-prints development mode events to stdout
|
|
8
|
+
class Logger
|
|
9
|
+
# ANSI color codes
|
|
10
|
+
COLORS = {
|
|
11
|
+
reset: "\e[0m",
|
|
12
|
+
bold: "\e[1m",
|
|
13
|
+
dim: "\e[2m",
|
|
14
|
+
# Services
|
|
15
|
+
recall: "\e[36m", # Cyan
|
|
16
|
+
reflex: "\e[31m", # Red
|
|
17
|
+
pulse: "\e[33m", # Yellow
|
|
18
|
+
flux: "\e[35m", # Magenta
|
|
19
|
+
signal: "\e[32m", # Green
|
|
20
|
+
vault: "\e[34m", # Blue
|
|
21
|
+
vision: "\e[95m", # Light magenta
|
|
22
|
+
cortex: "\e[96m", # Light cyan
|
|
23
|
+
beacon: "\e[92m", # Light green
|
|
24
|
+
nerve: "\e[93m", # Light yellow
|
|
25
|
+
dendrite: "\e[94m", # Light blue
|
|
26
|
+
sentinel: "\e[91m", # Light red
|
|
27
|
+
synapse: "\e[97m", # White
|
|
28
|
+
# Log levels
|
|
29
|
+
debug: "\e[37m", # Gray
|
|
30
|
+
info: "\e[32m", # Green
|
|
31
|
+
warn: "\e[33m", # Yellow
|
|
32
|
+
error: "\e[31m", # Red
|
|
33
|
+
fatal: "\e[35m" # Magenta
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def initialize(output: $stdout, colors: nil)
|
|
37
|
+
@output = output
|
|
38
|
+
@colors = colors.nil? ? tty? : colors
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Log an event to stdout in a readable format
|
|
42
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
43
|
+
# @param event_type [String] type of event
|
|
44
|
+
# @param payload [Hash] event data
|
|
45
|
+
def log(service:, event_type:, payload:)
|
|
46
|
+
timestamp = Time.now.strftime('%H:%M:%S.%L')
|
|
47
|
+
service_color = COLORS[service] || COLORS[:reset]
|
|
48
|
+
|
|
49
|
+
# Build the log line
|
|
50
|
+
parts = []
|
|
51
|
+
parts << colorize("[#{timestamp}]", :dim)
|
|
52
|
+
parts << colorize("[#{service.to_s.upcase}]", service_color, bold: true)
|
|
53
|
+
parts << colorize(event_type, :bold)
|
|
54
|
+
|
|
55
|
+
# Add message or name depending on event type
|
|
56
|
+
message = extract_message(payload, event_type)
|
|
57
|
+
parts << message if message
|
|
58
|
+
|
|
59
|
+
# Print the main line
|
|
60
|
+
@output.puts parts.join(' ')
|
|
61
|
+
|
|
62
|
+
# Print additional details indented
|
|
63
|
+
print_details(payload, event_type)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def tty?
|
|
69
|
+
@output.respond_to?(:tty?) && @output.tty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def colorize(text, color, bold: false)
|
|
73
|
+
return text unless @colors
|
|
74
|
+
|
|
75
|
+
color_code = color.is_a?(Symbol) ? COLORS[color] : color
|
|
76
|
+
prefix = bold ? "#{COLORS[:bold]}#{color_code}" : color_code.to_s
|
|
77
|
+
"#{prefix}#{text}#{COLORS[:reset]}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_message(payload, event_type)
|
|
81
|
+
case event_type
|
|
82
|
+
when 'log'
|
|
83
|
+
level = payload[:level]&.to_sym
|
|
84
|
+
level_color = COLORS[level] || COLORS[:info]
|
|
85
|
+
msg = "#{colorize("[#{level&.upcase}]", level_color)} #{payload[:message]}"
|
|
86
|
+
msg
|
|
87
|
+
when 'error'
|
|
88
|
+
"#{payload[:error_class]}: #{payload[:message]}"
|
|
89
|
+
when 'trace'
|
|
90
|
+
duration = payload[:duration_ms] ? "(#{payload[:duration_ms]}ms)" : ''
|
|
91
|
+
"#{payload[:name]} #{duration}"
|
|
92
|
+
when 'metric'
|
|
93
|
+
"#{payload[:name]} = #{payload[:value]}"
|
|
94
|
+
when 'span'
|
|
95
|
+
duration = payload[:duration_ms] ? "(#{payload[:duration_ms]}ms)" : ''
|
|
96
|
+
"#{payload[:name]} #{duration}"
|
|
97
|
+
else
|
|
98
|
+
payload[:message] || payload[:name]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_details(payload, event_type)
|
|
103
|
+
details = extract_details(payload, event_type)
|
|
104
|
+
return if details.empty?
|
|
105
|
+
|
|
106
|
+
details.each do |key, value|
|
|
107
|
+
formatted_value = format_value(value)
|
|
108
|
+
@output.puts " #{colorize(key.to_s, :dim)}: #{formatted_value}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_details(payload, event_type)
|
|
113
|
+
# Fields to exclude from details (already shown in main line)
|
|
114
|
+
excluded = %i[timestamp message level name kind]
|
|
115
|
+
|
|
116
|
+
case event_type
|
|
117
|
+
when 'log'
|
|
118
|
+
payload.except(*excluded, :environment, :service, :host)
|
|
119
|
+
when 'error'
|
|
120
|
+
payload.slice(:error_class, :environment, :request_id, :user, :tags).compact
|
|
121
|
+
when 'trace'
|
|
122
|
+
payload.slice(:request_method, :request_path, :status, :db_ms, :view_ms, :spans).compact
|
|
123
|
+
when 'metric'
|
|
124
|
+
payload.slice(:kind, :tags).compact
|
|
125
|
+
else
|
|
126
|
+
payload.except(*excluded)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_value(value)
|
|
131
|
+
case value
|
|
132
|
+
when Hash
|
|
133
|
+
if value.size <= 3
|
|
134
|
+
value.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')
|
|
135
|
+
else
|
|
136
|
+
"\n #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
|
|
137
|
+
end
|
|
138
|
+
when Array
|
|
139
|
+
if value.size <= 3 && value.all? { |v| v.is_a?(String) || v.is_a?(Numeric) }
|
|
140
|
+
value.inspect
|
|
141
|
+
else
|
|
142
|
+
"\n #{JSON.pretty_generate(value).gsub("\n", "\n ")}"
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
value.inspect
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sqlite3'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Development
|
|
9
|
+
# SQLite-backed store for development mode events
|
|
10
|
+
class Store
|
|
11
|
+
DEFAULT_PATH = 'tmp/brainzlab.sqlite3'
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
@db_path = config.development_db_path || DEFAULT_PATH
|
|
16
|
+
@db = nil
|
|
17
|
+
ensure_database!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Insert an event into the store
|
|
21
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
22
|
+
# @param event_type [String] type of event
|
|
23
|
+
# @param payload [Hash] event data
|
|
24
|
+
def insert(service:, event_type:, payload:)
|
|
25
|
+
db.execute(
|
|
26
|
+
'INSERT INTO events (service, event_type, payload, created_at) VALUES (?, ?, ?, ?)',
|
|
27
|
+
[service.to_s, event_type.to_s, JSON.generate(payload), Time.now.utc.iso8601(3)]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Query events from the store
|
|
32
|
+
# @param service [Symbol, nil] filter by service
|
|
33
|
+
# @param event_type [String, nil] filter by event type
|
|
34
|
+
# @param since [Time, nil] filter events after this time
|
|
35
|
+
# @param limit [Integer] max number of events to return
|
|
36
|
+
# @return [Array<Hash>] matching events
|
|
37
|
+
def query(service: nil, event_type: nil, since: nil, limit: 100)
|
|
38
|
+
conditions = []
|
|
39
|
+
params = []
|
|
40
|
+
|
|
41
|
+
if service
|
|
42
|
+
conditions << 'service = ?'
|
|
43
|
+
params << service.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if event_type
|
|
47
|
+
conditions << 'event_type = ?'
|
|
48
|
+
params << event_type.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if since
|
|
52
|
+
conditions << 'created_at >= ?'
|
|
53
|
+
params << since.utc.iso8601(3)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
where_clause = conditions.empty? ? '' : "WHERE #{conditions.join(' AND ')}"
|
|
57
|
+
params << limit
|
|
58
|
+
|
|
59
|
+
sql = "SELECT id, service, event_type, payload, created_at FROM events #{where_clause} ORDER BY created_at DESC LIMIT ?"
|
|
60
|
+
|
|
61
|
+
db.execute(sql, params).map do |row|
|
|
62
|
+
{
|
|
63
|
+
id: row[0],
|
|
64
|
+
service: row[1].to_sym,
|
|
65
|
+
event_type: row[2],
|
|
66
|
+
payload: JSON.parse(row[3], symbolize_names: true),
|
|
67
|
+
created_at: Time.parse(row[4])
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get event counts by service
|
|
73
|
+
def stats
|
|
74
|
+
results = db.execute('SELECT service, COUNT(*) as count FROM events GROUP BY service')
|
|
75
|
+
results.to_h { |row| [row[0].to_sym, row[1]] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clear all events
|
|
79
|
+
def clear!
|
|
80
|
+
db.execute('DELETE FROM events')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Close the database connection
|
|
84
|
+
def close
|
|
85
|
+
@db&.close
|
|
86
|
+
@db = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def db
|
|
92
|
+
@db ||= begin
|
|
93
|
+
SQLite3::Database.new(@db_path).tap do |database|
|
|
94
|
+
database.results_as_hash = false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ensure_database!
|
|
100
|
+
# Ensure the directory exists
|
|
101
|
+
FileUtils.mkdir_p(File.dirname(@db_path))
|
|
102
|
+
|
|
103
|
+
# Create the events table if it doesn't exist
|
|
104
|
+
db.execute(<<~SQL)
|
|
105
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
106
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
107
|
+
service TEXT NOT NULL,
|
|
108
|
+
event_type TEXT NOT NULL,
|
|
109
|
+
payload TEXT NOT NULL,
|
|
110
|
+
created_at TEXT NOT NULL
|
|
111
|
+
)
|
|
112
|
+
SQL
|
|
113
|
+
|
|
114
|
+
# Create indexes for common queries
|
|
115
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_service ON events(service)')
|
|
116
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type)')
|
|
117
|
+
db.execute('CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at)')
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'development/store'
|
|
4
|
+
require_relative 'development/logger'
|
|
5
|
+
|
|
6
|
+
module BrainzLab
|
|
7
|
+
# Development mode support for offline SDK usage
|
|
8
|
+
# Logs all events to stdout and stores them locally in SQLite
|
|
9
|
+
module Development
|
|
10
|
+
class << self
|
|
11
|
+
# Check if development mode is enabled
|
|
12
|
+
def enabled?
|
|
13
|
+
BrainzLab.configuration.mode == :development
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Get the store instance
|
|
17
|
+
def store
|
|
18
|
+
@store ||= Store.new(BrainzLab.configuration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the development logger
|
|
22
|
+
def logger
|
|
23
|
+
@logger ||= Logger.new(output: BrainzLab.configuration.development_log_output || $stdout)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Record an event from any service
|
|
27
|
+
# @param service [Symbol] :recall, :reflex, :pulse, etc.
|
|
28
|
+
# @param event_type [String] type of event (log, error, trace, metric, etc.)
|
|
29
|
+
# @param payload [Hash] event data
|
|
30
|
+
def record(service:, event_type:, payload:)
|
|
31
|
+
return unless enabled?
|
|
32
|
+
|
|
33
|
+
# Log to stdout
|
|
34
|
+
logger.log(service: service, event_type: event_type, payload: payload)
|
|
35
|
+
|
|
36
|
+
# Store in SQLite
|
|
37
|
+
store.insert(service: service, event_type: event_type, payload: payload)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Query stored events
|
|
41
|
+
# @param service [Symbol, nil] filter by service
|
|
42
|
+
# @param event_type [String, nil] filter by event type
|
|
43
|
+
# @param since [Time, nil] filter events after this time
|
|
44
|
+
# @param limit [Integer] max number of events to return (default: 100)
|
|
45
|
+
# @return [Array<Hash>] matching events
|
|
46
|
+
def events(service: nil, event_type: nil, since: nil, limit: 100)
|
|
47
|
+
return [] unless enabled?
|
|
48
|
+
|
|
49
|
+
store.query(service: service, event_type: event_type, since: since, limit: limit)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Clear all stored events
|
|
53
|
+
def clear!
|
|
54
|
+
store.clear! if enabled?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reset the development module (for testing)
|
|
58
|
+
def reset!
|
|
59
|
+
@store&.close
|
|
60
|
+
@store = nil
|
|
61
|
+
@logger = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get event counts by service
|
|
65
|
+
def stats
|
|
66
|
+
return {} unless enabled?
|
|
67
|
+
|
|
68
|
+
store.stats
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|