vectra-client 0.3.0 → 0.3.1

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,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Vectra
6
+ # Health check functionality for Vectra clients
7
+ #
8
+ # Provides health check methods to verify connectivity and status
9
+ # of vector database providers.
10
+ #
11
+ # @example Basic health check
12
+ # client = Vectra::Client.new(provider: :pinecone, ...)
13
+ # result = client.health_check
14
+ # puts result[:healthy] # => true/false
15
+ #
16
+ # @example Detailed health check
17
+ # result = client.health_check(
18
+ # index: "my-index",
19
+ # include_stats: true
20
+ # )
21
+ #
22
+ module HealthCheck
23
+ # Perform health check on the provider
24
+ #
25
+ # @param index [String, nil] Index to check (uses first available if nil)
26
+ # @param include_stats [Boolean] Include index statistics
27
+ # @param timeout [Float] Health check timeout in seconds
28
+ # @return [HealthCheckResult]
29
+ def health_check(index: nil, include_stats: false, timeout: 5)
30
+ start_time = Time.now
31
+
32
+ indexes = with_timeout(timeout) { list_indexes }
33
+ index_name = index || indexes.first&.dig(:name)
34
+
35
+ result = base_result(start_time, indexes)
36
+ add_index_stats(result, index_name, include_stats, timeout)
37
+ add_pool_stats(result)
38
+
39
+ HealthCheckResult.new(**result)
40
+ rescue StandardError => e
41
+ failure_result(start_time, e)
42
+ end
43
+
44
+ # Quick health check - just tests connectivity
45
+ #
46
+ # @param timeout [Float] Timeout in seconds
47
+ # @return [Boolean] true if healthy
48
+ def healthy?(timeout: 5)
49
+ health_check(timeout: timeout).healthy?
50
+ end
51
+
52
+ private
53
+
54
+ def with_timeout(seconds, &)
55
+ Timeout.timeout(seconds, &)
56
+ rescue Timeout::Error
57
+ raise Vectra::TimeoutError, "Health check timed out after #{seconds}s"
58
+ end
59
+
60
+ def base_result(start_time, indexes)
61
+ {
62
+ healthy: true,
63
+ provider: provider_name,
64
+ latency_ms: latency_since(start_time),
65
+ indexes_available: indexes.size,
66
+ checked_at: current_time_iso
67
+ }
68
+ end
69
+
70
+ def add_index_stats(result, index_name, include_stats, timeout)
71
+ return unless include_stats && index_name
72
+
73
+ stats = with_timeout(timeout) { stats(index: index_name) }
74
+ result[:index] = index_name
75
+ result[:stats] = {
76
+ vector_count: stats[:total_vector_count],
77
+ dimension: stats[:dimension]
78
+ }.compact
79
+ end
80
+
81
+ def add_pool_stats(result)
82
+ return unless provider.respond_to?(:pool_stats)
83
+
84
+ pool = provider.pool_stats
85
+ result[:pool] = pool unless pool[:status] == "not_initialized"
86
+ end
87
+
88
+ def failure_result(start_time, error)
89
+ HealthCheckResult.new(
90
+ healthy: false,
91
+ provider: provider_name,
92
+ latency_ms: latency_since(start_time),
93
+ error: error.class.name,
94
+ error_message: error.message,
95
+ checked_at: current_time_iso
96
+ )
97
+ end
98
+
99
+ def latency_since(start_time)
100
+ ((Time.now - start_time) * 1000).round(2)
101
+ end
102
+
103
+ def current_time_iso
104
+ Time.now.utc.iso8601
105
+ end
106
+ end
107
+
108
+ # Health check result object
109
+ #
110
+ # @example
111
+ # result = client.health_check
112
+ # if result.healthy?
113
+ # puts "All good! Latency: #{result.latency_ms}ms"
114
+ # else
115
+ # puts "Error: #{result.error_message}"
116
+ # end
117
+ #
118
+ class HealthCheckResult
119
+ attr_reader :provider, :latency_ms, :indexes_available, :checked_at,
120
+ :index, :stats, :pool, :error, :error_message
121
+
122
+ def initialize(healthy:, provider:, latency_ms:, checked_at:,
123
+ indexes_available: nil, index: nil, stats: nil,
124
+ pool: nil, error: nil, error_message: nil)
125
+ @healthy = healthy
126
+ @provider = provider
127
+ @latency_ms = latency_ms
128
+ @checked_at = checked_at
129
+ @indexes_available = indexes_available
130
+ @index = index
131
+ @stats = stats
132
+ @pool = pool
133
+ @error = error
134
+ @error_message = error_message
135
+ end
136
+
137
+ # Check if the health check passed
138
+ #
139
+ # @return [Boolean]
140
+ def healthy?
141
+ @healthy
142
+ end
143
+
144
+ # Check if the health check failed
145
+ #
146
+ # @return [Boolean]
147
+ def unhealthy?
148
+ !@healthy
149
+ end
150
+
151
+ # Convert to hash
152
+ #
153
+ # @return [Hash]
154
+ def to_h
155
+ {
156
+ healthy: @healthy,
157
+ provider: provider,
158
+ latency_ms: latency_ms,
159
+ checked_at: checked_at,
160
+ indexes_available: indexes_available,
161
+ index: index,
162
+ stats: stats,
163
+ pool: pool,
164
+ error: error,
165
+ error_message: error_message
166
+ }.compact
167
+ end
168
+
169
+ # Convert to JSON
170
+ #
171
+ # @return [String]
172
+ def to_json(*)
173
+ JSON.generate(to_h)
174
+ end
175
+ end
176
+
177
+ # Aggregate health checker for multiple providers
178
+ #
179
+ # @example
180
+ # checker = Vectra::AggregateHealthCheck.new(
181
+ # pinecone: pinecone_client,
182
+ # qdrant: qdrant_client,
183
+ # pgvector: pgvector_client
184
+ # )
185
+ #
186
+ # result = checker.check_all
187
+ # puts result[:overall_healthy]
188
+ #
189
+ class AggregateHealthCheck
190
+ attr_reader :clients
191
+
192
+ # Initialize aggregate health checker
193
+ #
194
+ # @param clients [Hash<Symbol, Client>] Named clients to check
195
+ def initialize(**clients)
196
+ @clients = clients
197
+ end
198
+
199
+ # Check health of all clients
200
+ #
201
+ # @param parallel [Boolean] Run checks in parallel
202
+ # @param timeout [Float] Timeout per check
203
+ # @return [Hash] Aggregate results
204
+ def check_all(parallel: true, timeout: 5)
205
+ start_time = Time.now
206
+
207
+ results = if parallel
208
+ check_parallel(timeout)
209
+ else
210
+ check_sequential(timeout)
211
+ end
212
+
213
+ healthy_count = results.count { |_, r| r.healthy? }
214
+ all_healthy = healthy_count == results.size
215
+
216
+ {
217
+ overall_healthy: all_healthy,
218
+ healthy_count: healthy_count,
219
+ total_count: results.size,
220
+ total_latency_ms: ((Time.now - start_time) * 1000).round(2),
221
+ checked_at: Time.now.utc.iso8601,
222
+ results: results.transform_values(&:to_h)
223
+ }
224
+ end
225
+
226
+ # Check if all providers are healthy
227
+ #
228
+ # @return [Boolean]
229
+ def all_healthy?(timeout: 5)
230
+ check_all(timeout: timeout)[:overall_healthy]
231
+ end
232
+
233
+ # Check if any provider is healthy
234
+ #
235
+ # @return [Boolean]
236
+ def any_healthy?(timeout: 5)
237
+ check_all(timeout: timeout)[:healthy_count].positive?
238
+ end
239
+
240
+ private
241
+
242
+ def check_parallel(timeout)
243
+ threads = clients.map do |name, client|
244
+ Thread.new { [name, client.health_check(timeout: timeout)] }
245
+ end
246
+
247
+ threads.to_h(&:value)
248
+ end
249
+
250
+ def check_sequential(timeout)
251
+ clients.transform_values { |client| client.health_check(timeout: timeout) }
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ module Instrumentation
5
+ # Honeybadger error tracking adapter
6
+ #
7
+ # Automatically reports Vectra errors to Honeybadger with context.
8
+ #
9
+ # @example Enable Honeybadger instrumentation
10
+ # # config/initializers/vectra.rb
11
+ # require 'vectra/instrumentation/honeybadger'
12
+ #
13
+ # Vectra.configure do |config|
14
+ # config.instrumentation = true
15
+ # end
16
+ #
17
+ # Vectra::Instrumentation::Honeybadger.setup!
18
+ #
19
+ module Honeybadger
20
+ class << self
21
+ # Setup Honeybadger instrumentation
22
+ #
23
+ # @param notify_on_rate_limit [Boolean] Report rate limit errors (default: false)
24
+ # @param notify_on_validation [Boolean] Report validation errors (default: false)
25
+ # @return [void]
26
+ def setup!(notify_on_rate_limit: false, notify_on_validation: false)
27
+ @notify_on_rate_limit = notify_on_rate_limit
28
+ @notify_on_validation = notify_on_validation
29
+
30
+ unless defined?(::Honeybadger)
31
+ warn "Honeybadger gem not found. Install with: gem 'honeybadger'"
32
+ return
33
+ end
34
+
35
+ Vectra::Instrumentation.on_operation do |event|
36
+ add_breadcrumb(event)
37
+ notify_error(event) if should_notify?(event)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Add breadcrumb for operation tracing
44
+ def add_breadcrumb(event)
45
+ ::Honeybadger.add_breadcrumb(
46
+ "Vectra #{event.operation}",
47
+ category: "vectra",
48
+ metadata: {
49
+ provider: event.provider.to_s,
50
+ operation: event.operation.to_s,
51
+ index: event.index,
52
+ duration_ms: event.duration,
53
+ success: event.success?,
54
+ vector_count: event.metadata[:vector_count],
55
+ result_count: event.metadata[:result_count]
56
+ }.compact
57
+ )
58
+ end
59
+
60
+ # Check if error should be reported
61
+ def should_notify?(event)
62
+ return false if event.success?
63
+
64
+ case event.error
65
+ when Vectra::RateLimitError
66
+ @notify_on_rate_limit
67
+ when Vectra::ValidationError
68
+ @notify_on_validation
69
+ else
70
+ true
71
+ end
72
+ end
73
+
74
+ # Notify Honeybadger of error
75
+ def notify_error(event)
76
+ ::Honeybadger.notify(
77
+ event.error,
78
+ context: {
79
+ vectra: {
80
+ provider: event.provider.to_s,
81
+ operation: event.operation.to_s,
82
+ index: event.index,
83
+ duration_ms: event.duration,
84
+ metadata: event.metadata
85
+ }
86
+ },
87
+ tags: build_tags(event),
88
+ fingerprint: build_fingerprint(event)
89
+ )
90
+ end
91
+
92
+ # Build tags for error grouping
93
+ def build_tags(event)
94
+ [
95
+ "vectra",
96
+ "provider:#{event.provider}",
97
+ "operation:#{event.operation}",
98
+ error_severity(event.error)
99
+ ]
100
+ end
101
+
102
+ # Build fingerprint for error grouping
103
+ def build_fingerprint(event)
104
+ [
105
+ "vectra",
106
+ event.provider.to_s,
107
+ event.operation.to_s,
108
+ event.error.class.name
109
+ ].join("-")
110
+ end
111
+
112
+ # Determine severity tag
113
+ def error_severity(error)
114
+ case error
115
+ when Vectra::AuthenticationError
116
+ "severity:critical"
117
+ when Vectra::ServerError
118
+ "severity:high"
119
+ when Vectra::RateLimitError
120
+ "severity:medium"
121
+ else
122
+ "severity:low"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vectra
4
+ module Instrumentation
5
+ # Sentry error tracking adapter
6
+ #
7
+ # Automatically reports Vectra errors to Sentry with context.
8
+ #
9
+ # @example Enable Sentry instrumentation
10
+ # # config/initializers/vectra.rb
11
+ # require 'vectra/instrumentation/sentry'
12
+ #
13
+ # Vectra.configure do |config|
14
+ # config.instrumentation = true
15
+ # end
16
+ #
17
+ # Vectra::Instrumentation::Sentry.setup!
18
+ #
19
+ module Sentry
20
+ class << self
21
+ # Setup Sentry instrumentation
22
+ #
23
+ # @param capture_all_errors [Boolean] Capture all errors, not just failures (default: false)
24
+ # @param fingerprint_by_operation [Boolean] Group errors by operation (default: true)
25
+ # @return [void]
26
+ def setup!(capture_all_errors: false, fingerprint_by_operation: true)
27
+ @capture_all_errors = capture_all_errors
28
+ @fingerprint_by_operation = fingerprint_by_operation
29
+
30
+ unless defined?(::Sentry)
31
+ warn "Sentry gem not found. Install with: gem 'sentry-ruby'"
32
+ return
33
+ end
34
+
35
+ Vectra::Instrumentation.on_operation do |event|
36
+ record_breadcrumb(event)
37
+ capture_error(event) if event.failure?
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Add breadcrumb for operation tracing
44
+ def record_breadcrumb(event)
45
+ ::Sentry.add_breadcrumb(
46
+ ::Sentry::Breadcrumb.new(
47
+ category: "vectra",
48
+ message: "#{event.operation} on #{event.index}",
49
+ level: event.success? ? "info" : "error",
50
+ data: {
51
+ provider: event.provider.to_s,
52
+ operation: event.operation.to_s,
53
+ index: event.index,
54
+ duration_ms: event.duration,
55
+ vector_count: event.metadata[:vector_count],
56
+ result_count: event.metadata[:result_count]
57
+ }.compact
58
+ )
59
+ )
60
+ end
61
+
62
+ # Capture error with context
63
+ def capture_error(event)
64
+ ::Sentry.with_scope do |scope|
65
+ scope.set_tags(
66
+ vectra_provider: event.provider.to_s,
67
+ vectra_operation: event.operation.to_s,
68
+ vectra_index: event.index
69
+ )
70
+
71
+ scope.set_context("vectra", build_context(event))
72
+
73
+ # Custom fingerprint to group similar errors
74
+ if @fingerprint_by_operation
75
+ scope.set_fingerprint(build_fingerprint(event))
76
+ end
77
+
78
+ # Set error level based on error type
79
+ scope.set_level(error_level(event.error))
80
+
81
+ ::Sentry.capture_exception(event.error)
82
+ end
83
+ end
84
+
85
+ # Build context hash for Sentry
86
+ def build_context(event)
87
+ {
88
+ provider: event.provider.to_s,
89
+ operation: event.operation.to_s,
90
+ index: event.index,
91
+ duration_ms: event.duration,
92
+ metadata: event.metadata
93
+ }
94
+ end
95
+
96
+ # Build fingerprint array for error grouping
97
+ def build_fingerprint(event)
98
+ ["vectra", event.provider.to_s, event.operation.to_s, event.error.class.name]
99
+ end
100
+
101
+ # Determine error level based on error type
102
+ def error_level(error)
103
+ case error
104
+ when Vectra::RateLimitError
105
+ :warning
106
+ when Vectra::ValidationError
107
+ :info
108
+ when Vectra::ServerError, Vectra::ConnectionError, Vectra::TimeoutError
109
+ :error
110
+ when Vectra::AuthenticationError
111
+ :fatal
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end