fluyenta-ruby 0.1.14

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module BrainzLab
8
+ module Recall
9
+ class Client
10
+ MAX_RETRIES = 3
11
+ RETRY_DELAY = 0.5
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ @uri = URI.parse(config.recall_url)
16
+ end
17
+
18
+ def send_log(log_entry)
19
+ return unless @config.recall_enabled && @config.valid?
20
+
21
+ post('/api/v1/log', log_entry)
22
+ end
23
+
24
+ def send_batch(log_entries)
25
+ return unless @config.recall_enabled && @config.valid?
26
+ return if log_entries.empty?
27
+
28
+ post('/api/v1/logs', { logs: log_entries })
29
+ end
30
+
31
+ private
32
+
33
+ def post(path, body)
34
+ uri = URI.join(@config.recall_url, path)
35
+
36
+ # Call on_send callback if configured
37
+ invoke_on_send(:recall, :post, path, body)
38
+
39
+ # Log debug output for request
40
+ log_debug_request(path, body)
41
+
42
+ request = Net::HTTP::Post.new(uri)
43
+ request['Content-Type'] = 'application/json'
44
+ request['Authorization'] = "Bearer #{@config.secret_key}"
45
+ request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
46
+ request.body = JSON.generate(body)
47
+
48
+ execute_with_retry(uri, request, path)
49
+ rescue StandardError => e
50
+ handle_error(e, context: { path: path, body_size: body.to_s.length })
51
+ nil
52
+ end
53
+
54
+ def execute_with_retry(uri, request, path)
55
+ retries = 0
56
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
57
+
58
+ begin
59
+ http = Net::HTTP.new(uri.host, uri.port)
60
+ http.use_ssl = uri.scheme == 'https'
61
+ http.open_timeout = 5
62
+ http.read_timeout = 10
63
+
64
+ response = http.request(request)
65
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
66
+
67
+ # Log debug output for response
68
+ log_debug_response(response.code.to_i, duration_ms)
69
+
70
+ case response.code.to_i
71
+ when 200..299
72
+ begin
73
+ JSON.parse(response.body)
74
+ rescue StandardError
75
+ {}
76
+ end
77
+ when 429, 500..599
78
+ raise RetryableError, "Server error: #{response.code}"
79
+ else
80
+ handle_error(
81
+ StandardError.new("Recall API error: #{response.code}"),
82
+ context: { path: path, status: response.code, body: response.body }
83
+ )
84
+ nil
85
+ end
86
+ rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
87
+ retries += 1
88
+ if retries <= MAX_RETRIES
89
+ sleep(RETRY_DELAY * retries)
90
+ retry
91
+ end
92
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
93
+ log_debug_response(0, duration_ms, error: e.message)
94
+ handle_error(e, context: { path: path, retries: retries })
95
+ nil
96
+ end
97
+ end
98
+
99
+ def log_debug_request(path, body)
100
+ return unless BrainzLab::Debug.enabled?
101
+
102
+ data = if body.is_a?(Hash) && body[:logs]
103
+ { count: body[:logs].size }
104
+ elsif body.is_a?(Hash) && body[:message]
105
+ { message: body[:message] }
106
+ else
107
+ {}
108
+ end
109
+
110
+ BrainzLab::Debug.log_request(:recall, 'POST', path, data: data)
111
+ end
112
+
113
+ def log_debug_response(status, duration_ms, error: nil)
114
+ return unless BrainzLab::Debug.enabled?
115
+
116
+ BrainzLab::Debug.log_response(:recall, status, duration_ms, error: error)
117
+ end
118
+
119
+ def invoke_on_send(service, method, path, payload)
120
+ return unless @config.on_send
121
+
122
+ @config.on_send.call(service, method, path, payload)
123
+ rescue StandardError => e
124
+ # Don't let callback errors break the SDK
125
+ log_error("on_send callback error: #{e.message}")
126
+ end
127
+
128
+ def handle_error(error, context: {})
129
+ # Wrap the error in a structured error if it's not already one
130
+ structured_error = if error.is_a?(BrainzLab::Error)
131
+ error
132
+ else
133
+ ErrorHandler.wrap(error, service: 'Recall', operation: context[:path] || 'unknown')
134
+ end
135
+
136
+ log_error(structured_error.message)
137
+
138
+ # Call on_error callback if configured
139
+ return unless @config.on_error
140
+
141
+ @config.on_error.call(structured_error, context.merge(service: :recall))
142
+ rescue StandardError => e
143
+ # Don't let callback errors break the SDK
144
+ log_error("on_error callback error: #{e.message}")
145
+ end
146
+
147
+ def log_error(message)
148
+ BrainzLab::Debug.log(message, level: :error) if BrainzLab::Debug.enabled?
149
+
150
+ return unless @config.logger
151
+
152
+ @config.logger.error("[BrainzLab::Recall] #{message}")
153
+ end
154
+
155
+ class RetryableError < StandardError; end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module BrainzLab
6
+ module Recall
7
+ class Logger < ::Logger
8
+ attr_accessor :broadcast_to
9
+
10
+ def initialize(service_name = nil, broadcast_to: nil)
11
+ super(nil)
12
+ @service_name = service_name
13
+ @broadcast_to = broadcast_to
14
+ @level = ::Logger::DEBUG
15
+ @formatter = proc { |_severity, _time, _progname, msg| msg }
16
+ end
17
+
18
+ def add(severity, message = nil, progname = nil)
19
+ severity ||= ::Logger::UNKNOWN
20
+
21
+ # Handle block-based messages
22
+ message = yield if message.nil? && block_given?
23
+
24
+ # Handle progname as message (standard Logger behavior)
25
+ if message.nil?
26
+ message = progname
27
+ progname = nil
28
+ end
29
+
30
+ # Broadcast to original logger if configured
31
+ @broadcast_to&.add(severity, message, progname)
32
+
33
+ # Skip if below configured level
34
+ return true if severity < @level
35
+
36
+ level = severity_to_level(severity)
37
+ return true unless BrainzLab.configuration.level_enabled?(level)
38
+
39
+ # Extract structured data if message is a hash
40
+ data = {}
41
+ if message.is_a?(Hash)
42
+ data = message.dup
43
+ message = data.delete(:message) || data.delete(:msg) || data.to_s
44
+ end
45
+
46
+ data[:service] = @service_name if @service_name
47
+ data[:progname] = progname if progname
48
+
49
+ Recall.log(level, message.to_s, **data)
50
+ true
51
+ end
52
+
53
+ def debug(message = nil, &)
54
+ add(::Logger::DEBUG, message, &)
55
+ end
56
+
57
+ def info(message = nil, &)
58
+ add(::Logger::INFO, message, &)
59
+ end
60
+
61
+ def warn(message = nil, &)
62
+ add(::Logger::WARN, message, &)
63
+ end
64
+
65
+ def error(message = nil, &)
66
+ add(::Logger::ERROR, message, &)
67
+ end
68
+
69
+ def fatal(message = nil, &)
70
+ add(::Logger::FATAL, message, &)
71
+ end
72
+
73
+ def unknown(message = nil, &)
74
+ add(::Logger::UNKNOWN, message, &)
75
+ end
76
+
77
+ # Rails compatibility methods
78
+ def silence(severity = ::Logger::ERROR)
79
+ old_level = @level
80
+ @level = severity
81
+ yield self
82
+ ensure
83
+ @level = old_level
84
+ end
85
+
86
+ def tagged(*tags)
87
+ if block_given?
88
+ BrainzLab.with_context(tags: tags) { yield self }
89
+ else
90
+ self
91
+ end
92
+ end
93
+
94
+ def flush
95
+ Recall.flush
96
+ end
97
+
98
+ def close
99
+ flush
100
+ end
101
+
102
+ private
103
+
104
+ def severity_to_level(severity)
105
+ case severity
106
+ when ::Logger::DEBUG then :debug
107
+ when ::Logger::INFO then :info
108
+ when ::Logger::WARN then :warn
109
+ when ::Logger::ERROR then :error
110
+ when ::Logger::FATAL, ::Logger::UNKNOWN then :fatal
111
+ else :info
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'fileutils'
7
+
8
+ module BrainzLab
9
+ module Recall
10
+ class Provisioner
11
+ CACHE_DIR = ENV.fetch('BRAINZLAB_CACHE_DIR') { File.join(Dir.home, '.brainzlab') }
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ end
16
+
17
+ def ensure_project!
18
+ return unless should_provision?
19
+
20
+ # Try cached credentials first
21
+ if (cached = load_cached_credentials)
22
+ apply_credentials(cached)
23
+ return cached
24
+ end
25
+
26
+ # Provision new project
27
+ project = provision_project
28
+ return unless project
29
+
30
+ # Cache and apply credentials
31
+ cache_credentials(project)
32
+ apply_credentials(project)
33
+
34
+ project
35
+ end
36
+
37
+ private
38
+
39
+ def should_provision?
40
+ if @config.debug
41
+ log_debug('Checking provision conditions:')
42
+ log_debug(" recall_auto_provision: #{@config.recall_auto_provision}")
43
+ log_debug(" app_name: '#{@config.app_name}'")
44
+ log_debug(" secret_key set: #{@config.secret_key.to_s.strip.length.positive?}")
45
+ log_debug(" recall_master_key set: #{@config.recall_master_key.to_s.strip.length.positive?}")
46
+ end
47
+
48
+ return false unless @config.recall_auto_provision
49
+ return false unless @config.app_name.to_s.strip.length.positive?
50
+ return false if @config.secret_key.to_s.strip.length.positive?
51
+ return false unless @config.recall_master_key.to_s.strip.length.positive?
52
+
53
+ log_debug('Will provision Recall project') if @config.debug
54
+ true
55
+ end
56
+
57
+ def log_debug(message)
58
+ if @config.logger
59
+ @config.logger.info("[BrainzLab::Debug] #{message}")
60
+ else
61
+ puts "[BrainzLab::Debug] #{message}"
62
+ end
63
+ end
64
+
65
+ def provision_project
66
+ uri = URI.parse("#{@config.recall_url}/api/v1/projects/provision")
67
+ request = Net::HTTP::Post.new(uri)
68
+ request['Content-Type'] = 'application/json'
69
+ request['X-Master-Key'] = @config.recall_master_key
70
+ request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
71
+ request.body = JSON.generate({ name: @config.app_name })
72
+
73
+ response = execute(uri, request)
74
+ return nil unless response.is_a?(Net::HTTPSuccess)
75
+
76
+ JSON.parse(response.body, symbolize_names: true)
77
+ rescue StandardError => e
78
+ log_error("Failed to provision Recall project: #{e.message}")
79
+ nil
80
+ end
81
+
82
+ def load_cached_credentials
83
+ path = cache_file_path
84
+ return nil unless File.exist?(path)
85
+
86
+ data = JSON.parse(File.read(path), symbolize_names: true)
87
+
88
+ # Validate cached data has required keys
89
+ return nil unless data[:ingest_key]
90
+
91
+ data
92
+ rescue StandardError => e
93
+ log_error("Failed to load cached credentials: #{e.message}")
94
+ nil
95
+ end
96
+
97
+ def cache_credentials(project)
98
+ FileUtils.mkdir_p(CACHE_DIR)
99
+ File.write(cache_file_path, JSON.generate(project))
100
+ rescue StandardError => e
101
+ log_error("Failed to cache credentials: #{e.message}")
102
+ end
103
+
104
+ def cache_file_path
105
+ File.join(CACHE_DIR, "#{@config.app_name}.recall.json")
106
+ end
107
+
108
+ def apply_credentials(project)
109
+ @config.secret_key = project[:ingest_key]
110
+
111
+ # Also set service name from app_name if not already set
112
+ @config.service ||= @config.app_name
113
+ end
114
+
115
+ def execute(uri, request)
116
+ http = Net::HTTP.new(uri.host, uri.port)
117
+ http.use_ssl = uri.scheme == 'https'
118
+ http.open_timeout = 5
119
+ http.read_timeout = 10
120
+ http.request(request)
121
+ end
122
+
123
+ def log_error(message)
124
+ return unless @config.logger
125
+
126
+ @config.logger.error("[BrainzLab] #{message}")
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'recall/client'
4
+ require_relative 'recall/buffer'
5
+ require_relative 'recall/logger'
6
+ require_relative 'recall/provisioner'
7
+
8
+ module BrainzLab
9
+ module Recall
10
+ class << self
11
+ def debug(message, **data)
12
+ log(:debug, message, **data)
13
+ end
14
+
15
+ def info(message, **data)
16
+ log(:info, message, **data)
17
+ end
18
+
19
+ def warn(message, **data)
20
+ log(:warn, message, **data)
21
+ end
22
+
23
+ def error(message, **data)
24
+ log(:error, message, **data)
25
+ end
26
+
27
+ def fatal(message, **data)
28
+ log(:fatal, message, **data)
29
+ end
30
+
31
+ def log(level, message, **data)
32
+ config = BrainzLab.configuration
33
+ return unless config.recall_effectively_enabled?
34
+ return unless config.level_enabled?(level)
35
+
36
+ entry = build_entry(level, message, data)
37
+
38
+ # Log debug output for the operation
39
+ log_debug_operation(level, message, data)
40
+
41
+ # In development mode, log locally instead of sending to server
42
+ if config.development_mode?
43
+ Development.record(service: :recall, event_type: 'log', payload: entry)
44
+ return
45
+ end
46
+
47
+ # Auto-provision project on first log if app_name is configured
48
+ ensure_provisioned!
49
+
50
+ return unless config.valid?
51
+
52
+ buffer.push(entry)
53
+ end
54
+
55
+ def ensure_provisioned!
56
+ config = BrainzLab.configuration
57
+ puts "[BrainzLab::Debug] Recall.ensure_provisioned! called, @provisioned=#{@provisioned}" if config.debug
58
+
59
+ return if @provisioned
60
+
61
+ @provisioned = true
62
+ provisioner.ensure_project!
63
+ end
64
+
65
+ def provisioner
66
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
67
+ end
68
+
69
+ def time(label, **data)
70
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
71
+ result = yield
72
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
73
+
74
+ info("#{label} (#{duration_ms}ms)", **data, duration_ms: duration_ms)
75
+ result
76
+ end
77
+
78
+ def flush
79
+ buffer.flush
80
+ end
81
+
82
+ def logger(name = nil)
83
+ Logger.new(name)
84
+ end
85
+
86
+ def client
87
+ @client ||= Client.new(BrainzLab.configuration)
88
+ end
89
+
90
+ def buffer
91
+ @buffer ||= Buffer.new(BrainzLab.configuration, client)
92
+ end
93
+
94
+ def reset!
95
+ @client = nil
96
+ @buffer = nil
97
+ @provisioner = nil
98
+ @provisioned = false
99
+ end
100
+
101
+ private
102
+
103
+ def build_entry(level, message, data)
104
+ config = BrainzLab.configuration
105
+ context = Context.current
106
+
107
+ entry = {
108
+ timestamp: Time.now.utc.iso8601(3),
109
+ level: level.to_s,
110
+ message: message.to_s
111
+ }
112
+
113
+ # Add configuration context
114
+ entry[:environment] = config.environment if config.environment
115
+ entry[:service] = config.service if config.service
116
+ entry[:host] = config.host if config.host
117
+ entry[:commit] = config.commit if config.commit
118
+ entry[:branch] = config.branch if config.branch
119
+
120
+ # Add request context
121
+ entry[:request_id] = context.request_id if context.request_id
122
+ entry[:session_id] = context.session_id if context.session_id
123
+
124
+ # Merge context data with provided data
125
+ merged_data = context.data_hash.merge(scrub_data(data))
126
+ entry[:data] = merged_data unless merged_data.empty?
127
+
128
+ entry
129
+ end
130
+
131
+ def scrub_data(data)
132
+ return data if BrainzLab.configuration.scrub_fields.empty?
133
+
134
+ scrub_fields = BrainzLab.configuration.scrub_fields
135
+ deep_scrub(data, scrub_fields)
136
+ end
137
+
138
+ def deep_scrub(obj, fields)
139
+ case obj
140
+ when Hash
141
+ obj.each_with_object({}) do |(key, value), result|
142
+ result[key] = if should_scrub?(key, fields)
143
+ '[FILTERED]'
144
+ else
145
+ deep_scrub(value, fields)
146
+ end
147
+ end
148
+ when Array
149
+ obj.map { |item| deep_scrub(item, fields) }
150
+ else
151
+ obj
152
+ end
153
+ end
154
+
155
+ def should_scrub?(key, fields)
156
+ key_str = key.to_s.downcase
157
+ fields.any? do |field|
158
+ case field
159
+ when Regexp
160
+ key_str.match?(field)
161
+ else
162
+ key_str == field.to_s.downcase
163
+ end
164
+ end
165
+ end
166
+
167
+ def log_debug_operation(level, message, data)
168
+ return unless BrainzLab::Debug.enabled?
169
+
170
+ truncated_message = message.to_s.length > 50 ? "#{message.to_s[0..47]}..." : message.to_s
171
+ BrainzLab::Debug.log_operation(:recall, "#{level.to_s.upcase} \"#{truncated_message}\"", **data.slice(*data.keys.first(3)))
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Reflex
5
+ class Breadcrumbs
6
+ MAX_BREADCRUMBS = 50
7
+
8
+ def initialize
9
+ @breadcrumbs = []
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def add(message:, category: 'default', level: :info, data: nil)
14
+ crumb = {
15
+ timestamp: Time.now.utc.iso8601(3),
16
+ message: message.to_s,
17
+ category: category.to_s,
18
+ level: level.to_s
19
+ }
20
+ crumb[:data] = data if data
21
+
22
+ @mutex.synchronize do
23
+ @breadcrumbs << crumb
24
+ @breadcrumbs.shift if @breadcrumbs.size > MAX_BREADCRUMBS
25
+ end
26
+ end
27
+
28
+ def to_a
29
+ @mutex.synchronize { @breadcrumbs.dup }
30
+ end
31
+
32
+ def clear!
33
+ @mutex.synchronize { @breadcrumbs.clear }
34
+ end
35
+
36
+ def size
37
+ @mutex.synchronize { @breadcrumbs.size }
38
+ end
39
+ end
40
+
41
+ class << self
42
+ def breadcrumbs
43
+ Context.current.breadcrumbs
44
+ end
45
+
46
+ def add_breadcrumb(message, category: 'default', level: :info, data: nil)
47
+ breadcrumbs.add(message: message, category: category, level: level, data: data)
48
+ end
49
+
50
+ def clear_breadcrumbs!
51
+ breadcrumbs.clear!
52
+ end
53
+ end
54
+ end
55
+ end