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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -37
- data/SECURITY.md +134 -4
- data/docs/_layouts/page.html +2 -0
- data/docs/guides/monitoring.md +860 -0
- data/docs/guides/runbooks/cache-issues.md +267 -0
- data/docs/guides/runbooks/high-error-rate.md +152 -0
- data/docs/guides/runbooks/high-latency.md +287 -0
- data/docs/guides/runbooks/pool-exhausted.md +216 -0
- data/docs/guides/security.md +348 -0
- data/lib/vectra/audit_log.rb +225 -0
- data/lib/vectra/circuit_breaker.rb +336 -0
- data/lib/vectra/client.rb +2 -0
- data/lib/vectra/credential_rotation.rb +199 -0
- data/lib/vectra/health_check.rb +254 -0
- data/lib/vectra/instrumentation/honeybadger.rb +128 -0
- data/lib/vectra/instrumentation/sentry.rb +117 -0
- data/lib/vectra/logging.rb +242 -0
- data/lib/vectra/rate_limiter.rb +304 -0
- data/lib/vectra/version.rb +1 -1
- data/lib/vectra.rb +6 -0
- metadata +15 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vectra
|
|
4
|
+
# Credential rotation helper for seamless API key updates
|
|
5
|
+
#
|
|
6
|
+
# Provides utilities for rotating API keys without downtime by supporting
|
|
7
|
+
# multiple credentials and gradual migration.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic rotation
|
|
10
|
+
# rotator = Vectra::CredentialRotator.new(
|
|
11
|
+
# primary_key: ENV['PINECONE_API_KEY'],
|
|
12
|
+
# secondary_key: ENV['PINECONE_API_KEY_NEW']
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# # Test new key before switching
|
|
16
|
+
# if rotator.test_secondary
|
|
17
|
+
# rotator.switch_to_secondary
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class CredentialRotator
|
|
21
|
+
attr_reader :primary_key, :secondary_key, :current_key
|
|
22
|
+
|
|
23
|
+
# Initialize credential rotator
|
|
24
|
+
#
|
|
25
|
+
# @param primary_key [String] Current active API key
|
|
26
|
+
# @param secondary_key [String, nil] New API key to rotate to
|
|
27
|
+
# @param provider [Symbol] Provider name
|
|
28
|
+
# @param test_client [Client, nil] Client instance for testing
|
|
29
|
+
def initialize(primary_key:, secondary_key: nil, provider: nil, test_client: nil)
|
|
30
|
+
@primary_key = primary_key
|
|
31
|
+
@secondary_key = secondary_key
|
|
32
|
+
@provider = provider
|
|
33
|
+
@test_client = test_client
|
|
34
|
+
@current_key = primary_key
|
|
35
|
+
@rotation_complete = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Test if secondary key is valid
|
|
39
|
+
#
|
|
40
|
+
# @param timeout [Float] Test timeout in seconds
|
|
41
|
+
# @return [Boolean] true if secondary key works
|
|
42
|
+
def test_secondary(timeout: 5)
|
|
43
|
+
return false if secondary_key.nil? || secondary_key.empty?
|
|
44
|
+
|
|
45
|
+
client = build_test_client(secondary_key)
|
|
46
|
+
client.healthy?(timeout: timeout)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Switch to secondary key
|
|
52
|
+
#
|
|
53
|
+
# @param validate [Boolean] Validate key before switching
|
|
54
|
+
# @return [Boolean] true if switched successfully
|
|
55
|
+
# rubocop:disable Naming/PredicateMethod
|
|
56
|
+
def switch_to_secondary(validate: true)
|
|
57
|
+
return false if secondary_key.nil? || secondary_key.empty?
|
|
58
|
+
|
|
59
|
+
if validate && !test_secondary
|
|
60
|
+
raise CredentialRotationError, "Secondary key validation failed"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@current_key = secondary_key
|
|
64
|
+
@rotation_complete = true
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Naming/PredicateMethod
|
|
68
|
+
|
|
69
|
+
# Rollback to primary key
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
def rollback
|
|
73
|
+
@current_key = primary_key
|
|
74
|
+
@rotation_complete = false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check if rotation is complete
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean]
|
|
80
|
+
def rotation_complete?
|
|
81
|
+
@rotation_complete
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get current active key
|
|
85
|
+
#
|
|
86
|
+
# @return [String]
|
|
87
|
+
def active_key
|
|
88
|
+
@current_key
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def build_test_client(key)
|
|
94
|
+
return @test_client if @test_client
|
|
95
|
+
|
|
96
|
+
config = Vectra::Configuration.new
|
|
97
|
+
config.provider = @provider || Vectra.configuration.provider
|
|
98
|
+
config.api_key = key
|
|
99
|
+
config.host = Vectra.configuration.host
|
|
100
|
+
config.environment = Vectra.configuration.environment
|
|
101
|
+
|
|
102
|
+
Vectra::Client.new(
|
|
103
|
+
provider: config.provider,
|
|
104
|
+
api_key: key,
|
|
105
|
+
host: config.host,
|
|
106
|
+
environment: config.environment
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Error raised during credential rotation
|
|
112
|
+
class CredentialRotationError < Vectra::Error; end
|
|
113
|
+
|
|
114
|
+
# Credential rotation manager for multiple providers
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# manager = Vectra::CredentialRotationManager.new
|
|
118
|
+
#
|
|
119
|
+
# manager.register(:pinecone,
|
|
120
|
+
# primary: ENV['PINECONE_API_KEY'],
|
|
121
|
+
# secondary: ENV['PINECONE_API_KEY_NEW']
|
|
122
|
+
# )
|
|
123
|
+
#
|
|
124
|
+
# manager.rotate_all
|
|
125
|
+
#
|
|
126
|
+
module CredentialRotationManager
|
|
127
|
+
class << self
|
|
128
|
+
# Register a credential rotator
|
|
129
|
+
#
|
|
130
|
+
# @param provider [Symbol] Provider name
|
|
131
|
+
# @param primary [String] Primary API key
|
|
132
|
+
# @param secondary [String, nil] Secondary API key
|
|
133
|
+
# @return [CredentialRotator]
|
|
134
|
+
def register(provider, primary:, secondary: nil)
|
|
135
|
+
rotators[provider] = CredentialRotator.new(
|
|
136
|
+
primary_key: primary,
|
|
137
|
+
secondary_key: secondary,
|
|
138
|
+
provider: provider
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get rotator for provider
|
|
143
|
+
#
|
|
144
|
+
# @param provider [Symbol] Provider name
|
|
145
|
+
# @return [CredentialRotator, nil]
|
|
146
|
+
def [](provider)
|
|
147
|
+
rotators[provider.to_sym]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Test all secondary keys
|
|
151
|
+
#
|
|
152
|
+
# @return [Hash<Symbol, Boolean>] Test results
|
|
153
|
+
def test_all_secondary
|
|
154
|
+
rotators.transform_values(&:test_secondary)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Rotate all providers
|
|
158
|
+
#
|
|
159
|
+
# @param validate [Boolean] Validate before rotating
|
|
160
|
+
# @return [Hash<Symbol, Boolean>] Rotation results
|
|
161
|
+
def rotate_all(validate: true)
|
|
162
|
+
rotators.transform_values { |r| r.switch_to_secondary(validate: validate) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Rollback all rotations
|
|
166
|
+
#
|
|
167
|
+
# @return [void]
|
|
168
|
+
def rollback_all
|
|
169
|
+
rotators.each_value(&:rollback)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get rotation status
|
|
173
|
+
#
|
|
174
|
+
# @return [Hash<Symbol, Hash>]
|
|
175
|
+
def status
|
|
176
|
+
rotators.transform_values do |r|
|
|
177
|
+
{
|
|
178
|
+
rotation_complete: r.rotation_complete?,
|
|
179
|
+
has_secondary: !r.secondary_key.nil?,
|
|
180
|
+
active_key: "#{r.active_key[0, 8]}..." # First 8 chars only
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Clear all rotators
|
|
186
|
+
#
|
|
187
|
+
# @return [void]
|
|
188
|
+
def clear!
|
|
189
|
+
@rotators = {}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def rotators
|
|
195
|
+
@rotators ||= {}
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -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
|