logstash-input-vespa 0.3.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0c4d47c3bf0a7f5265018a47f773cb938b83b451446d7fc2e77e04f47f9e217
4
- data.tar.gz: 9b4fd7081b3c45a8adb0560fbebec89dd07c5c5c08714aef8171c35e198e40e1
3
+ metadata.gz: cc362062aaf492eb77f990be4f6844b34051ef51c7a47cae6b268e48f0c3012f
4
+ data.tar.gz: f16fd1a3621feb7efdd4b65f039b94cb40a57434bc2e286e2310d794b00d2eb0
5
5
  SHA512:
6
- metadata.gz: 5802d28aa5b4949e7506ff44805ea7318d05e6ae9e2f000e89b874f1fbc00fe208f5ae54404b705e6ef40773158ea95ebbbdeb2e21829b9542f3a2ee00fe7a31
7
- data.tar.gz: 7907e63f27e29d5a98224ca0090d63dab632b314ce6839ed8265a661a593792d253172db0cd6a06000770ce33219c18b1c13abf7519ea69fdd93952f5e0368e5
6
+ metadata.gz: 23436b3c39d0259b144a1d82fd7fc970b824b74fd9e1bd90d320fbde5501dae3b42660708acef1e2d16fbec6f955dc945d5dab941dbd15b92eb2cf779ffe5851
7
+ data.tar.gz: c8d280292ad101c31f8795e7aab8ee3f74c5044e76d688bb354637ad1f6c9aeac5e3cc9e3f3c6472a78da7b02dcabf4c0b104b695961448519b382c38044937a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 0.3.0
2
+ added retry+backoff logic
3
+
1
4
  ## 0.2.0
2
5
  Added support for mTLS certificates, selector, page_size, backend_concurrency, timeout, from_timestamp, and to_timestamp
3
6
 
data/README.md CHANGED
@@ -20,6 +20,15 @@ export LOGSTASH_SOURCE=1
20
20
  bundle exec rspec
