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 +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +21 -1
- data/lib/logstash/inputs/vespa.rb +14 -3
- data/logstash-input-vespa.gemspec +1 -1
- data/spec/inputs/vespa_spec.rb +172 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc362062aaf492eb77f990be4f6844b34051ef51c7a47cae6b268e48f0c3012f
|
4
|
+
data.tar.gz: f16fd1a3621feb7efdd4b65f039b94cb40a57434bc2e286e2310d794b00d2eb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23436b3c39d0259b144a1d82fd7fc970b824b74fd9e1bd90d320fbde5501dae3b42660708acef1e2d16fbec6f955dc945d5dab941dbd15b92eb2cf779ffe5851
|
7
|
+
data.tar.gz: c8d280292ad101c31f8795e7aab8ee3f74c5044e76d688bb354637ad1f6c9aeac5e3cc9e3f3c6472a78da7b02dcabf4c0b104b695961448519b382c38044937a
|
data/CHANGELOG.md
CHANGED
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
|
-
|
164
|
-
|
165
|
-
|
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
|
+
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"
|
data/spec/inputs/vespa_spec.rb
CHANGED
@@ -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.
|
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:
|
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
|