brainzlab 0.1.11 → 0.1.20

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +210 -3
  4. data/lib/brainzlab/beacon/client.rb +21 -1
  5. data/lib/brainzlab/configuration.rb +81 -4
  6. data/lib/brainzlab/cortex/client.rb +21 -1
  7. data/lib/brainzlab/debug.rb +305 -0
  8. data/lib/brainzlab/dendrite/client.rb +21 -1
  9. data/lib/brainzlab/development/logger.rb +150 -0
  10. data/lib/brainzlab/development/store.rb +121 -0
  11. data/lib/brainzlab/development.rb +72 -0
  12. data/lib/brainzlab/devtools/assets/devtools.css +245 -109
  13. data/lib/brainzlab/devtools/assets/devtools.js +40 -0
  14. data/lib/brainzlab/devtools/middleware/asset_server.rb +1 -0
  15. data/lib/brainzlab/devtools/middleware/debug_panel.rb +1 -0
  16. data/lib/brainzlab/devtools/middleware/error_page.rb +56 -8
  17. data/lib/brainzlab/errors.rb +490 -0
  18. data/lib/brainzlab/flux/buffer.rb +2 -2
  19. data/lib/brainzlab/flux/client.rb +2 -2
  20. data/lib/brainzlab/instrumentation/active_support_cache.rb +60 -30
  21. data/lib/brainzlab/instrumentation/net_http.rb +21 -16
  22. data/lib/brainzlab/instrumentation.rb +6 -0
  23. data/lib/brainzlab/nerve/client.rb +21 -1
  24. data/lib/brainzlab/pulse/client.rb +66 -5
  25. data/lib/brainzlab/pulse.rb +24 -5
  26. data/lib/brainzlab/rails/log_formatter.rb +1 -1
  27. data/lib/brainzlab/rails/railtie.rb +18 -3
  28. data/lib/brainzlab/recall/buffer.rb +3 -1
  29. data/lib/brainzlab/recall/client.rb +74 -6
  30. data/lib/brainzlab/recall.rb +19 -2
  31. data/lib/brainzlab/reflex/client.rb +66 -5
  32. data/lib/brainzlab/reflex.rb +40 -8
  33. data/lib/brainzlab/sentinel/client.rb +21 -1
  34. data/lib/brainzlab/synapse/client.rb +21 -1
  35. data/lib/brainzlab/testing/event_store.rb +377 -0
  36. data/lib/brainzlab/testing/helpers.rb +650 -0
  37. data/lib/brainzlab/testing/matchers.rb +391 -0
  38. data/lib/brainzlab/testing.rb +327 -0
  39. data/lib/brainzlab/utilities/circuit_breaker.rb +32 -3
  40. data/lib/brainzlab/vault/client.rb +21 -1
  41. data/lib/brainzlab/version.rb +1 -1
  42. data/lib/brainzlab/vision/client.rb +53 -6
  43. data/lib/brainzlab.rb +67 -0
  44. data/lib/fluyenta-ruby.rb +3 -0
  45. metadata +34 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49af0bcd7ebaf8cc4b66040ae38be6eb6e8826178e493283b9241083ef54b79f
4
- data.tar.gz: '0057911ea7e6c7113e06a148863dc7e806e7af32f0bd9eb6f579bb7b69377849'
3
+ metadata.gz: ac1922e302a243a61b38d955da04782ae4e66c11908bea728a39b6ad3c68634e
4
+ data.tar.gz: 748d7f686bf204d0e0ef9322fb445dd75b62c13404472758631e976cc135c295
5
5
  SHA512:
6
- metadata.gz: 91b591d98847edc9f401e6f505df003a1a802319db212615dab3a33a09fd320cde743c9c0dac40717dade391434a127ffa0975b28ec903f07a3907ab3f78f8fd
7
- data.tar.gz: a9f1105138dc3a9871854e9a6526381950b66c0f2d4e18dcc40b2f434cb55d7f7af3bd2a27933401fa4ebb1b658d68fcf420b3476ebbea7a15944d543c16ab76
6
+ metadata.gz: ee4bdba72d0fed3db7489bf0335de8dd4b8139a941eb381b4a35a3425ca0fa7755f3702869ac93405a5cd0e9def39174ced81eb559c3de99b6970e353c99f977
7
+ data.tar.gz: 8aec3a840b93948c73ad1e1e518182b316b4a8a29c20c8b5a6819a88c5f12e647d9c576dc865e04991ac0ea4976e2536d21751b1291ebbaf5594713a9317b2b7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.13] - 2026-02-24
6
+
7
+ ### Fixed
8
+
9
+ - **Rails LogFormatter** - Fix `TypeError: wrong element type Hash at 0 (expected array)` in `format_params_toml`
10
+ - `hash_like?` incorrectly matched Arrays (they respond to `to_h` and `each`), causing `Array#to_h` to fail when params contained arrays of hashes
11
+
5
12
  ## [0.1.1] - 2025-12-23
6
13
 
7
14
  ### Fixed
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,7 +7,11 @@ module BrainzLab
7
7
  # recall_min_level has a custom setter with validation
8
8
  attr_reader :recall_min_level
9
9
 
10
- attr_accessor :secret_key,
10
+ # mode has a custom setter with validation
11
+ attr_reader :mode
12
+
13
+ attr_accessor :enabled,
14
+ :secret_key,
11
15
  :environment,
12
16
  :service,
13
17
  :host,
@@ -106,6 +110,8 @@ module BrainzLab
106
110
  :synapse_auto_provision,
107
111
  :scrub_fields,
108
112
  :logger,
113
+ :on_error,
114
+ :on_send,
109
115
  :instrument_http,
110
116
  :instrument_active_record,
111
117
  :instrument_redis,
@@ -150,7 +156,9 @@ module BrainzLab
150
156
  :devtools_asset_path,
151
157
  :devtools_panel_position,
152
158
  :devtools_expand_by_default,
153
- :rails_instrumentation_handled_externally
159
+ :rails_instrumentation_handled_externally,
160
+ :development_db_path,
161
+ :development_log_output
154
162
 
155
163
  # Services that should not track themselves to avoid circular dependencies
156
164
  SELF_TRACKING_SERVICES = {
@@ -162,6 +170,9 @@ module BrainzLab
162
170
  }.freeze
163
171
 
164
172
  def initialize
173
+ # SDK enabled flag — set BRAINZLAB_SDK_ENABLED=false to completely disable
174
+ @enabled = ENV.fetch('BRAINZLAB_SDK_ENABLED', 'true') != 'false'
175
+
165
176
  # Authentication
166
177
  @secret_key = ENV.fetch('BRAINZLAB_SECRET_KEY', nil)
167
178
 
@@ -180,6 +191,13 @@ module BrainzLab
180
191
  # Debug mode - enables verbose logging
181
192
  @debug = ENV['BRAINZLAB_DEBUG'] == 'true'
182
193
 
194
+ # SDK mode - :production (default) or :development (offline, local storage)
195
+ @mode = ENV['BRAINZLAB_MODE']&.to_sym || :production
196
+
197
+ # Development mode settings
198
+ @development_db_path = ENV['BRAINZLAB_DEV_DB_PATH'] || 'tmp/brainzlab.sqlite3'
199
+ @development_log_output = $stdout
200
+
183
201
  # Disable self-tracking - prevents services from tracking to themselves
184
202
  # e.g., Recall won't log to itself, Reflex won't track errors to itself
185
203
  @disable_self_tracking = ENV.fetch('BRAINZLAB_DISABLE_SELF_TRACKING', 'true') == 'true'
@@ -306,6 +324,12 @@ module BrainzLab
306
324
  # Internal logger for debugging SDK issues
307
325
  @logger = nil
308
326
 
327
+ # Debug callbacks
328
+ # Called when an SDK error occurs (lambda/proc receiving error object and context hash)
329
+ @on_error = nil
330
+ # Called before each API request (lambda/proc receiving service, method, path, and payload)
331
+ @on_send = nil
332
+
309
333
  # Instrumentation
310
334
  @instrument_http = true # Enable HTTP client instrumentation (Net::HTTP, Faraday, HTTParty)
311
335
  @instrument_active_record = true # AR breadcrumbs for Reflex
@@ -363,17 +387,50 @@ module BrainzLab
363
387
 