21
21
  ```
22
22
 
23
+ To run integration tests, you'll need to have a Vespa instance running with an app deployed that supports an "id" field. And Logstash installed.
24
+
25
+ Check out the `integration-test` directory for more information.
26
+
27
+ ```
28
+ cd integration-test
29
+ ./run_tests.sh
30
+ ```
31
+
23
32
  ## Usage
24
33
 
25
34
  Minimal Logstash config example:
@@ -50,6 +59,9 @@ input {
50
59
  client_cert => "/Users/myuser/.vespa/mytenant.myapp.default/data-plane-public-cert.pem"
51
60
  client_key => "/Users/myuser/.vespa/mytenant.myapp.default/data-plane-private-key.pem"
52
61
 
62
+ # as an alternative to mTLS, you can use an authentication token for Vespa Cloud
63
+ auth_token => "vespa_cloud_TOKEN_GOES_HERE"
64
+
53
65
  # page size
54
66
  page_size => 100
55
67
 
@@ -62,6 +74,12 @@ input {
62
74
  # HTTP request timeout
63
75
  timeout => 180
64
76
 
77
+ # maximum retries for failed HTTP requests
78
+ max_retries => 3
79
+
80
+ # delay in seconds for the first retry attempt. We double this delay for each subsequent retry.
81
+ retry_delay => 1
82
+
65
83
  # lower timestamp bound (microseconds since epoch)
66
84
  from_timestamp => 1600000000000000
67
85
 
@@ -73,4 +91,6 @@ input {
73
91
  output {
74
92
  stdout {}
75
93
  }
76
- ```
94
+ ```
95
+
96
+ To migrate from one Vespa cluster to another, see [this blog post](https://blog.vespa.ai/logstash-vespa-tutorials/).
@@ -37,6 +37,10 @@ class LogStash::Inputs::Vespa < LogStash::Inputs::Base
37
37
  # Path to the client key file for mTLS.
38
38
  config :client_key, :validate => :path
39
39
 
40
+ # Authentication token for Vespa Cloud
41
+ # it will be sent as a Bearer token in the Authorization header
42
+ config :auth_token, :validate => :string
43
+
40
44
  # desired page size for the visit request, i.e. the wantedDocumentCount parameter
41
45
  config :page_size, :validate => :number, :default => 100
42
46
 
@@ -160,12 +164,19 @@ class LogStash::Inputs::Vespa < LogStash::Inputs::Base
160
164
  http = Net::HTTP.new(uri.host, uri.port)
161
165
  if uri.scheme == "https"
162
166
  http.use_ssl = true
163
- http.cert = @cert
164
- http.key = @key
165
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
167
+ if @client_cert && @client_key
168
+ http.cert = @cert
169
+ http.key = @key
170
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
171
+ end
166
172
  end
167
173
 
168
174
  request = Net::HTTP::Get.new(uri.request_uri)
175
+ # Add auth token if provided
176
+ if @auth_token
177
+ request['Authorization'] = "Bearer #{@auth_token}"
178
+ end
179
+
169
180
  http.request(request)
170
181
  rescue => e
171
182
  retries += 1
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-input-vespa'
3
- s.version = '0.3.0'
3
+ s.version = '0.4.0'
4
4
  s.licenses = ['Apache-2.0']
5
5
  s.summary = "Logstash input plugin reading from Vespa"
6
6
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -2,6 +2,8 @@
2
2
  require "logstash/devutils/rspec/spec_helper"
3
3
  require "logstash/inputs/vespa"
4
4
  require "webmock/rspec"
5
+ require 'tempfile'
6
+ require 'openssl'
5
7
 
6
8
  describe LogStash::Inputs::Vespa do
7
9
  let(:config) do
@@ -112,5 +114,175 @@ describe LogStash::Inputs::Vespa do
112
114
  expect(a_request(:get, "#{base_uri}?#{uri_params}&continuation=AAAAAA")).to have_been_made.once
113
115
  end
114
116
  end
117
+
118
+ context "when using authentication" do
119
+ it "adds Bearer token to request headers when auth_token is provided" do
120
+ config_with_token = config.merge({"auth_token" => "test-token"})
121
+ plugin = described_class.new(config_with_token)
122
+ plugin.register
123
+
124
+ stub_request(:get, "#{base_uri}?#{uri_params}")
125
+ .with(headers: { 'Authorization' => 'Bearer test-token' })
126
+ .to_return(status: 200, body: '{"documents": [], "documentCount": 0}')
127
+
128
+ plugin.run(queue)
129
+ expect(a_request(:get, "#{base_uri}?#{uri_params}")
130
+ .with(headers: { 'Authorization' => 'Bearer test-token' }))
131
+ .to have_been_made.once
132
+ end
133
+ end
134
+ end
135
+
136
+ describe "#register" do
137
+ let(:temp_cert) do
138
+ file = Tempfile.new(['cert', '.pem'])
139
+ # Create a self-signed certificate for testing
140
+ key = OpenSSL::PKey::RSA.new(2048)
141
+ cert = OpenSSL::X509::Certificate.new
142
+ cert.version = 2
143
+ cert.serial = 1
144
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Test")
145
+ cert.issuer = cert.subject
146
+ cert.public_key = key.public_key
147
+ cert.not_before = Time.now
148
+ cert.not_after = Time.now + 3600
149
+
150
+ # Sign the certificate
151
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
152
+
153
+ file.write(cert.to_pem)
154
+ file.close
155
+ file
156
+ end
157
+
158
+ let(:temp_key) do
159
+ file = Tempfile.new(['key', '.pem'])
160
+ # Create a valid RSA key for testing
161
+ key = OpenSSL::PKey::RSA.new(2048)
162
+ file.write(key.to_pem)
163
+ file.close
164
+ file
165
+ end
166
+
167
+ after do
168
+ temp_cert.unlink
169
+ temp_key.unlink
170
+ end
171
+
172
+ it "raises error when only client_cert is provided" do
173
+ invalid_config = config.merge({"client_cert" => temp_cert.path})
174
+ plugin = described_class.new(invalid_config)
175
+
176
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError,
177
+ "Both client_cert and client_key must be set, you can't have just one")
178
+ end
179
+
180
+ it "raises error when only client_key is provided" do
181
+ invalid_config = config.merge({"client_key" => temp_key.path})
182
+ plugin = described_class.new(invalid_config)
183
+
184
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError,
185
+ "Both client_cert and client_key must be set, you can't have just one")
186
+ end
187
+
188
+ it "correctly sets up URI parameters" do
189
+ full_config = config.merge({
190
+ "selection" => "true",
191
+ "from_timestamp" => 1234567890,
192
+ "to_timestamp" => 2234567890,
193
+ "page_size" => 50,
194
+
195
+ "backend_concurrency" => 2,
196
+ "timeout" => 120
197
+ })
198
+
199
+ plugin = described_class.new(full_config)
200
+ plugin.register
201
+
202
+ # Access the private @uri_params using send
203
+ uri_params = plugin.send(:instance_variable_get, :@uri_params)
204
+ expect(uri_params[:selection]).to eq("true")
205
+ expect(uri_params[:fromTimestamp]).to eq(1234567890)
206
+ expect(uri_params[:toTimestamp]).to eq(2234567890)
207
+ expect(uri_params[:wantedDocumentCount]).to eq(50)
208
+ expect(uri_params[:concurrency]).to eq(2)
209
+ expect(uri_params[:timeout]).to eq(120)
210
+ end
211
+ end
212
+
213
+ describe "#parse_response" do
214
+ it "handles malformed JSON responses" do
215
+ response = double("response", :body => "invalid json{")
216
+ result = plugin.parse_response(response)
217
+ expect(result).to be_nil
218
+ end
219
+
220
+ it "successfully parses valid JSON responses" do
221
+ valid_json = {
222
+ "documents" => [{"id" => "doc1"}],
223
+ "documentCount" => 1
224
+ }.to_json
225
+ response = double("response", :body => valid_json)
226
+
227
+ result = plugin.parse_response(response)
228
+ expect(result["documentCount"]).to eq(1)
229
+ expect(result["documents"]).to be_an(Array)
230
+ end
231
+ end
232
+
233
+ describe "#process_documents" do
234
+ it "creates events with correct decoration" do
235
+ documents = [
236
+ {"id" => "doc1", "fields" => {"field1" => "value1"}},
237
+ {"id" => "doc2", "fields" => {"field1" => "value2"}}
238
+ ]
239
+
240
+ # Test that decoration is applied
241
+ expect(plugin).to receive(:decorate).twice
242
+
243
+ plugin.process_documents(documents, queue)
244
+ expect(queue.size).to eq(2)
245
+
246
+ event1 = queue.pop
247
+ expect(event1.get("id")).to eq("doc1")
248
+ expect(event1.get("fields")["field1"]).to eq("value1")
249
+
250
+ event2 = queue.pop
251
+ expect(event2.get("id")).to eq("doc2")
252
+ expect(event2.get("fields")["field1"]).to eq("value2")
253
+ end
254
+ end
255
+
256
+ describe "#stop" do
257
+ it "sets stopping flag" do
258
+ plugin.stop
259
+ expect(plugin.instance_variable_get(:@stopping)).to be true
260
+ end
261
+
262
+ it "interrupts running visit operation" do
263
+ request_made = Queue.new # Use a Queue for thread synchronization
264
+
265
+ # Setup a response that would normally continue
266
+ stub_request(:get, "#{base_uri}?#{uri_params}")
267
+ .to_return(status: 200, body: {
268
+ documents: [{"id" => "doc1"}],
269
+ documentCount: 1,
270
+ continuation: "token"
271
+ }.to_json)
272
+ .with { |req| request_made.push(true); true } # Signal when request is made
273
+
274
+ # Run in a separate thread
275
+ thread = Thread.new { plugin.run(queue) }
276
+
277
+ # Wait for the first request to be made
278
+ request_made.pop
279
+
280
+ # Now we know the first request has been made, stop the plugin
281
+ plugin.stop
282
+ thread.join
283
+
284
+ # Should only make one request despite having a continuation token
285
+ expect(a_request(:get, "#{base_uri}?#{uri_params}")).to have_been_made.once
286
+ end
115
287
  end
116
288
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-vespa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Radu Gheorghe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-19 00:00:00.000000000 Z
11
+ date: 2025-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement