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.
@@ -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