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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49af0bcd7ebaf8cc4b66040ae38be6eb6e8826178e493283b9241083ef54b79f
4
- data.tar.gz: '0057911ea7e6c7113e06a148863dc7e806e7af32f0bd9eb6f579bb7b69377849'
3
+ metadata.gz: 85a5cf3a64e256a569be39f7caa1ec6268755616ac6037db51f1543ee74dd50c
4
+ data.tar.gz: '09c531bb0793d26db1f62faf41f9ead294130a7036f6e5f6ab270fd8c6c46040'
5
5
  SHA512:
6
- metadata.gz: 91b591d98847edc9f401e6f505df003a1a802319db212615dab3a33a09fd320cde743c9c0dac40717dade391434a127ffa0975b28ec903f07a3907ab3f78f8fd
7
- data.tar.gz: a9f1105138dc3a9871854e9a6526381950b66c0f2d4e18dcc40b2f434cb55d7f7af3bd2a27933401fa4ebb1b658d68fcf420b3476ebbea7a15944d543c16ab76
6
+ metadata.gz: 28c2f10fc4457d4f2f34c5bcf4d63c14a062d4fcd8437b13cbcebc1a062976361b2014f5567e0fff82682a054fa83363f7e955a23d3d59ae285a9f06500c79e6
7
+ data.tar.gz: 5d130bf7242d57c3149803649262995daf298483c406002ab99e01495efb14df7a6874b8eb485daa90c79da8903ad37f6516ba0ca7e2c8930ff069a1b536a206
data/README.md CHANGED
@@ -298,6 +298,8 @@ end
298
298
  | `BRAINZLAB_SERVICE` | Service name |
299
299
  | `BRAINZLAB_APP_NAME` | App name for auto-provisioning |
300
300
  | `BRAINZLAB_DEBUG` | Enable debug logging (`true`/`false`) |
301
+ | `BRAINZLAB_MODE` | SDK mode: `production` (default) or `development` (offline) |
302
+ | `BRAINZLAB_DEV_DB_PATH` | SQLite database path for development mode |
301
303
  | `RECALL_URL` | Custom Recall endpoint |
302
304
  | `REFLEX_URL` | Custom Reflex endpoint |
303
305
  | `PULSE_URL` | Custom Pulse endpoint |
