vectra-client 0.3.0 → 0.3.2

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.
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+
6
+ module Vectra
7
+ # Structured JSON logger for Vectra operations
8
+ #
9
+ # Provides consistent, machine-readable logging for all Vectra operations.
10
+ # Output is JSON formatted for easy parsing by log aggregators.
11
+ #
12
+ # @example Basic usage
13
+ # Vectra.configure do |config|
14
+ # config.logger = Vectra::JsonLogger.new(STDOUT)
15
+ # end
16
+ #
17
+ # @example With file output
18
+ # Vectra.configure do |config|
19
+ # config.logger = Vectra::JsonLogger.new("log/vectra.log")
20
+ # end
21
+ #
22
+ # @example With custom metadata
23
+ # logger = Vectra::JsonLogger.new(STDOUT, app: "my-app", env: "production")
24
+ #
25
+ class JsonLogger
26
+ SEVERITY_LABELS = {
27
+ Logger::DEBUG => "debug",
28
+ Logger::INFO => "info",
29
+ Logger::WARN => "warn",
30
+ Logger::ERROR => "error",
31
+ Logger::FATAL => "fatal"
32
+ }.freeze
33
+
34
+ attr_reader :output, :default_metadata
35
+
36
+ # Initialize JSON logger
37
+ #
38
+ # @param output [IO, String] Output destination (IO object or file path)
39
+ # @param metadata [Hash] Default metadata to include in all log entries
40
+ def initialize(output = $stdout, **metadata)
41
+ @output = resolve_output(output)
42
+ @default_metadata = metadata
43
+ @mutex = Mutex.new
44
+ end
45
+
46
+ # Log debug message
47
+ #
48
+ # @param message [String] Log message
49
+ # @param data [Hash] Additional data
50
+ def debug(message, **data)
51
+ log(Logger::DEBUG, message, **data)
52
+ end
53
+
54
+ # Log info message
55
+ #
56
+ # @param message [String] Log message
57
+ # @param data [Hash] Additional data
58
+ def info(message, **data)
59
+ log(Logger::INFO, message, **data)
60
+ end
61
+
62
+ # Log warning message
63
+ #
64
+ # @param message [String] Log message
65
+ # @param data [Hash] Additional data
66
+ def warn(message, **data)
67
+ log(Logger::WARN, message, **data)
68
+ end
69
+
70
+ # Log error message
71
+ #
72
+ # @param message [String] Log message
73
+ # @param data [Hash] Additional data
74
+ def error(message, **data)
75
+ log(Logger::ERROR, message, **data)
76
+ end
77
+
78
+ # Log fatal message
79
+ #
80
+ # @param message [String] Log message
81
+ # @param data [Hash] Additional data
82
+ def fatal(message, **data)
83
+ log(Logger::FATAL, message, **data)
84
+ end
85
+
86
+ # Log Vectra operation event
87
+ #
88
+ # @param event [Instrumentation::Event] Operation event
89
+ def log_operation(event)
90
+ data = {
91
+ provider: event.provider.to_s,
92
+ operation: event.operation.to_s,
93
+ index: event.index,
94
+ duration_ms: event.duration,
95
+ success: event.success?
96
+ }
97
+
98
+ # Add metadata
99
+ data[:vector_count] = event.metadata[:vector_count] if event.metadata[:vector_count]
100
+ data[:result_count] = event.metadata[:result_count] if event.metadata[:result_count]
101
+
102
+ # Add error info if present
103
+ if event.error
104
+ data[:error_class] = event.error.class.name
105
+ data[:error_message] = event.error.message
106
+ end
107
+
108
+ level = event.success? ? Logger::INFO : Logger::ERROR
109
+ log(level, "vectra.#{event.operation}", **data)
110
+ end
111
+
112
+ # Close the logger
113
+ #
114
+ # @return [void]
115
+ def close
116
+ @output.close if @output.respond_to?(:close) && @output != $stdout && @output != $stderr
117
+ end
118
+
119
+ private
120
+
121
+ def log(severity, message, **data)
122
+ entry = build_entry(severity, message, data)
123
+
124
+ @mutex.synchronize do
125
+ @output.puts(JSON.generate(entry))
126
+ @output.flush if @output.respond_to?(:flush)
127
+ end
128
+ end
129
+
130
+ def build_entry(severity, message, data)
131
+ {
132
+ timestamp: Time.now.utc.iso8601(3),
133
+ level: SEVERITY_LABELS[severity],
134
+ logger: "vectra",
135
+ message: message
136
+ }.merge(default_metadata).merge(data).compact
137
+ end
138
+
139
+ def resolve_output(output)
140
+ case output
141
+ when IO, StringIO
142
+ output
143
+ when String
144
+ File.open(output, "a")
145
+ else
146
+ $stdout
147
+ end
148
+ end
149
+ end
150
+
151
+ # Instrumentation handler for JSON logging
152
+ #
153
+ # @example Enable JSON logging
154
+ # require 'vectra/logging'
155
+ #
156
+ # Vectra::Logging.setup!(
157
+ # output: "log/vectra.json.log",
158
+ # app: "my-service"
159
+ # )
160
+ #
161
+ module Logging
162
+ class << self
163
+ attr_reader :logger
164
+
165
+ # Setup JSON logging for Vectra
166
+ #
167
+ # @param output [IO, String] Log output
168
+ # @param metadata [Hash] Default metadata
169
+ # @return [JsonLogger]
170
+ def setup!(output: $stdout, **metadata)
171
+ @logger = JsonLogger.new(output, **metadata)
172
+
173
+ # Register as instrumentation handler
174
+ Vectra::Instrumentation.on_operation do |event|
175
+ @logger.log_operation(event)
176
+ end
177
+
178
+ # Also set as Vectra's logger for retry/error logging
179
+ Vectra.configuration.logger = @logger
180
+
181
+ @logger
182
+ end
183
+
184
+ # Log a custom event
185
+ #
186
+ # @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
187
+ # @param message [String] Log message
188
+ # @param data [Hash] Additional data
189
+ def log(level, message, **data)
190
+ return unless @logger
191
+
192
+ @logger.public_send(level, message, **data)
193
+ end
194
+ end
195
+ end
196
+
197
+ # Log formatter for standard Ruby Logger that outputs JSON
198
+ #
199
+ # @example With standard Logger
200
+ # logger = Logger.new(STDOUT)
201
+ # logger.formatter = Vectra::JsonFormatter.new(app: "my-app")
202
+ #
203
+ class JsonFormatter
204
+ attr_reader :default_metadata
205
+
206
+ def initialize(**metadata)
207
+ @default_metadata = metadata
208
+ end
209
+
210
+ def call(severity, time, _progname, message)
211
+ entry = {
212
+ timestamp: time.utc.iso8601(3),
213
+ level: severity.downcase,
214
+ logger: "vectra",
215
+ message: format_message(message)
216
+ }.merge(default_metadata)
217
+
218
+ # Parse structured data if message is a hash
219
+ if message.is_a?(Hash)
220
+ entry.merge!(message)
221
+ entry[:message] = message[:message] || message[:msg] || "operation"
222
+ end
223
+
224
+ "#{JSON.generate(entry.compact)}\n"
225
+ end
226
+
227
+ private
228
+
229
+ def format_message(message)
230
+ case message
231
+ when String
232
+ message
233
+ when Exception
234
+ "#{message.class}: #{message.message}"
235
+ when Hash
236
+ message[:message] || message.to_s
237
+ else
238
+ message.to_s
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ # Proactive rate limiter using token bucket algorithm
5
+ #
6
+ # Throttles requests BEFORE sending to prevent rate limit errors from providers.
7
+ # Uses token bucket algorithm for smooth rate limiting with burst support.
8
+ #
9
+ # @example Basic usage
10
+ # limiter = Vectra::RateLimiter.new(
11
+ # requests_per_second: 10,
12
+ # burst_size: 20
13
+ # )
14
+ #
15
+ # limiter.acquire do
16
+ # client.query(...)
17
+ # end
18
+ #
19
+ # @example With client wrapper
20
+ # client = Vectra::RateLimitedClient.new(
21
+ # Vectra::Client.new(...),
22
+ # requests_per_second: 10
23
+ # )
24
+ # client.query(...) # Automatically rate limited
25
+ #
26
+ class RateLimiter
27
+ class RateLimitExceededError < Vectra::Error
28
+ attr_reader :wait_time
29
+
30
+ def initialize(wait_time:)
31
+ @wait_time = wait_time
32
+ super("Rate limit exceeded. Retry after #{wait_time.round(2)} seconds")
33
+ end
34
+ end
35
+
36
+ attr_reader :requests_per_second, :burst_size
37
+
38
+ # Initialize rate limiter
39
+ #
40
+ # @param requests_per_second [Float] Sustained request rate
41
+ # @param burst_size [Integer] Maximum burst capacity (default: 2x RPS)
42
+ def initialize(requests_per_second:, burst_size: nil)
43
+ @requests_per_second = requests_per_second.to_f
44
+ @burst_size = burst_size || (requests_per_second * 2).to_i
45
+ @tokens = @burst_size.to_f
46
+ @last_refill = Time.now
47
+ @mutex = Mutex.new
48
+ end
49
+
50
+ # Acquire a token and execute block
51
+ #
52
+ # @param wait [Boolean] Wait for token if not available (default: true)
53
+ # @param timeout [Float] Maximum wait time in seconds (default: 30)
54
+ # @yield Block to execute after acquiring token
55
+ # @return [Object] Result of block
56
+ # @raise [RateLimitExceededError] If wait is false and no token available
57
+ def acquire(wait: true, timeout: 30, &)
58
+ acquired = try_acquire(wait: wait, timeout: timeout)
59
+
60
+ unless acquired
61
+ wait_time = time_until_token
62
+ raise RateLimitExceededError.new(wait_time: wait_time)
63
+ end
64
+
65
+ yield
66
+ end
67
+
68
+ # Try to acquire a token without blocking
69
+ #
70
+ # @return [Boolean] true if token acquired
71
+ def try_acquire(wait: false, timeout: 30)
72
+ deadline = Time.now + timeout
73
+
74
+ loop do
75
+ @mutex.synchronize do
76
+ refill_tokens
77
+ if @tokens >= 1
78
+ @tokens -= 1
79
+ return true
80
+ end
81
+ end
82
+
83
+ return false unless wait
84
+ return false if Time.now >= deadline
85
+
86
+ # Wait a bit before retrying
87
+ sleep([time_until_token, 0.1].min)
88
+ end
89
+ end
90
+
91
+ # Get current token count
92
+ #
93
+ # @return [Float]
94
+ def available_tokens
95
+ @mutex.synchronize do
96
+ refill_tokens
97
+ @tokens
98
+ end
99
+ end
100
+
101
+ # Get time until next token is available
102
+ #
103
+ # @return [Float] Seconds until next token
104
+ def time_until_token
105
+ @mutex.synchronize do
106
+ refill_tokens
107
+ return 0 if @tokens >= 1
108
+
109
+ tokens_needed = 1 - @tokens
110
+ tokens_needed / @requests_per_second
111
+ end
112
+ end
113
+
114
+ # Get rate limiter statistics
115
+ #
116
+ # @return [Hash]
117
+ def stats
118
+ @mutex.synchronize do
119
+ refill_tokens
120
+ {
121
+ requests_per_second: @requests_per_second,
122
+ burst_size: @burst_size,
123
+ available_tokens: @tokens.round(2),
124
+ time_until_token: @tokens >= 1 ? 0 : ((1 - @tokens) / @requests_per_second).round(3)
125
+ }
126
+ end
127
+ end
128
+
129
+ # Reset the rate limiter to full capacity
130
+ #
131
+ # @return [void]
132
+ def reset!
133
+ @mutex.synchronize do
134
+ @tokens = @burst_size.to_f
135
+ @last_refill = Time.now
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def refill_tokens
142
+ now = Time.now
143
+ elapsed = now - @last_refill
144
+ @last_refill = now
145
+
146
+ # Add tokens based on elapsed time
147
+ new_tokens = elapsed * @requests_per_second
148
+ @tokens = [@tokens + new_tokens, @burst_size.to_f].min
149
+ end
150
+ end
151
+
152
+ # Client wrapper with automatic rate limiting
153
+ #
154
+ # @example
155
+ # client = Vectra::RateLimitedClient.new(
156
+ # Vectra::Client.new(provider: :pinecone, ...),
157
+ # requests_per_second: 10,
158
+ # burst_size: 20
159
+ # )
160
+ #
161
+ # client.query(...) # Automatically waits if rate limited
162
+ #
163
+ class RateLimitedClient
164
+ RATE_LIMITED_METHODS = [:upsert, :query, :fetch, :update, :delete].freeze
165
+
166
+ attr_reader :client, :limiter
167
+
168
+ # Initialize rate limited client
169
+ #
170
+ # @param client [Client] The underlying Vectra client
171
+ # @param requests_per_second [Float] Request rate limit
172
+ # @param burst_size [Integer] Burst capacity
173
+ # @param wait [Boolean] Wait for rate limit (default: true)
174
+ def initialize(client, requests_per_second:, burst_size: nil, wait: true)
175
+ @client = client
176
+ @limiter = RateLimiter.new(
177
+ requests_per_second: requests_per_second,
178
+ burst_size: burst_size
179
+ )
180
+ @wait = wait
181
+ end
182
+
183
+ # Rate-limited upsert
184
+ def upsert(...)
185
+ with_rate_limit { client.upsert(...) }
186
+ end
187
+
188
+ # Rate-limited query
189
+ def query(...)
190
+ with_rate_limit { client.query(...) }
191
+ end
192
+
193
+ # Rate-limited fetch
194
+ def fetch(...)
195
+ with_rate_limit { client.fetch(...) }
196
+ end
197
+
198
+ # Rate-limited update
199
+ def update(...)
200
+ with_rate_limit { client.update(...) }
201
+ end
202
+
203
+ # Rate-limited delete
204
+ def delete(...)
205
+ with_rate_limit { client.delete(...) }
206
+ end
207
+
208
+ # Pass through other methods without rate limiting
209
+ def method_missing(method, *, **, &)
210
+ if client.respond_to?(method)
211
+ client.public_send(method, *, **, &)
212
+ else
213
+ super
214
+ end
215
+ end
216
+
217
+ def respond_to_missing?(method, include_private = false)
218
+ client.respond_to?(method, include_private) || super
219
+ end
220
+
221
+ # Get rate limiter stats
222
+ #
223
+ # @return [Hash]
224
+ def rate_limit_stats
225
+ limiter.stats
226
+ end
227
+
228
+ private
229
+
230
+ def with_rate_limit(&)
231
+ limiter.acquire(wait: @wait, &)
232
+ end
233
+ end
234
+
235
+ # Per-provider rate limiter registry
236
+ #
237
+ # @example
238
+ # # Configure rate limits per provider
239
+ # Vectra::RateLimiterRegistry.configure(:pinecone, requests_per_second: 100)
240
+ # Vectra::RateLimiterRegistry.configure(:qdrant, requests_per_second: 50)
241
+ #
242
+ # # Get limiter for provider
243
+ # limiter = Vectra::RateLimiterRegistry[:pinecone]
244
+ # limiter.acquire { ... }
245
+ #
246
+ module RateLimiterRegistry
247
+ class << self
248
+ # Configure rate limiter for provider
249
+ #
250
+ # @param provider [Symbol] Provider name
251
+ # @param requests_per_second [Float] Request rate
252
+ # @param burst_size [Integer] Burst capacity
253
+ # @return [RateLimiter]
254
+ def configure(provider, requests_per_second:, burst_size: nil)
255
+ limiters[provider.to_sym] = RateLimiter.new(
256
+ requests_per_second: requests_per_second,
257
+ burst_size: burst_size
258
+ )
259
+ end
260
+
261
+ # Get limiter for provider
262
+ #
263
+ # @param provider [Symbol] Provider name
264
+ # @return [RateLimiter, nil]
265
+ def [](provider)
266
+ limiters[provider.to_sym]
267
+ end
268
+
269
+ # Get all configured limiters
270
+ #
271
+ # @return [Hash<Symbol, RateLimiter>]
272
+ def all
273
+ limiters.dup
274
+ end
275
+
276
+ # Get stats for all limiters
277
+ #
278
+ # @return [Hash<Symbol, Hash>]
279
+ def stats
280
+ limiters.transform_values(&:stats)
281
+ end
282
+
283
+ # Reset all limiters
284
+ #
285
+ # @return [void]
286
+ def reset_all!
287
+ limiters.each_value(&:reset!)
288
+ end
289
+
290
+ # Clear all limiters
291
+ #
292
+ # @return [void]
293
+ def clear!
294
+ @limiters = {}
295
+ end
296
+
297
+ private
298
+
299
+ def limiters
300
+ @limiters ||= {}
301
+ end
302
+ end
303
+ end
304
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vectra
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/vectra.rb CHANGED
@@ -11,6 +11,12 @@ require_relative "vectra/batch"
11
11
  require_relative "vectra/streaming"