364
388
  def recall_min_level=(level)
365
389
  level = level.to_sym
366
- raise ArgumentError, "Invalid level: #{level}" unless LEVELS.include?(level)
390
+ unless LEVELS.include?(level)
391
+ raise ValidationError.new(
392
+ "Invalid log level: #{level}",
393
+ hint: "Valid log levels are: #{LEVELS.join(', ')}",
394
+ code: 'invalid_log_level',
395
+ field: 'recall_min_level',
396
+ context: { provided: level, valid_values: LEVELS }
397
+ )
398
+ end
367
399
 
368
400
  @recall_min_level = level
369
401
  end
370
402
 
403
+ MODES = %i[production development].freeze
404
+
405
+ def mode=(mode)
406
+ mode = mode.to_sym
407
+ unless MODES.include?(mode)
408
+ raise ValidationError.new(
409
+ "Invalid mode: #{mode}",
410
+ hint: "Valid modes are: #{MODES.join(', ')}. Use :development for offline mode with local storage.",
411
+ code: 'invalid_mode',
412
+ field: 'mode',
413
+ context: { provided: mode, valid_values: MODES }
414
+ )
415
+ end
416
+
417
+ @mode = mode
418
+ end
419
+
420
+ def development_mode?
421
+ @mode == :development
422
+ end
423
+
371
424
  def level_enabled?(level)
372
425
  LEVELS.index(level.to_sym) >= LEVELS.index(@recall_min_level)
373
426
  end
374
427
 
375
428
  def valid?
376
- !@secret_key.nil? && !@secret_key.empty?
429
+ @enabled && !@secret_key.nil? && !@secret_key.empty?
430
+ end
431
+
432
+ def enabled?
433
+ @enabled == true
377
434
  end
378
435
 
379
436
  def reflex_valid?
@@ -488,8 +545,24 @@ module BrainzLab
488
545
  @debug == true
489
546
  end
490
547
 
548
+ # Returns hostnames of all configured SDK service URLs
549
+ # Used by Net::HTTP instrumentation to skip tracking SDK's own HTTP calls
550
+ def sdk_service_hosts
551
+ @sdk_service_hosts ||= begin
552
+ urls = [
553
+ @recall_url, @reflex_url, @pulse_url, @flux_url,
554
+ @signal_url, @vault_url, @vision_url, @cortex_url,
555
+ @beacon_url, @nerve_url, @dendrite_url, @sentinel_url,
556
+ @synapse_url
557
+ ].compact
558
+
559
+ urls.filter_map { |url| URI.parse(url).host rescue nil }.uniq
560
+ end
561
+ end
562
+
491
563
  # Check if recall is effectively enabled (considering self-tracking)
492
564
  def recall_effectively_enabled?
565
+ return false unless @enabled
493
566
  return false unless @recall_enabled
494
567
  return true unless @disable_self_tracking
495
568
 
@@ -500,6 +573,7 @@ module BrainzLab
500
573
 
501
574
  # Check if reflex is effectively enabled (considering self-tracking)
502
575
  def reflex_effectively_enabled?
576
+ return false unless @enabled
503
577
  return false unless @reflex_enabled
504
578
  return true unless @disable_self_tracking
505
579
 
@@ -510,6 +584,7 @@ module BrainzLab
510
584
 
511
585
  # Check if pulse is effectively enabled (considering self-tracking)
512
586
  def pulse_effectively_enabled?
587
+ return false unless @enabled
513
588
  return false unless @pulse_enabled
514
589
  return true unless @disable_self_tracking
515
590
 
@@ -520,6 +595,7 @@ module BrainzLab
520
595
 
521
596
  # Check if flux is effectively enabled (considering self-tracking)
522
597
  def flux_effectively_enabled?
598
+ return false unless @enabled
523
599
  return false unless @flux_enabled
524
600
  return true unless @disable_self_tracking
525
601
 
@@ -530,6 +606,7 @@ module BrainzLab
530
606
 
531
607
  # Check if signal is effectively enabled (considering self-tracking)
532
608
  def signal_effectively_enabled?
609
+ return false unless @enabled
533
610
  return false unless @signal_enabled
534
611
  return true unless @disable_self_tracking
535
612
 
@@ -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