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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- 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 +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- 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 +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- 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
|