12
12
  require_relative "vectra/cache"
13
13
  require_relative "vectra/pool"
14
+ require_relative "vectra/circuit_breaker"
15
+ require_relative "vectra/rate_limiter"
16
+ require_relative "vectra/logging"
17
+ require_relative "vectra/health_check"
18
+ require_relative "vectra/credential_rotation"
19
+ require_relative "vectra/audit_log"
14
20
  require_relative "vectra/active_record"
15
21
  require_relative "vectra/providers/base"
16
22
  require_relative "vectra/providers/pinecone"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vectra-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mijo Kristo
@@ -258,7 +258,13 @@ files:
258
258
  - docs/examples/index.md
259
259
  - docs/guides/getting-started.md
260
260
  - docs/guides/installation.md
261
+ - docs/guides/monitoring.md
261
262
  - docs/guides/performance.md
263
+ - docs/guides/runbooks/cache-issues.md
264
+ - docs/guides/runbooks/high-error-rate.md
265
+ - docs/guides/runbooks/high-latency.md
266
+ - docs/guides/runbooks/pool-exhausted.md
267
+ - docs/guides/security.md
262
268
  - docs/index.md
263
269
  - docs/providers/index.md
264
270
  - docs/providers/pgvector.md
@@ -272,14 +278,21 @@ files:
272
278
  - lib/generators/vectra/templates/vectra.rb
273
279
  - lib/vectra.rb
274
280
  - lib/vectra/active_record.rb
281
+ - lib/vectra/audit_log.rb
275
282
  - lib/vectra/batch.rb
276
283
  - lib/vectra/cache.rb
284
+ - lib/vectra/circuit_breaker.rb
277
285
  - lib/vectra/client.rb
278
286
  - lib/vectra/configuration.rb
287
+ - lib/vectra/credential_rotation.rb
279
288
  - lib/vectra/errors.rb
289
+ - lib/vectra/health_check.rb
280
290
  - lib/vectra/instrumentation.rb
281
291
  - lib/vectra/instrumentation/datadog.rb
292
+ - lib/vectra/instrumentation/honeybadger.rb
282
293
  - lib/vectra/instrumentation/new_relic.rb
294
+ - lib/vectra/instrumentation/sentry.rb
295
+ - lib/vectra/logging.rb
283
296
  - lib/vectra/pool.rb
284
297
  - lib/vectra/providers/base.rb
285
298
  - lib/vectra/providers/pgvector.rb
@@ -290,6 +303,7 @@ files:
290
303
  - lib/vectra/providers/qdrant.rb
291
304
  - lib/vectra/providers/weaviate.rb
292
305
  - lib/vectra/query_result.rb
306
+ - lib/vectra/rate_limiter.rb
293
307
  - lib/vectra/retry.rb
294
308
  - lib/vectra/streaming.rb
295
309
  - lib/vectra/vector.rb