@@ -312,13 +314,218 @@ config.scrub_fields = [:password, :password_confirmation, :token, :api_key, :sec
312
314
 
313
315
  ## Debug Mode
314
316
 
315
- Enable debug mode to see SDK activity:
317
+ Debug mode provides detailed visibility into SDK operations, including all API requests and responses with timing information. This is invaluable for troubleshooting integration issues.
318
+
319
+ ### Enabling Debug Mode
316
320
 
317
321
  ```ruby
318
- config.debug = true
319
- # Or set BRAINZLAB_DEBUG=true
322
+ # In your initializer
323
+ BrainzLab.configure do |config|
324
+ config.debug = true
325
+ end
326
+
327
+ # Or via environment variable
328
+ # BRAINZLAB_DEBUG=true
329
+ ```
330
+
331
+ ### Debug Output Format
332
+
333
+ When debug mode is enabled, you'll see colorized output in your terminal:
334
+
335
+ ```
336
+ [BrainzLab] 12:34:56 -> Recall POST /api/v1/logs (count: 5)
337
+ [BrainzLab] 12:34:56 <- Recall 200 OK (45ms)
338
+
339
+ [BrainzLab] 12:34:57 -> Reflex POST /api/v1/errors (exception: RuntimeError)
340
+ [BrainzLab] 12:34:57 <- Reflex 201 Created (23ms)
341
+
342
+ [BrainzLab] 12:34:58 -> Pulse POST /api/v1/traces (name: GET /users)
343
+ [BrainzLab] 12:34:58 <- Pulse 200 OK (18ms)
344
+ ```
345
+
346
+ The output includes:
347
+ - Timestamp for each operation
348
+ - Service name (Recall, Reflex, Pulse, etc.)
349
+ - Request method and path
350
+ - Payload summary (log count, exception type, etc.)
351
+ - Response status code and message
352
+ - Request duration with color coding (green < 100ms, yellow < 1s, red > 1s)
353
+
354
+ ### Custom Logger
355
+
356
+ You can provide your own logger to capture debug output:
357
+
358
+ ```ruby
359
+ BrainzLab.configure do |config|
360
+ config.debug = true
361
+ config.logger = Rails.logger
362
+ # Or any Logger-compatible object
363
+ config.logger = Logger.new('log/brainzlab.log')
364
+ end
320
365
  ```
321
366
 
367
+ ### Debug Callbacks
368
+
369
+ For advanced debugging and monitoring, you can hook into SDK operations:
370
+
371
+ ```ruby
372
+ BrainzLab.configure do |config|
373
+ # Called before each API request
374
+ config.on_send = ->(service, method, path, payload) {
375
+ Rails.logger.debug "[BrainzLab] Sending to #{service}: #{method} #{path}"
376
+
377
+ # You can use this to:
378
+ # - Log all outgoing requests
379
+ # - Send metrics to your monitoring system
380
+ # - Add custom tracing
381
+ }
382
+
383
+ # Called when an SDK error occurs
384
+ config.on_error = ->(error, context) {
385
+ Rails.logger.error "[BrainzLab] Error in #{context[:service]}: #{error.message}"
386
+
387
+ # You can use this to:
388
+ # - Alert on SDK failures
389
+ # - Track error rates
390
+ # - Fallback to alternative logging
391
+
392
+ # Note: This is for SDK errors, not application errors
393
+ # Application errors are sent to Reflex as normal
394
+ }
395
+ end
396
+ ```
397
+
398
+ ### Programmatic Debug Logging
399
+
400
+ You can also use the Debug module directly:
401
+
402
+ ```ruby
403
+ # Log a debug message (only outputs when debug=true)
404
+ BrainzLab::Debug.log("Custom message", level: :info)
405
+ BrainzLab::Debug.log("Something went wrong", level: :error, error_code: 500)
406
+
407
+ # Measure operation timing
408
+ BrainzLab::Debug.measure(:custom, "expensive_operation") do
409
+ # Your code here
410
+ end
411
+
412
+ # Check if debug mode is enabled
413
+ if BrainzLab::Debug.enabled?
414
+ # Perform additional debug operations
415
+ end
416
+ ```
417
+
418
+ ### Debug Output Levels
419
+
420
+ Debug messages are color-coded by level:
421
+ - **DEBUG** (gray) - Verbose internal operations
422
+ - **INFO** (cyan) - Normal operations
423
+ - **WARN** (yellow) - Potential issues
424
+ - **ERROR** (red) - Failed operations
425
+
426
+ ## Development Mode
427
+
428
+ Development mode allows you to use the SDK without a BrainzLab server connection. Events are logged to stdout in a readable format and stored locally in a SQLite database.
429
+
430
+ ### Configuration
431
+
432
+ ```ruby
433
+ # config/initializers/brainzlab.rb
434
+ BrainzLab.configure do |config|
435
+ # Enable development mode (works offline)
436
+ config.mode = :development
437
+
438
+ # Optional: customize the SQLite database path (default: tmp/brainzlab.sqlite3)
439
+ config.development_db_path = 'tmp/brainzlab_dev.sqlite3'
440
+
441
+ # Other settings still apply
442
+ config.environment = Rails.env
443
+ config.service = 'my-app'
444
+ end
445
+ ```
446
+
447
+ Or use the environment variable:
448
+
449
+ ```bash
450
+ export BRAINZLAB_MODE=development
451
+ ```
452
+
453
+ ### Features
454
+
455
+ In development mode:
456
+
457
+ - **No server connection required** - Works completely offline
458
+ - **Stdout logging** - All events are pretty-printed to the console with colors
459
+ - **Local storage** - Events are stored in SQLite at `tmp/brainzlab.sqlite3`
460
+ - **Queryable** - Use `BrainzLab.development_events` to query stored events
461
+
462
+ ### Querying Events
463
+
464
+ ```ruby
465
+ # Get all events
466
+ events = BrainzLab.development_events
467
+
468
+ # Filter by service
469
+ logs = BrainzLab.development_events(service: :recall)
470
+ errors = BrainzLab.development_events(service: :reflex)
471
+ traces = BrainzLab.development_events(service: :pulse)
472
+
473
+ # Filter by event type
474
+ BrainzLab.development_events(event_type: 'log')
475
+ BrainzLab.development_events(event_type: 'error')
476
+ BrainzLab.development_events(event_type: 'trace')
477
+
478
+ # Filter by time
479
+ BrainzLab.development_events(since: 1.hour.ago)
480
+
481
+ # Limit results
482
+ BrainzLab.development_events(limit: 10)
483
+
484
+ # Combine filters
485
+ BrainzLab.development_events(
486
+ service: :recall,
487
+ since: 30.minutes.ago,
488
+ limit: 50
489
+ )
490
+
491
+ # Get stats by service
492
+ BrainzLab.development_stats
493
+ # => { recall: 42, reflex: 3, pulse: 15 }
494
+
495
+ # Clear all stored events
496
+ BrainzLab.clear_development_events!
497
+ ```
498
+
499
+ ### Console Output
500
+
501
+ In development mode, events are pretty-printed to stdout:
502
+
503
+ ```
504
+ [14:32:15.123] [RECALL] log [INFO] User signed up
505
+ user_id: 123
506
+ data: {email: "user@example.com"}
507
+
508
+ [14:32:16.456] [REFLEX] error RuntimeError: Something went wrong
509
+ error_class: "RuntimeError"
510
+ environment: "development"
511
+ request_id: "abc-123"
512
+
513
+ [14:32:17.789] [PULSE] trace GET /users (45.2ms)
514
+ request_method: "GET"
515
+ request_path: "/users"
516
+ status: 200
517
+ db_ms: 12.3
518
+ ```
519
+
520
+ ### Use Cases
521
+
522
+ Development mode is useful for:
523
+
524
+ - **Local development** without setting up a BrainzLab account
525
+ - **Testing** SDK integration in CI/CD pipelines
526
+ - **Debugging** to inspect exactly what events would be sent
527
+ - **Offline development** when working without internet access
528
+
322
529
  ## Self-Hosted
323
530
 
324
531
  For self-hosted BrainzLab installations:
@@ -200,7 +200,27 @@ module BrainzLab
200
200
  end
201
201
 
202
202
  def log_error(operation, error)
203
- BrainzLab.debug_log("[Beacon::Client] #{operation} failed: #{error.message}")
203
+ structured_error = ErrorHandler.wrap(error, service: 'Beacon', operation: operation)
204
+ BrainzLab.debug_log("[Beacon::Client] #{operation} failed: #{structured_error.message}")
205
+
206
+ # Call on_error callback if configured
207
+ if @config.on_error
208
+ @config.on_error.call(structured_error, { service: 'Beacon', operation: operation })
209
+ end
210
+ end
211
+
212
+ def handle_response_error(response, operation)
213
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
214
+
215
+ structured_error = ErrorHandler.from_response(response, service: 'Beacon', operation: operation)
216
+ BrainzLab.debug_log("[Beacon::Client] #{operation} failed: #{structured_error.message}")
217
+
218
+ # Call on_error callback if configured
219
+ if @config.on_error
220
+ @config.on_error.call(structured_error, { service: 'Beacon', operation: operation })
221
+ end
222
+
223
+ structured_error
204
224
  end
205
225
  end
206
226
  end
@@ -7,6 +7,9 @@ module BrainzLab
7
7
  # recall_min_level has a custom setter with validation
8
8
  attr_reader :recall_min_level
9
9
 
10
+ # mode has a custom setter with validation
11
+ attr_reader :mode
12
+
10
13
  attr_accessor :secret_key,
11
14
  :environment,
12
15
  :service,
@@ -106,6 +109,8 @@ module BrainzLab
106
109
  :synapse_auto_provision,
107
110
  :scrub_fields,
108
111
  :logger,
112
+ :on_error,
113
+ :on_send,
109
114
  :instrument_http,
110
115
  :instrument_active_record,
111
116
  :instrument_redis,
@@ -150,7 +155,9 @@ module BrainzLab
150
155
  :devtools_asset_path,
151
156
  :devtools_panel_position,
152
157
  :devtools_expand_by_default,
153
- :rails_instrumentation_handled_externally
158
+ :rails_instrumentation_handled_externally,
159
+ :development_db_path,
160
+ :development_log_output
154
161
 
155
162
  # Services that should not track themselves to avoid circular dependencies
156
163
  SELF_TRACKING_SERVICES = {
@@ -180,6 +187,13 @@ module BrainzLab
180
187
  # Debug mode - enables verbose logging
181
188
  @debug = ENV['BRAINZLAB_DEBUG'] == 'true'
182
189
 
190
+ # SDK mode - :production (default) or :development (offline, local storage)
191
+ @mode = ENV['BRAINZLAB_MODE']&.to_sym || :production
192
+
193
+ # Development mode settings
194
+ @development_db_path = ENV['BRAINZLAB_DEV_DB_PATH'] || 'tmp/brainzlab.sqlite3'
195
+ @development_log_output = $stdout
196
+
183
197
  # Disable self-tracking - prevents services from tracking to themselves
184
198
  # e.g., Recall won't log to itself, Reflex won't track errors to itself
185
199
  @disable_self_tracking = ENV.fetch('BRAINZLAB_DISABLE_SELF_TRACKING', 'true') == 'true'
@@ -306,6 +320,12 @@ module BrainzLab
306
320
  # Internal logger for debugging SDK issues
307
321
  @logger = nil
308
322
 
323
+ # Debug callbacks
324
+ # Called when an SDK error occurs (lambda/proc receiving error object and context hash)
325
+ @on_error = nil
326
+ # Called before each API request (lambda/proc receiving service, method, path, and payload)
327
+ @on_send = nil
328
+
309
329
  # Instrumentation
310
330
  @instrument_http = true # Enable HTTP client instrumentation (Net::HTTP, Faraday, HTTParty)
311
331
  @instrument_active_record = true # AR breadcrumbs for Reflex
@@ -363,11 +383,40 @@ module BrainzLab
363
383
 
364
384
  def recall_min_level=(level)
365
385
  level = level.to_sym
366
- raise ArgumentError, "Invalid level: #{level}" unless LEVELS.include?(level)
386
+ unless LEVELS.include?(level)
387
+ raise ValidationError.new(
388
+ "Invalid log level: #{level}",
389
+ hint: "Valid log levels are: #{LEVELS.join(', ')}",
390
+ code: 'invalid_log_level',
391
+ field: 'recall_min_level',
392
+ context: { provided: level, valid_values: LEVELS }
393
+ )
394
+ end
367
395
 
368
396
  @recall_min_level = level
369
397
  end
370
398
 
399
+ MODES = %i[production development].freeze
400
+
401
+ def mode=(mode)
402
+ mode = mode.to_sym
403
+ unless MODES.include?(mode)
404
+ raise ValidationError.new(
405
+ "Invalid mode: #{mode}",
406
+ hint: "Valid modes are: #{MODES.join(', ')}. Use :development for offline mode with local storage.",
407
+ code: 'invalid_mode',
408
+ field: 'mode',
409
+ context: { provided: mode, valid_values: MODES }
410
+ )
411
+ end
412
+
413
+ @mode = mode
414
+ end
415
+
416
+ def development_mode?
417
+ @mode == :development
418
+ end
419
+
371
420
  def level_enabled?(level)
372
421
  LEVELS.index(level.to_sym) >= LEVELS.index(@recall_min_level)
373
422
  end
@@ -132,7 +132,27 @@ module BrainzLab
132
132
  end
133
133
 
134
134
  def log_error(operation, error)
135
- BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{error.message}")
135
+ structured_error = ErrorHandler.wrap(error, service: 'Cortex', operation: operation)
136
+ BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
137
+
138
+ # Call on_error callback if configured
139
+ if @config.on_error
140
+ @config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
141
+ end
142
+ end
143
+
144
+ def handle_response_error(response, operation)
145
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
146
+
147
+ structured_error = ErrorHandler.from_response(response, service: 'Cortex', operation: operation)
148
+ BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
149
+
150
+ # Call on_error callback if configured
151
+ if @config.on_error
152
+ @config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
153
+ end
154
+
155
+ structured_error
136
156
  end
137
157
  end
138
158
  end
@@ -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