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,490 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ # Base error class for all BrainzLab SDK errors.
5
+ # Provides structured error information including hints and documentation links.
6
+ #
7
+ # @example Raising a structured error
8
+ # raise BrainzLab::Error.new(
9
+ # "Operation failed",
10
+ # hint: "Check your network connection",
11
+ # docs_url: "https://docs.brainzlab.io/troubleshooting",
12
+ # code: "operation_failed"
13
+ # )
14
+ #
15
+ # @example Catching and inspecting errors
16
+ # begin
17
+ # BrainzLab::Vault.get("secret")
18
+ # rescue BrainzLab::Error => e
19
+ # puts e.message # What went wrong
20
+ # puts e.hint # How to fix it
21
+ # puts e.docs_url # Where to learn more
22
+ # puts e.code # Machine-readable code
23
+ # end
24
+ #
25
+ class Error < StandardError
26
+ # @return [String, nil] A helpful hint on how to resolve the error
27
+ attr_reader :hint
28
+
29
+ # @return [String, nil] URL to relevant documentation
30
+ attr_reader :docs_url
31
+
32
+ # @return [String, nil] Machine-readable error code for programmatic handling
33
+ attr_reader :code
34
+
35
+ # @return [Hash, nil] Additional context about the error
36
+ attr_reader :context
37
+
38
+ DOCS_BASE_URL = 'https://docs.brainzlab.io'
39
+
40
+ # Initialize a new BrainzLab error.
41
+ #
42
+ # @param message [String] The error message describing what went wrong
43
+ # @param hint [String, nil] A helpful hint on how to resolve the error
44
+ # @param docs_url [String, nil] URL to relevant documentation
45
+ # @param code [String, nil] Machine-readable error code
46
+ # @param context [Hash, nil] Additional context about the error
47
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
48
+ @message = message
49
+ @hint = hint
50
+ @docs_url = docs_url
51
+ @code = code
52
+ @context = context
53
+ super(message)
54
+ end
55
+
56
+ # Format the error as a detailed string with hints and documentation links.
57
+ #
58
+ # @return [String] Formatted error message
59
+ def to_s
60
+ super
61
+ end
62
+
63
+ # Return a detailed formatted version of the error with hints and documentation links.
64
+ # Use this method when you want the full structured output.
65
+ #
66
+ # @return [String] Detailed formatted error message
67
+ def detailed_message(highlight: false, **_kwargs)
68
+ # Get the base message without class name duplication
69
+ base_msg = @message || super
70
+
71
+ parts = ["#{self.class.name}: #{base_msg}"]
72
+
73
+ parts << "" << "Hint: #{hint}" if hint
74
+ parts << "Docs: #{docs_url}" if docs_url
75
+ parts << "Code: #{code}" if code
76
+
77
+ if context && !context.empty?
78
+ parts << "" << "Context:"
79
+ context.each do |key, value|
80
+ parts << " #{key}: #{value}"
81
+ end
82
+ end
83
+
84
+ parts.join("\n")
85
+ end
86
+
87
+ # Inspect the error for debugging
88
+ #
89
+ # @return [String] Inspection output
90
+ def inspect
91
+ "#<#{self.class.name}: #{message}#{" (#{code})" if code}>"
92
+ end
93
+
94
+ # Return a hash representation of the error for logging/serialization.
95
+ #
96
+ # @return [Hash] Error details as a hash
97
+ def to_h
98
+ {
99
+ error_class: self.class.name,
100
+ message: message,
101
+ hint: hint,
102
+ docs_url: docs_url,
103
+ code: code,
104
+ context: context
105
+ }.compact
106
+ end
107
+
108
+ # Alias for to_h
109
+ def as_json
110
+ to_h
111
+ end
112
+ end
113
+
114
+ # Raised when the SDK is misconfigured or required configuration is missing.
115
+ #
116
+ # @example Missing API key
117
+ # raise BrainzLab::ConfigurationError.new(
118
+ # "API key is required",
119
+ # hint: "Set BRAINZLAB_SECRET_KEY environment variable or configure via BrainzLab.configure",
120
+ # code: "missing_api_key"
121
+ # )
122
+ #
123
+ class ConfigurationError < Error
124
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
125
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration"
126
+ code ||= 'configuration_error'
127
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context)
128
+ end
129
+ end
130
+
131
+ # Raised when authentication fails due to invalid or expired credentials.
132
+ #
133
+ # @example Invalid API key
134
+ # raise BrainzLab::AuthenticationError.new(
135
+ # "Invalid API key",
136
+ # hint: "Check that your API key is correct and has not expired",
137
+ # code: "invalid_api_key"
138
+ # )
139
+ #
140
+ class AuthenticationError < Error
141
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
142
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/authentication"
143
+ code ||= 'authentication_error'
144
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context)
145
+ end
146
+ end
147
+
148
+ # Raised when a connection to BrainzLab services cannot be established.
149
+ #
150
+ # @example Connection timeout
151
+ # raise BrainzLab::ConnectionError.new(
152
+ # "Connection timed out",
153
+ # hint: "Check your network connection and firewall settings",
154
+ # code: "connection_timeout"
155
+ # )
156
+ #
157
+ class ConnectionError < Error
158
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
159
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#connection-issues"
160
+ code ||= 'connection_error'
161
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context)
162
+ end
163
+ end
164
+
165
+ # Raised when the rate limit for API requests has been exceeded.
166
+ #
167
+ # @example Rate limit exceeded
168
+ # raise BrainzLab::RateLimitError.new(
169
+ # "Rate limit exceeded",
170
+ # hint: "Wait before retrying or consider upgrading your plan",
171
+ # code: "rate_limit_exceeded",
172
+ # context: { retry_after: 60, limit: 1000, remaining: 0 }
173
+ # )
174
+ #
175
+ class RateLimitError < Error
176
+ # @return [Integer, nil] Seconds to wait before retrying
177
+ attr_reader :retry_after
178
+
179
+ # @return [Integer, nil] The rate limit ceiling
180
+ attr_reader :limit
181
+
182
+ # @return [Integer, nil] Remaining requests in the current window
183
+ attr_reader :remaining
184
+
185
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, retry_after: nil, limit: nil, remaining: nil)
186
+ @retry_after = retry_after
187
+ @limit = limit
188
+ @remaining = remaining
189
+
190
+ hint ||= retry_after ? "Wait #{retry_after} seconds before retrying" : 'Reduce request frequency or upgrade your plan'
191
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/rate-limits"
192
+ code ||= 'rate_limit_exceeded'
193
+
194
+ context ||= {}
195
+ context[:retry_after] = retry_after if retry_after
196
+ context[:limit] = limit if limit
197
+ context[:remaining] = remaining if remaining
198
+
199
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
200
+ end
201
+ end
202
+
203
+ # Raised when request parameters or data fail validation.
204
+ #
205
+ # @example Invalid parameter
206
+ # raise BrainzLab::ValidationError.new(
207
+ # "Invalid email format",
208
+ # hint: "Provide a valid email address (e.g., user@example.com)",
209
+ # code: "invalid_email",
210
+ # context: { field: "email", value: "invalid" }
211
+ # )
212
+ #
213
+ class ValidationError < Error
214
+ # @return [String, nil] The field that failed validation
215
+ attr_reader :field
216
+
217
+ # @return [Array<Hash>, nil] List of validation errors for multiple fields
218
+ attr_reader :errors
219
+
220
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, field: nil, errors: nil)
221
+ @field = field
222
+ @errors = errors
223
+
224
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/api-reference"
225
+ code ||= 'validation_error'
226
+
227
+ context ||= {}
228
+ context[:field] = field if field
229
+ context[:errors] = errors if errors
230
+
231
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
232
+ end
233
+ end
234
+
235
+ # Raised when a requested resource is not found.
236
+ #
237
+ # @example Resource not found
238
+ # raise BrainzLab::NotFoundError.new(
239
+ # "Secret 'database_url' not found",
240
+ # hint: "Verify the secret name and environment",
241
+ # code: "secret_not_found"
242
+ # )
243
+ #
244
+ class NotFoundError < Error
245
+ # @return [String, nil] The type of resource that was not found
246
+ attr_reader :resource_type
247
+
248
+ # @return [String, nil] The identifier of the resource that was not found
249
+ attr_reader :resource_id
250
+
251
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, resource_type: nil, resource_id: nil)
252
+ @resource_type = resource_type
253
+ @resource_id = resource_id
254
+
255
+ code ||= 'not_found'
256
+
257
+ context ||= {}
258
+ context[:resource_type] = resource_type if resource_type
259
+ context[:resource_id] = resource_id if resource_id
260
+
261
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
262
+ end
263
+ end
264
+
265
+ # Raised when a server-side error occurs.
266
+ #
267
+ # @example Server error
268
+ # raise BrainzLab::ServerError.new(
269
+ # "Internal server error",
270
+ # hint: "This is a temporary issue. Please retry your request.",
271
+ # code: "internal_server_error"
272
+ # )
273
+ #
274
+ class ServerError < Error
275
+ # @return [Integer, nil] HTTP status code from the server
276
+ attr_reader :status_code
277
+
278
+ # @return [String, nil] Request ID for support reference
279
+ attr_reader :request_id
280
+
281
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, status_code: nil, request_id: nil)
282
+ @status_code = status_code
283
+ @request_id = request_id
284
+
285
+ hint ||= 'This is a temporary issue. Please retry your request. If the problem persists, contact support.'
286
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#server-errors"
287
+ code ||= 'server_error'
288
+
289
+ context ||= {}
290
+ context[:status_code] = status_code if status_code
291
+ context[:request_id] = request_id if request_id
292
+
293
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
294
+ end
295
+ end
296
+
297
+ # Raised when an operation times out.
298
+ #
299
+ # @example Request timeout
300
+ # raise BrainzLab::TimeoutError.new(
301
+ # "Request timed out after 30 seconds",
302
+ # hint: "The operation took too long. Try again or increase timeout settings.",
303
+ # code: "request_timeout"
304
+ # )
305
+ #
306
+ class TimeoutError < Error
307
+ # @return [Integer, nil] Timeout duration in seconds
308
+ attr_reader :timeout_seconds
309
+
310
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, timeout_seconds: nil)
311
+ @timeout_seconds = timeout_seconds
312
+
313
+ hint ||= 'The operation took too long. Try again or increase timeout settings.'
314
+ docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration#timeouts"
315
+ code ||= 'timeout'
316
+
317
+ context ||= {}
318
+ context[:timeout_seconds] = timeout_seconds if timeout_seconds
319
+
320
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
321
+ end
322
+ end
323
+
324
+ # Raised when a service is temporarily unavailable.
325
+ #
326
+ # @example Service unavailable
327
+ # raise BrainzLab::ServiceUnavailableError.new(
328
+ # "Vault service is currently unavailable",
329
+ # hint: "The service is undergoing maintenance. Please try again later.",
330
+ # code: "vault_unavailable"
331
+ # )
332
+ #
333
+ class ServiceUnavailableError < Error
334
+ # @return [String, nil] The name of the unavailable service
335
+ attr_reader :service_name
336
+
337
+ def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, service_name: nil)
338
+ @service_name = service_name
339
+
340
+ hint ||= 'The service is temporarily unavailable. Please try again later.'
341
+ docs_url ||= "#{DOCS_BASE_URL}/status"
342
+ code ||= 'service_unavailable'
343
+
344
+ context ||= {}
345
+ context[:service_name] = service_name if service_name
346
+
347
+ super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
348
+ end
349
+ end
350
+
351
+ # Helper module for wrapping low-level errors into structured BrainzLab errors
352
+ module ErrorHandler
353
+ module_function
354
+
355
+ # Wrap a standard error into a structured BrainzLab error.
356
+ #
357
+ # @param error [StandardError] The original error
358
+ # @param service [String] The service name (e.g., 'Vault', 'Cortex')
359
+ # @param operation [String] The operation being performed
360
+ # @return [BrainzLab::Error] A structured BrainzLab error
361
+ def wrap(error, service:, operation:)
362
+ case error
363
+ when Net::OpenTimeout, Net::ReadTimeout, Timeout::Error
364
+ TimeoutError.new(
365
+ "#{service} #{operation} timed out: #{error.message}",
366
+ hint: 'Check your network connection or increase timeout settings.',
367
+ code: "#{service.downcase}_timeout",
368
+ context: { service: service, operation: operation }
369
+ )
370
+ when Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ENETUNREACH
371
+ ConnectionError.new(
372
+ "Unable to connect to #{service}: #{error.message}",
373
+ hint: 'Check that the service is running and accessible.',
374
+ code: "#{service.downcase}_connection_failed",
375
+ context: { service: service, operation: operation }
376
+ )
377
+ when SocketError
378
+ ConnectionError.new(
379
+ "DNS resolution failed for #{service}: #{error.message}",
380
+ hint: 'Check your network connection and DNS settings.',
381
+ code: "#{service.downcase}_dns_error",
382
+ context: { service: service, operation: operation }
383
+ )
384
+ when JSON::ParserError
385
+ ServerError.new(
386
+ "Invalid response from #{service}: #{error.message}",
387
+ hint: 'The server returned an unexpected response format.',
388
+ code: "#{service.downcase}_invalid_response",
389
+ context: { service: service, operation: operation }
390
+ )
391
+ when OpenSSL::SSL::SSLError
392
+ ConnectionError.new(
393
+ "SSL error connecting to #{service}: #{error.message}",
394
+ hint: 'Check SSL certificates and ensure the connection is secure.',
395
+ code: "#{service.downcase}_ssl_error",
396
+ context: { service: service, operation: operation }
397
+ )
398
+ else
399
+ Error.new(
400
+ "#{service} #{operation} failed: #{error.message}",
401
+ hint: 'An unexpected error occurred. Check the logs for more details.',
402
+ code: "#{service.downcase}_error",
403
+ context: { service: service, operation: operation, original_error: error.class.name }
404
+ )
405
+ end
406
+ end
407
+
408
+ # Convert an HTTP response to a structured error.
409
+ #
410
+ # @param response [Net::HTTPResponse] The HTTP response
411
+ # @param service [String] The service name
412
+ # @param operation [String] The operation being performed
413
+ # @return [BrainzLab::Error] A structured BrainzLab error
414
+ def from_response(response, service:, operation:)
415
+ status_code = response.code.to_i
416
+ body = parse_response_body(response)
417
+ message = body[:message] || body[:error] || "HTTP #{status_code}"
418
+ request_id = response['X-Request-Id']
419
+
420
+ case status_code
421
+ when 400
422
+ ValidationError.new(
423
+ message,
424
+ hint: body[:hint] || 'Check the request parameters.',
425
+ code: body[:code] || 'bad_request',
426
+ context: { service: service, operation: operation, status_code: status_code }
427
+ )
428
+ when 401
429
+ AuthenticationError.new(
430
+ message,
431
+ hint: body[:hint] || "Verify your #{service} API key is correct and active.",
432
+ code: body[:code] || 'unauthorized',
433
+ context: { service: service, operation: operation }
434
+ )
435
+ when 403
436
+ AuthenticationError.new(
437
+ message,
438
+ hint: body[:hint] || 'Your API key does not have permission for this operation.',
439
+ code: body[:code] || 'forbidden',
440
+ context: { service: service, operation: operation }
441
+ )
442
+ when 404
443
+ NotFoundError.new(
444
+ message,
445
+ hint: body[:hint] || 'The requested resource does not exist.',
446
+ code: body[:code] || 'not_found',
447
+ context: { service: service, operation: operation }
448
+ )
449
+ when 422
450
+ ValidationError.new(
451
+ message,
452
+ hint: body[:hint] || 'The request was well-formed but contained invalid data.',
453
+ code: body[:code] || 'unprocessable_entity',
454
+ errors: body[:errors],
455
+ context: { service: service, operation: operation, status_code: status_code }
456
+ )
457
+ when 429
458
+ RateLimitError.new(
459
+ message,
460
+ retry_after: response['Retry-After']&.to_i,
461
+ limit: response['X-RateLimit-Limit']&.to_i,
462
+ remaining: response['X-RateLimit-Remaining']&.to_i,
463
+ context: { service: service, operation: operation }
464
+ )
465
+ when 500..599
466
+ ServerError.new(
467
+ message,
468
+ hint: body[:hint] || 'A server error occurred. Please retry your request.',
469
+ code: body[:code] || "server_error_#{status_code}",
470
+ status_code: status_code,
471
+ request_id: request_id,
472
+ context: { service: service, operation: operation }
473
+ )
474
+ else
475
+ Error.new(
476
+ message,
477
+ hint: body[:hint],
478
+ code: body[:code] || "http_#{status_code}",
479
+ context: { service: service, operation: operation, status_code: status_code }
480
+ )
481
+ end
482
+ end
483
+
484
+ def parse_response_body(response)
485
+ JSON.parse(response.body, symbolize_names: true)
486
+ rescue JSON::ParserError, TypeError
487
+ {}
488
+ end
489
+ end
490
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Flux
5
+ class Buffer
6
+ MAX_EVENTS = 100
7
+ MAX_METRICS = 100
8
+ FLUSH_INTERVAL = 5 # seconds
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @events = []
13
+ @metrics = []
14
+ @mutex = Mutex.new
15
+ @last_flush = Time.now
16
+
17
+ start_flush_thread
18
+ end
19
+
20
+ def add(type, data)
21
+ @mutex.synchronize do
22
+ case type
23
+ when :event
24
+ @events << data
25
+ when :metric
26
+ @metrics << data
27
+ end
28
+
29
+ flush_if_needed
30
+ end
31
+ end
32
+
33
+ def flush!
34
+ events_to_send = nil
35
+ metrics_to_send = nil
36
+
37
+ @mutex.synchronize do
38
+ events_to_send = @events.dup
39
+ metrics_to_send = @metrics.dup
40
+ @events.clear
41
+ @metrics.clear
42
+ @last_flush = Time.now
43
+ end
44
+
45
+ send_batch(events_to_send, metrics_to_send)
46
+ end
47
+
48
+ def size
49
+ @mutex.synchronize { @events.size + @metrics.size }
50
+ end
51
+
52
+ private
53
+
54
+ def flush_if_needed
55
+ should_flush = @events.size >= MAX_EVENTS ||
56
+ @metrics.size >= MAX_METRICS ||
57
+ Time.now - @last_flush >= FLUSH_INTERVAL
58
+
59
+ flush_async if should_flush
60
+ end
61
+
62
+ def flush_async
63
+ events_to_send = @events.dup
64
+ metrics_to_send = @metrics.dup
65
+ @events.clear
66
+ @metrics.clear
67
+ @last_flush = Time.now
68
+
69
+ Thread.new do
70
+ send_batch(events_to_send, metrics_to_send)
71
+ end
72
+ end
73
+
74
+ def send_batch(events, metrics)
75
+ return if events.empty? && metrics.empty?
76
+
77
+ @client.send_batch(events: events, metrics: metrics)
78
+ rescue StandardError => e
79
+ BrainzLab.debug("[Flux] Batch send failed: #{e.message}")
80
+ end
81
+
82
+ def start_flush_thread
83
+ Thread.new do
84
+ loop do
85
+ sleep FLUSH_INTERVAL
86
+ begin
87
+ flush! if size.positive?
88
+ rescue StandardError => e
89
+ BrainzLab.debug("[Flux] Flush thread error: #{e.message}")
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BrainzLab
8
+ module Flux
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def send_event(event)
15
+ post('/api/v1/events', event)
16
+ end
17
+
18
+ def send_events(events)
19
+ post('/api/v1/events/batch', { events: events })
20
+ end
21
+
22
+ def send_metric(metric)
23
+ post('/api/v1/metrics', metric)
24
+ end
25
+
26
+ def send_metrics(metrics)
27
+ post('/api/v1/metrics/batch', { metrics: metrics })
28
+ end
29
+
30
+ def send_batch(events:, metrics:)
31
+ post('/api/v1/flux/batch', { events: events, metrics: metrics })
32
+ end
33
+
34
+ private
35
+
36
+ def post(path, body)
37
+ uri = URI.parse("#{base_url}#{path}")
38
+ http = Net::HTTP.new(uri.host, uri.port)
39
+ http.use_ssl = uri.scheme == 'https'
40
+ http.open_timeout = 5
41
+ http.read_timeout = 10
42
+
43
+ request = Net::HTTP::Post.new(uri.path)
44
+ request['Content-Type'] = 'application/json'
45
+ request['Authorization'] = "Bearer #{api_key}"
46
+ request['User-Agent'] = "brainzlab-sdk/#{BrainzLab::VERSION}"
47
+ request.body = body.to_json
48
+
49
+ response = http.request(request)
50
+
51
+ BrainzLab.debug("[Flux] Request failed: #{response.code} - #{response.body}") unless response.is_a?(Net::HTTPSuccess)
52
+
53
+ response
54
+ rescue StandardError => e
55
+ BrainzLab.debug("[Flux] Request error: #{e.message}")
56
+ nil
57
+ end
58
+
59
+ def base_url
60
+ @config.flux_url
61
+ end
62
+
63
+ def api_key
64
+ @config.flux_ingest_key || @config.flux_api_key
65
+ end
66
+ end
67
+ end
68
+ end