brainzlab 0.1.10 → 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.
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ # Debug module for SDK operation logging
5
+ #
6
+ # Provides pretty-printed debug output for all SDK operations when debug mode is enabled.
7
+ # Includes timing information and request/response details.
8
+ #
9
+ # @example Enable debug mode
10
+ # BrainzLab.configure do |config|
11
+ # config.debug = true
12
+ # end
13
+ #
14
+ # @example Use custom logger
15
+ # BrainzLab.configure do |config|
16
+ # config.debug = true
17
+ # config.logger = Logger.new(STDOUT)
18
+ # end
19
+ #
20
+ # @example Manual debug logging
21
+ # BrainzLab::Debug.log("Custom message", level: :info)
22
+ #
23
+ module Debug
24
+ COLORS = {
25
+ reset: "\e[0m",
26
+ bold: "\e[1m",
27
+ dim: "\e[2m",
28
+ red: "\e[31m",
29
+ green: "\e[32m",
30
+ yellow: "\e[33m",
31
+ blue: "\e[34m",
32
+ magenta: "\e[35m",
33
+ cyan: "\e[36m",
34
+ white: "\e[37m",
35
+ gray: "\e[90m"
36
+ }.freeze
37
+
38
+ LEVEL_COLORS = {
39
+ debug: :gray,
40
+ info: :cyan,
41
+ warn: :yellow,
42
+ error: :red,
43
+ fatal: :red
44
+ }.freeze
45
+
46
+ LEVEL_LABELS = {
47
+ debug: 'DEBUG',
48
+ info: 'INFO',
49
+ warn: 'WARN',
50
+ error: 'ERROR',
51
+ fatal: 'FATAL'
52
+ }.freeze
53
+
54
+ class << self
55
+ # Log a debug message if debug mode is enabled
56
+ #
57
+ # @param message [String] The message to log
58
+ # @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
59
+ # @param data [Hash] Optional additional data to include
60
+ # @return [void]
61
+ def log(message, level: :info, **data)
62
+ return unless enabled?
63
+
64
+ output = format_message(message, level: level, **data)
65
+ write_output(output, level: level)
66
+ end
67
+
68
+ # Log an outgoing request
69
+ #
70
+ # @param service [String, Symbol] The service name (e.g., :recall, :reflex)
71
+ # @param method [String] HTTP method
72
+ # @param path [String] Request path
73
+ # @param data [Hash] Request payload summary
74
+ # @return [void]
75
+ def log_request(service, method, path, data: nil)
76
+ return unless enabled?
77
+
78
+ data_summary = summarize_data(data) if data
79
+ message = data_summary ? "#{method} #{path} #{data_summary}" : "#{method} #{path}"
80
+
81
+ output = format_arrow_message(:out, service, message)
82
+ write_output(output, level: :info)
83
+ end
84
+
85
+ # Log an incoming response
86
+ #
87
+ # @param service [String, Symbol] The service name
88
+ # @param status [Integer] HTTP status code
89
+ # @param duration_ms [Float] Request duration in milliseconds
90
+ # @param error [String, nil] Error message if request failed
91
+ # @return [void]
92
+ def log_response(service, status, duration_ms, error: nil)
93
+ return unless enabled?
94
+
95
+ status_text = status_message(status)
96
+ duration_text = format_duration(duration_ms)
97
+
98
+ message = if error
99
+ "#{status} #{status_text} (#{duration_text}) - #{error}"
100
+ else
101
+ "#{status} #{status_text} (#{duration_text})"
102
+ end
103
+
104
+ level = status >= 400 ? :error : :info
105
+ output = format_arrow_message(:in, service, message, level: level)
106
+ write_output(output, level: level)
107
+ end
108
+
109
+ # Log an SDK operation with timing
110
+ #
111
+ # @param service [String, Symbol] The service name
112
+ # @param operation [String] Operation description
113
+ # @param data [Hash] Operation data
114
+ # @return [void]
115
+ def log_operation(service, operation, **data)
116
+ return unless enabled?
117
+
118
+ data_summary = data.empty? ? '' : " (#{format_data_inline(data)})"
119
+ message = "#{operation}#{data_summary}"
120
+
121
+ output = format_arrow_message(:out, service, message)
122
+ write_output(output, level: :info)
123
+ end
124
+
125
+ # Measure and log execution time of a block
126
+ #
127
+ # @param service [String, Symbol] The service name
128
+ # @param operation [String] Operation description
129
+ # @yield Block to measure
130
+ # @return [Object] Result of the block
131
+ def measure(service, operation)
132
+ return yield unless enabled?
133
+
134
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
+ log_operation(service, operation)
136
+
137
+ result = yield
138
+
139
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
140
+ log("#{operation} completed", level: :debug, duration_ms: duration_ms, service: service.to_s)
141
+
142
+ result
143
+ rescue StandardError => e
144
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
145
+ log("#{operation} failed: #{e.message}", level: :error, duration_ms: duration_ms, service: service.to_s)
146
+ raise
147
+ end
148
+
149
+ # Check if debug mode is enabled
150
+ #
151
+ # @return [Boolean]
152
+ def enabled?
153
+ BrainzLab.configuration.debug?
154
+ end
155
+
156
+ # Check if colorized output should be used
157
+ #
158
+ # @return [Boolean]
159
+ def colorize?
160
+ return false unless enabled?
161
+ return @colorize if defined?(@colorize)
162
+
163
+ @colorize = $stdout.tty?
164
+ end
165
+
166
+ # Reset colorize detection (useful for testing)
167
+ def reset_colorize!
168
+ remove_instance_variable(:@colorize) if defined?(@colorize)
169
+ end
170
+
171
+ private
172
+
173
+ def format_message(message, level:, **data)
174
+ timestamp = format_timestamp
175
+ prefix = colorize("[BrainzLab]", :bold, :blue)
176
+ level_badge = format_level(level)
177
+ data_str = data.empty? ? '' : " #{format_data(data)}"
178
+
179
+ "#{prefix} #{timestamp} #{level_badge} #{message}#{data_str}"
180
+ end
181
+
182
+ def format_arrow_message(direction, service, message, level: :info)
183
+ timestamp = format_timestamp
184
+ prefix = colorize("[BrainzLab]", :bold, :blue)
185
+ arrow = direction == :out ? colorize("->", :cyan) : colorize("<-", :green)
186
+ service_name = colorize(service.to_s.capitalize, :magenta)
187
+
188
+ "#{prefix} #{timestamp} #{arrow} #{service_name} #{message}"
189
+ end
190
+
191
+ def format_timestamp
192
+ time = Time.now.strftime('%H:%M:%S')
193
+ colorize(time, :dim)
194
+ end
195
+
196
+ def format_level(level)
197
+ label = LEVEL_LABELS[level] || level.to_s.upcase
198
+ color = LEVEL_COLORS[level] || :white
199
+ colorize("[#{label}]", color)
200
+ end
201
+
202
+ def format_duration(ms)
203
+ if ms < 1
204
+ colorize("#{(ms * 1000).round(0)}us", :green)
205
+ elsif ms < 100
206
+ colorize("#{ms.round(1)}ms", :green)
207
+ elsif ms < 1000
208
+ colorize("#{ms.round(0)}ms", :yellow)
209
+ else
210
+ colorize("#{(ms / 1000.0).round(2)}s", :red)
211
+ end
212
+ end
213
+
214
+ def format_data(data)
215
+ pairs = data.map { |k, v| "#{k}: #{format_value(v)}" }
216
+ colorize("(#{pairs.join(', ')})", :dim)
217
+ end
218
+
219
+ def format_data_inline(data)
220
+ data.map { |k, v| "#{k}: #{format_value(v)}" }.join(', ')
221
+ end
222
+
223
+ def format_value(value)
224
+ case value
225
+ when String
226
+ value.length > 50 ? "#{value[0..47]}..." : value
227
+ when Hash
228
+ "{#{value.keys.join(', ')}}"
229
+ when Array
230
+ "[#{value.length} items]"
231
+ else
232
+ value.to_s
233
+ end
234
+ end
235
+
236
+ def summarize_data(data)
237
+ return nil unless data.is_a?(Hash)
238
+
239
+ summary_parts = []
240
+ summary_parts << "\"#{truncate(data[:message] || data['message'], 30)}\"" if data[:message] || data['message']
241
+
242
+ other_keys = data.keys.reject { |k| %i[message timestamp level].include?(k.to_sym) }
243
+ if other_keys.any?
244
+ key_summary = other_keys.take(3).map { |k| "#{k}: #{format_value(data[k])}" }.join(', ')
245
+ key_summary += ", ..." if other_keys.length > 3
246
+ summary_parts << "(#{key_summary})"
247
+ end
248
+
249
+ summary_parts.join(' ')
250
+ end
251
+
252
+ def truncate(str, length)
253
+ return '' unless str
254
+
255
+ str = str.to_s
256
+ str.length > length ? "#{str[0..length - 4]}..." : str
257
+ end
258
+
259
+ def status_message(status)
260
+ case status
261
+ when 200 then 'OK'
262
+ when 201 then 'Created'
263
+ when 204 then 'No Content'
264
+ when 400 then 'Bad Request'
265
+ when 401 then 'Unauthorized'
266
+ when 403 then 'Forbidden'
267
+ when 404 then 'Not Found'
268
+ when 422 then 'Unprocessable Entity'
269
+ when 429 then 'Too Many Requests'
270
+ when 500 then 'Internal Server Error'
271
+ when 502 then 'Bad Gateway'
272
+ when 503 then 'Service Unavailable'
273
+ else ''
274
+ end
275
+ end
276
+
277
+ def colorize(text, *colors)
278
+ return text unless colorize?
279
+
280
+ color_codes = colors.map { |c| COLORS[c] }.compact.join
281
+ "#{color_codes}#{text}#{COLORS[:reset]}"
282
+ end
283
+
284
+ def write_output(output, level:)
285
+ config = BrainzLab.configuration
286
+
287
+ if config.logger
288
+ case level
289
+ when :debug then config.logger.debug(strip_colors(output))
290
+ when :info then config.logger.info(strip_colors(output))
291
+ when :warn then config.logger.warn(strip_colors(output))
292
+ when :error, :fatal then config.logger.error(strip_colors(output))
293
+ else config.logger.info(strip_colors(output))
294
+ end
295
+ else
296
+ $stderr.puts output
297
+ end
298
+ end
299
+
300
+ def strip_colors(text)
301
+ text.gsub(/\e\[\d+m/, '')
302
+ end
303
+ end
304
+ end
305
+ end
@@ -223,7 +223,27 @@ module BrainzLab
223
223
  end
224
224
 
225
225
  def log_error(operation, error)
226
- BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{error.message}")
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