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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz: '
|
|
3
|
+
metadata.gz: 85a5cf3a64e256a569be39f7caa1ec6268755616ac6037db51f1543ee74dd50c
|
|
4
|
+
data.tar.gz: '09c531bb0793d26db1f62faf41f9ead294130a7036f6e5f6ab270fd8c6c46040'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|