logstash-output-elasticsearch 11.17.0-java → 11.18.0-java
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/lib/logstash/outputs/elasticsearch/http_client/pool.rb +106 -61
- data/lib/logstash/outputs/elasticsearch/http_client.rb +1 -1
- data/lib/logstash/outputs/elasticsearch.rb +3 -1
- data/lib/logstash/plugin_mixins/elasticsearch/common.rb +3 -1
- data/logstash-output-elasticsearch.gemspec +1 -1
- data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +212 -111
- data/spec/unit/outputs/elasticsearch/http_client_spec.rb +1 -1
- 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: b42642e174e8a6f0bf30c67cbdf25e27cc8197f2aeb56cb2268fb11f65426a03
|
4
|
+
data.tar.gz: 26ca32e908ef3d42ec4281259ea9a6bf31746031b12c14ad08fbfd189c069c3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd0b5731beb34c4e331a8ea52252c4b53af5d8c62ffb97c106eab961709d0d884a02a228d48a236eee635629d23a6c2243528a20e3f1f7368f609e232f26d7f7
|
7
|
+
data.tar.gz: 79e980b61c3bdd1b3339f1f03eccff4c52fe596fc34e6069fac1239a5c67e17357d118a95d9536bc1937f930f1c60f935858c236447f5b87f8517ec9b79ef53e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
## 11.18.0
|
2
|
+
- Added request header `Elastic-Api-Version` for serverless [#1147](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/1147)
|
3
|
+
|
1
4
|
## 11.17.0
|
2
5
|
- Added support to http compression level. Deprecated `http_compression` in favour of `compression_level` and enabled compression level 1 by default. [#1148](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/1148)
|
3
6
|
|
@@ -16,6 +16,18 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
16
16
|
@response_body = response_body
|
17
17
|
end
|
18
18
|
|
19
|
+
def invalid_eav_header?
|
20
|
+
@response_code == 400 && @response_body&.include?(ELASTIC_API_VERSION)
|
21
|
+
end
|
22
|
+
|
23
|
+
def invalid_credentials?
|
24
|
+
@response_code == 401
|
25
|
+
end
|
26
|
+
|
27
|
+
def forbidden?
|
28
|
+
@response_code == 403
|
29
|
+
end
|
30
|
+
|
19
31
|
end
|
20
32
|
class HostUnreachableError < Error;
|
21
33
|
attr_reader :original_error, :url
|
@@ -48,7 +60,9 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
48
60
|
:sniffer_delay => 10,
|
49
61
|
}.freeze
|
50
62
|
|
51
|
-
|
63
|
+
BUILD_FLAVOR_SERVERLESS = 'serverless'.freeze
|
64
|
+
ELASTIC_API_VERSION = "Elastic-Api-Version".freeze
|
65
|
+
DEFAULT_EAV_HEADER = { ELASTIC_API_VERSION => "2023-10-31" }.freeze
|
52
66
|
|
53
67
|
def initialize(logger, adapter, initial_urls=[], options={})
|
54
68
|
@logger = logger
|
@@ -77,7 +91,7 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
77
91
|
@license_checker = options[:license_checker] || LogStash::PluginMixins::ElasticSearch::NoopLicenseChecker::INSTANCE
|
78
92
|
|
79
93
|
@last_es_version = Concurrent::AtomicReference.new
|
80
|
-
@
|
94
|
+
@build_flavor = Concurrent::AtomicReference.new
|
81
95
|
end
|
82
96
|
|
83
97
|
def start
|
@@ -232,39 +246,56 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
232
246
|
end
|
233
247
|
|
234
248
|
def health_check_request(url)
|
235
|
-
|
236
|
-
|
249
|
+
logger.debug("Running health check to see if an Elasticsearch connection is working",
|
250
|
+
:healthcheck_url => url.sanitized.to_s, :path => @healthcheck_path)
|
251
|
+
begin
|
252
|
+
response = perform_request_to_url(url, :head, @healthcheck_path)
|
253
|
+
return response, nil
|
254
|
+
rescue ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError => e
|
255
|
+
logger.warn("Health check failed", code: e.response_code, url: e.url, message: e.message)
|
256
|
+
return nil, e
|
257
|
+
end
|
237
258
|
end
|
238
259
|
|
239
260
|
def healthcheck!(register_phase = true)
|
240
261
|
# Try to keep locking granularity low such that we don't affect IO...
|
241
262
|
@state_mutex.synchronize { @url_info.select {|url,meta| meta[:state] != :alive } }.each do |url,meta|
|
242
263
|
begin
|
243
|
-
|
244
|
-
|
245
|
-
health_check_request(url)
|
264
|
+
_, health_bad_code_err = health_check_request(url)
|
265
|
+
root_response, root_bad_code_err = get_root_path(url) if health_bad_code_err.nil? || register_phase
|
246
266
|
|
247
267
|
# when called from resurrectionist skip the product check done during register phase
|
248
268
|
if register_phase
|
249
|
-
|
250
|
-
|
251
|
-
|
269
|
+
raise LogStash::ConfigurationError,
|
270
|
+
"Could not read Elasticsearch. Please check the credentials" if root_bad_code_err&.invalid_credentials?
|
271
|
+
raise LogStash::ConfigurationError,
|
272
|
+
"Could not read Elasticsearch. Please check the privileges" if root_bad_code_err&.forbidden?
|
273
|
+
# when customer_headers is invalid
|
274
|
+
raise LogStash::ConfigurationError,
|
275
|
+
"The Elastic-Api-Version header is not valid" if root_bad_code_err&.invalid_eav_header?
|
276
|
+
# when it is not Elasticserach
|
277
|
+
raise LogStash::ConfigurationError,
|
278
|
+
"Could not connect to a compatible version of Elasticsearch" if root_bad_code_err.nil? && !elasticsearch?(root_response)
|
279
|
+
|
280
|
+
test_serverless_connection(url, root_response)
|
252
281
|
end
|
282
|
+
|
283
|
+
raise health_bad_code_err if health_bad_code_err
|
284
|
+
raise root_bad_code_err if root_bad_code_err
|
285
|
+
|
253
286
|
# If no exception was raised it must have succeeded!
|
254
287
|
logger.warn("Restored connection to ES instance", url: url.sanitized.to_s)
|
255
|
-
|
256
|
-
|
257
|
-
es_version =
|
258
|
-
|
259
|
-
|
260
|
-
if es_version.nil?
|
261
|
-
|
262
|
-
next
|
263
|
-
end
|
288
|
+
|
289
|
+
# We check its ES version
|
290
|
+
es_version, build_flavor = parse_es_version(root_response)
|
291
|
+
logger.warn("Failed to retrieve Elasticsearch build flavor") if build_flavor.nil?
|
292
|
+
logger.warn("Failed to retrieve Elasticsearch version data from connected endpoint, connection aborted", :url => url.sanitized.to_s) if es_version.nil?
|
293
|
+
next if es_version.nil?
|
294
|
+
|
264
295
|
@state_mutex.synchronize do
|
265
296
|
meta[:version] = es_version
|
266
297
|
set_last_es_version(es_version, url)
|
267
|
-
|
298
|
+
set_build_flavor(build_flavor)
|
268
299
|
|
269
300
|
alive = @license_checker.appropriate_license?(self, url)
|
270
301
|
meta[:state] = alive ? :alive : :dead
|
@@ -275,40 +306,21 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
275
306
|
end
|
276
307
|
end
|
277
308
|
|
278
|
-
def
|
309
|
+
def get_root_path(url, params={})
|
279
310
|
begin
|
280
|
-
|
311
|
+
resp = perform_request_to_url(url, :get, ROOT_URI_PATH, params)
|
312
|
+
return resp, nil
|
281
313
|
rescue ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError => e
|
282
|
-
|
283
|
-
|
314
|
+
logger.warn("Elasticsearch main endpoint returns #{e.response_code}", message: e.message, body: e.response_body)
|
315
|
+
return nil, e
|
284
316
|
end
|
285
|
-
|
286
|
-
version_info = LogStash::Json.load(response.body)
|
287
|
-
return false if version_info['version'].nil?
|
288
|
-
|
289
|
-
version = ::Gem::Version.new(version_info["version"]['number'])
|
290
|
-
return false if version < ::Gem::Version.new('6.0.0')
|
291
|
-
|
292
|
-
if VERSION_6_TO_7.satisfied_by?(version)
|
293
|
-
return valid_tagline?(version_info)
|
294
|
-
elsif VERSION_7_TO_7_14.satisfied_by?(version)
|
295
|
-
build_flavor = version_info["version"]['build_flavor']
|
296
|
-
return false if build_flavor.nil? || build_flavor != 'default' || !valid_tagline?(version_info)
|
297
|
-
else
|
298
|
-
# case >= 7.14
|
299
|
-
lower_headers = response.headers.transform_keys {|key| key.to_s.downcase }
|
300
|
-
product_header = lower_headers['x-elastic-product']
|
301
|
-
return false if product_header != 'Elasticsearch'
|
302
|
-
end
|
303
|
-
return true
|
304
|
-
rescue => e
|
305
|
-
logger.error("Unable to retrieve Elasticsearch version", url: url.sanitized.to_s, exception: e.class, message: e.message)
|
306
|
-
false
|
307
317
|
end
|
308
318
|
|
309
|
-
def
|
310
|
-
|
311
|
-
|
319
|
+
def test_serverless_connection(url, root_response)
|
320
|
+
_, build_flavor = parse_es_version(root_response)
|
321
|
+
params = { :headers => DEFAULT_EAV_HEADER }
|
322
|
+
_, bad_code_err = get_root_path(url, params) if build_flavor == BUILD_FLAVOR_SERVERLESS
|
323
|
+
raise LogStash::ConfigurationError, "The Elastic-Api-Version header is not valid" if bad_code_err&.invalid_eav_header?
|
312
324
|
end
|
313
325
|
|
314
326
|
def stop_resurrectionist
|
@@ -334,6 +346,7 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
334
346
|
end
|
335
347
|
|
336
348
|
def perform_request_to_url(url, method, path, params={}, body=nil)
|
349
|
+
params[:headers] = DEFAULT_EAV_HEADER.merge(params[:headers] || {}) if serverless?
|
337
350
|
@adapter.perform_request(url, method, path, params, body)
|
338
351
|
end
|
339
352
|
|
@@ -476,15 +489,6 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
476
489
|
end
|
477
490
|
end
|
478
491
|
|
479
|
-
def get_es_version(url)
|
480
|
-
response = perform_request_to_url(url, :get, ROOT_URI_PATH)
|
481
|
-
return nil unless (200..299).cover?(response.code)
|
482
|
-
|
483
|
-
response = LogStash::Json.load(response.body)
|
484
|
-
|
485
|
-
response.fetch('version', {})
|
486
|
-
end
|
487
|
-
|
488
492
|
def last_es_version
|
489
493
|
@last_es_version.get
|
490
494
|
end
|
@@ -494,7 +498,7 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
494
498
|
end
|
495
499
|
|
496
500
|
def serverless?
|
497
|
-
@
|
501
|
+
@build_flavor.get == BUILD_FLAVOR_SERVERLESS
|
498
502
|
end
|
499
503
|
|
500
504
|
private
|
@@ -526,9 +530,50 @@ module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
|
526
530
|
previous_major: @maximum_seen_major_version, new_major: major, node_url: url.sanitized.to_s)
|
527
531
|
end
|
528
532
|
|
529
|
-
def
|
530
|
-
@
|
533
|
+
def set_build_flavor(flavor)
|
534
|
+
@build_flavor.set(flavor)
|
535
|
+
end
|
536
|
+
|
537
|
+
def parse_es_version(response)
|
538
|
+
return nil, nil unless (200..299).cover?(response&.code)
|
539
|
+
|
540
|
+
response = LogStash::Json.load(response&.body)
|
541
|
+
version_info = response.fetch('version', {})
|
542
|
+
es_version = version_info.fetch('number', nil)
|
543
|
+
build_flavor = version_info.fetch('build_flavor', nil)
|
544
|
+
|
545
|
+
return es_version, build_flavor
|
546
|
+
end
|
547
|
+
|
548
|
+
def elasticsearch?(response)
|
549
|
+
return false if response.nil?
|
550
|
+
|
551
|
+
version_info = LogStash::Json.load(response.body)
|
552
|
+
return false if version_info['version'].nil?
|
553
|
+
|
554
|
+
version = ::Gem::Version.new(version_info["version"]['number'])
|
555
|
+
return false if version < ::Gem::Version.new('6.0.0')
|
556
|
+
|
557
|
+
if VERSION_6_TO_7.satisfied_by?(version)
|
558
|
+
return valid_tagline?(version_info)
|
559
|
+
elsif VERSION_7_TO_7_14.satisfied_by?(version)
|
560
|
+
build_flavor = version_info["version"]['build_flavor']
|
561
|
+
return false if build_flavor.nil? || build_flavor != 'default' || !valid_tagline?(version_info)
|
562
|
+
else
|
563
|
+
# case >= 7.14
|
564
|
+
lower_headers = response.headers.transform_keys {|key| key.to_s.downcase }
|
565
|
+
product_header = lower_headers['x-elastic-product']
|
566
|
+
return false if product_header != 'Elasticsearch'
|
567
|
+
end
|
568
|
+
return true
|
569
|
+
rescue => e
|
570
|
+
logger.error("Unable to retrieve Elasticsearch version", exception: e.class, message: e.message)
|
571
|
+
false
|
531
572
|
end
|
532
573
|
|
574
|
+
def valid_tagline?(version_info)
|
575
|
+
tagline = version_info['tagline']
|
576
|
+
tagline == "You Know, for Search"
|
577
|
+
end
|
533
578
|
end
|
534
579
|
end; end; end; end;
|
@@ -596,7 +596,9 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
|
|
596
596
|
def install_template
|
597
597
|
TemplateManager.install_template(self)
|
598
598
|
rescue => e
|
599
|
-
|
599
|
+
details = { message: e.message, exception: e.class, backtrace: e.backtrace }
|
600
|
+
details[:body] = e.response_body if e.respond_to?(:response_body)
|
601
|
+
@logger.error("Failed to install template", details)
|
600
602
|
end
|
601
603
|
|
602
604
|
def setup_ecs_compatibility_related_defaults
|
@@ -179,7 +179,9 @@ module LogStash; module PluginMixins; module ElasticSearch
|
|
179
179
|
cluster_info = client.get('/')
|
180
180
|
plugin_metadata.set(:cluster_uuid, cluster_info['cluster_uuid'])
|
181
181
|
rescue => e
|
182
|
-
|
182
|
+
details = { message: e.message, exception: e.class, backtrace: e.backtrace }
|
183
|
+
details[:body] = e.response_body if e.respond_to?(:response_body)
|
184
|
+
@logger.error("Unable to retrieve Elasticsearch cluster uuid", details)
|
183
185
|
end
|
184
186
|
|
185
187
|
def retrying_submit(actions)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-output-elasticsearch'
|
3
|
-
s.version = '11.
|
3
|
+
s.version = '11.18.0'
|
4
4
|
s.licenses = ['apache-2.0']
|
5
5
|
s.summary = "Stores logs in Elasticsearch"
|
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"
|
@@ -7,8 +7,14 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
7
7
|
let(:adapter) { LogStash::Outputs::ElasticSearch::HttpClient::ManticoreAdapter.new(logger, {}) }
|
8
8
|
let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://localhost:9200")] }
|
9
9
|
let(:options) { {:resurrect_delay => 3, :url_normalizer => proc {|u| u}} } # Shorten the delay a bit to speed up tests
|
10
|
-
let(:es_version_info) { [ { "number" => '0.0.0', "build_flavor" => 'default'} ] }
|
11
10
|
let(:license_status) { 'active' }
|
11
|
+
let(:root_response) { MockResponse.new(200,
|
12
|
+
{"tagline" => "You Know, for Search",
|
13
|
+
"version" => {
|
14
|
+
"number" => '8.9.0',
|
15
|
+
"build_flavor" => 'default'} },
|
16
|
+
{ "X-Elastic-Product" => "Elasticsearch" }
|
17
|
+
) }
|
12
18
|
|
13
19
|
subject { described_class.new(logger, adapter, initial_urls, options) }
|
14
20
|
|
@@ -22,7 +28,6 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
22
28
|
|
23
29
|
allow(::Manticore::Client).to receive(:new).and_return(manticore_double)
|
24
30
|
|
25
|
-
allow(subject).to receive(:get_es_version).with(any_args).and_return(*es_version_info)
|
26
31
|
allow(subject.license_checker).to receive(:license_status).and_return(license_status)
|
27
32
|
end
|
28
33
|
|
@@ -37,35 +42,42 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
37
42
|
end
|
38
43
|
end
|
39
44
|
|
40
|
-
describe "
|
41
|
-
before(:each) { subject.start }
|
42
|
-
it "should start the resurrectionist when created" do
|
43
|
-
expect(subject.resurrectionist_alive?).to eql(true)
|
44
|
-
end
|
45
|
+
describe "healthcheck" do
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
47
|
+
describe "the resurrectionist" do
|
48
|
+
before(:each) { subject.start }
|
49
|
+
it "should start the resurrectionist when created" do
|
50
|
+
expect(subject.resurrectionist_alive?).to eql(true)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should attempt to resurrect connections after the ressurrect delay" do
|
54
|
+
expect(subject).to receive(:healthcheck!).once
|
55
|
+
sleep(subject.resurrect_delay + 1)
|
56
|
+
end
|
49
57
|
end
|
50
58
|
|
51
|
-
describe "healthcheck
|
59
|
+
describe "healthcheck path handling" do
|
52
60
|
let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://localhost:9200")] }
|
53
|
-
let(:
|
61
|
+
let(:healthcheck_response) { double("Response", :code => 200) }
|
54
62
|
|
55
63
|
before(:example) do
|
64
|
+
subject.start
|
65
|
+
|
66
|
+
expect(adapter).to receive(:perform_request).with(anything, :head, eq(healthcheck_path), anything, anything) do |url, _, _, _, _|
|
67
|
+
expect(url.path).to be_empty
|
68
|
+
healthcheck_response
|
69
|
+
end
|
70
|
+
|
56
71
|
expect(adapter).to receive(:perform_request).with(anything, :get, "/", anything, anything) do |url, _, _, _, _|
|
57
72
|
expect(url.path).to be_empty
|
73
|
+
root_response
|
58
74
|
end
|
59
75
|
end
|
60
76
|
|
61
77
|
context "and not setting healthcheck_path" do
|
78
|
+
let(:healthcheck_path) { "/" }
|
62
79
|
it "performs the healthcheck to the root" do
|
63
|
-
|
64
|
-
expect(url.path).to be_empty
|
65
|
-
|
66
|
-
success_response
|
67
|
-
end
|
68
|
-
expect { subject.healthcheck! }.to raise_error(LogStash::ConfigurationError, "Could not connect to a compatible version of Elasticsearch")
|
80
|
+
subject.healthcheck!
|
69
81
|
end
|
70
82
|
end
|
71
83
|
|
@@ -73,14 +85,116 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
73
85
|
let(:healthcheck_path) { "/my/health" }
|
74
86
|
let(:options) { super().merge(:healthcheck_path => healthcheck_path) }
|
75
87
|
it "performs the healthcheck to the healthcheck_path" do
|
76
|
-
|
77
|
-
|
88
|
+
subject.healthcheck!
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "register phase" do
|
94
|
+
shared_examples_for "root path returns bad code error" do |err_msg|
|
95
|
+
before :each do
|
96
|
+
subject.update_initial_urls
|
97
|
+
expect(subject).to receive(:elasticsearch?).never
|
98
|
+
end
|
99
|
+
|
100
|
+
it "raises ConfigurationError" do
|
101
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", nil])
|
102
|
+
expect(subject).to receive(:get_root_path).with(anything).and_return([nil,
|
103
|
+
::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(mock_resp.code, nil, nil, mock_resp.body)])
|
104
|
+
expect { subject.healthcheck! }.to raise_error(LogStash::ConfigurationError, err_msg)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "with 200 without version" do
|
109
|
+
let(:mock_resp) { MockResponse.new(200, {"tagline" => "You Know, for Search"}) }
|
78
110
|
|
79
|
-
|
80
|
-
|
111
|
+
it "raises ConfigurationError" do
|
112
|
+
subject.update_initial_urls
|
113
|
+
|
114
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", nil])
|
115
|
+
expect(subject).to receive(:get_root_path).with(anything).and_return([mock_resp, nil])
|
81
116
|
expect { subject.healthcheck! }.to raise_error(LogStash::ConfigurationError, "Could not connect to a compatible version of Elasticsearch")
|
82
117
|
end
|
83
118
|
end
|
119
|
+
|
120
|
+
context "with 200 serverless" do
|
121
|
+
let(:good_resp) { MockResponse.new(200,
|
122
|
+
{ "tagline" => "You Know, for Search",
|
123
|
+
"version" => { "number" => '8.10.0', "build_flavor" => 'serverless'}
|
124
|
+
},
|
125
|
+
{ "X-Elastic-Product" => "Elasticsearch" }
|
126
|
+
) }
|
127
|
+
let(:bad_400_err) do
|
128
|
+
::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(400,
|
129
|
+
nil, nil,
|
130
|
+
"The requested [Elastic-Api-Version] header value of [2024-10-31] is not valid. Only [2023-10-31] is supported")
|
131
|
+
end
|
132
|
+
|
133
|
+
it "raises ConfigurationError when the serverless connection test fails" do
|
134
|
+
subject.update_initial_urls
|
135
|
+
|
136
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", nil])
|
137
|
+
expect(subject).to receive(:get_root_path).with(anything).and_return([good_resp, nil])
|
138
|
+
expect(subject).to receive(:get_root_path).with(anything, hash_including(:headers => LogStash::Outputs::ElasticSearch::HttpClient::Pool::DEFAULT_EAV_HEADER)).and_return([nil, bad_400_err])
|
139
|
+
expect { subject.healthcheck! }.to raise_error(LogStash::ConfigurationError, "The Elastic-Api-Version header is not valid")
|
140
|
+
end
|
141
|
+
|
142
|
+
it "passes when the serverless connection test succeeds" do
|
143
|
+
subject.update_initial_urls
|
144
|
+
|
145
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", nil])
|
146
|
+
expect(subject).to receive(:get_root_path).with(anything).and_return([good_resp, nil])
|
147
|
+
expect(subject).to receive(:get_root_path).with(anything, hash_including(:headers => LogStash::Outputs::ElasticSearch::HttpClient::Pool::DEFAULT_EAV_HEADER)).and_return([good_resp, nil])
|
148
|
+
expect { subject.healthcheck! }.not_to raise_error
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context "with 200 default" do
|
153
|
+
let(:good_resp) { MockResponse.new(200,
|
154
|
+
{ "tagline" => "You Know, for Search",
|
155
|
+
"version" => { "number" => '8.10.0', "build_flavor" => 'default'}
|
156
|
+
},
|
157
|
+
{ "X-Elastic-Product" => "Elasticsearch" }
|
158
|
+
) }
|
159
|
+
|
160
|
+
it "passes without checking serverless connection" do
|
161
|
+
subject.update_initial_urls
|
162
|
+
|
163
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", nil])
|
164
|
+
expect(subject).to receive(:get_root_path).with(anything).and_return([good_resp, nil])
|
165
|
+
expect(subject).not_to receive(:get_root_path).with(anything, hash_including(:headers => LogStash::Outputs::ElasticSearch::HttpClient::Pool::DEFAULT_EAV_HEADER))
|
166
|
+
expect { subject.healthcheck! }.not_to raise_error
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "with 400" do
|
171
|
+
let(:mock_resp) { MockResponse.new(400, "The requested [Elastic-Api-Version] header value of [2024-10-31] is not valid. Only [2023-10-31] is supported") }
|
172
|
+
it_behaves_like "root path returns bad code error", "The Elastic-Api-Version header is not valid"
|
173
|
+
end
|
174
|
+
|
175
|
+
context "with 401" do
|
176
|
+
let(:mock_resp) { MockResponse.new(401, "missing authentication") }
|
177
|
+
it_behaves_like "root path returns bad code error", "Could not read Elasticsearch. Please check the credentials"
|
178
|
+
end
|
179
|
+
|
180
|
+
context "with 403" do
|
181
|
+
let(:mock_resp) { MockResponse.new(403, "Forbidden") }
|
182
|
+
it_behaves_like "root path returns bad code error", "Could not read Elasticsearch. Please check the privileges"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "non register phase" do
|
187
|
+
let(:health_bad_code_err) { ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(400, nil, nil, nil) }
|
188
|
+
|
189
|
+
before :each do
|
190
|
+
subject.update_initial_urls
|
191
|
+
end
|
192
|
+
|
193
|
+
it "does not call root path when health check request fails" do
|
194
|
+
expect(subject).to receive(:health_check_request).with(anything).and_return(["", health_bad_code_err])
|
195
|
+
expect(subject).to receive(:get_root_path).never
|
196
|
+
subject.healthcheck!(false)
|
197
|
+
end
|
84
198
|
end
|
85
199
|
end
|
86
200
|
|
@@ -251,23 +365,23 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
251
365
|
::LogStash::Util::SafeURI.new("http://otherhost:9201")
|
252
366
|
] }
|
253
367
|
|
254
|
-
let(:
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
it "picks the largest major version" do
|
266
|
-
expect(subject.maximum_seen_major_version).to eq(0)
|
267
|
-
end
|
368
|
+
let(:root_response) { MockResponse.new(200, {"tagline" => "You Know, for Search",
|
369
|
+
"version" => {
|
370
|
+
"number" => '0.0.0',
|
371
|
+
"build_flavor" => 'default'}
|
372
|
+
}) }
|
373
|
+
let(:root_response2) { MockResponse.new(200, {"tagline" => "You Know, for Search",
|
374
|
+
"version" => {
|
375
|
+
"number" => '6.0.0',
|
376
|
+
"build_flavor" => 'default'}
|
377
|
+
}) }
|
268
378
|
|
269
379
|
context "if there are nodes with multiple major versions" do
|
270
|
-
|
380
|
+
before(:each) do
|
381
|
+
allow(subject).to receive(:perform_request_to_url).and_return(root_response, root_response2)
|
382
|
+
subject.start
|
383
|
+
end
|
384
|
+
|
271
385
|
it "picks the largest major version" do
|
272
386
|
expect(subject.maximum_seen_major_version).to eq(6)
|
273
387
|
end
|
@@ -275,32 +389,31 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
275
389
|
end
|
276
390
|
|
277
391
|
|
278
|
-
describe "build
|
392
|
+
describe "build flavor tracking" do
|
279
393
|
let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://somehost:9200")] }
|
280
394
|
|
281
|
-
let(:
|
282
|
-
|
283
|
-
let(:valid_response) { MockResponse.new(200,
|
395
|
+
let(:root_response) { MockResponse.new(200,
|
284
396
|
{"tagline" => "You Know, for Search",
|
285
397
|
"version" => {
|
286
398
|
"number" => '8.9.0',
|
287
|
-
"build_flavor" => LogStash::Outputs::ElasticSearch::HttpClient::Pool::
|
399
|
+
"build_flavor" => LogStash::Outputs::ElasticSearch::HttpClient::Pool::BUILD_FLAVOR_SERVERLESS} },
|
288
400
|
{ "X-Elastic-Product" => "Elasticsearch" }
|
289
401
|
) }
|
290
402
|
|
291
403
|
before(:each) do
|
292
|
-
allow(subject).to receive(:perform_request_to_url).and_return(
|
404
|
+
allow(subject).to receive(:perform_request_to_url).and_return(root_response)
|
293
405
|
subject.start
|
294
406
|
end
|
295
407
|
|
296
|
-
it "picks the build
|
408
|
+
it "picks the build flavor" do
|
297
409
|
expect(subject.serverless?).to be_truthy
|
298
410
|
end
|
299
411
|
end
|
300
412
|
|
301
413
|
describe "license checking" do
|
302
414
|
before(:each) do
|
303
|
-
allow(subject).to receive(:health_check_request)
|
415
|
+
allow(subject).to receive(:health_check_request).and_return(["", nil])
|
416
|
+
allow(subject).to receive(:perform_request_to_url).and_return(root_response)
|
304
417
|
allow(subject).to receive(:elasticsearch?).and_return(true)
|
305
418
|
end
|
306
419
|
|
@@ -327,6 +440,36 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
327
440
|
end
|
328
441
|
end
|
329
442
|
|
443
|
+
describe "elastic api version header" do
|
444
|
+
let(:eav) { "Elastic-Api-Version" }
|
445
|
+
|
446
|
+
context "when it is serverless" do
|
447
|
+
before(:each) do
|
448
|
+
expect(subject).to receive(:serverless?).and_return(true)
|
449
|
+
end
|
450
|
+
|
451
|
+
it "add the default header" do
|
452
|
+
expect(adapter).to receive(:perform_request).with(anything, :get, "/", anything, anything) do |_, _, _, params, _|
|
453
|
+
expect(params[:headers]).to eq({ "User-Agent" => "chromium", "Elastic-Api-Version" => "2023-10-31"})
|
454
|
+
end
|
455
|
+
subject.perform_request_to_url(initial_urls, :get, "/", { :headers => { "User-Agent" => "chromium" }} )
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
context "when it is stateful" do
|
460
|
+
before(:each) do
|
461
|
+
expect(subject).to receive(:serverless?).and_return(false)
|
462
|
+
end
|
463
|
+
|
464
|
+
it "add the default header" do
|
465
|
+
expect(adapter).to receive(:perform_request).with(anything, :get, "/", anything, anything) do |_, _, _, params, _|
|
466
|
+
expect(params[:headers]).to be_nil
|
467
|
+
end
|
468
|
+
subject.perform_request_to_url(initial_urls, :get, "/" )
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
330
473
|
# TODO: extract to ElasticSearchOutputLicenseChecker unit spec
|
331
474
|
describe "license checking with ElasticSearchOutputLicenseChecker" do
|
332
475
|
let(:options) do
|
@@ -334,7 +477,8 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
|
|
334
477
|
end
|
335
478
|
|
336
479
|
before(:each) do
|
337
|
-
allow(subject).to receive(:health_check_request)
|
480
|
+
allow(subject).to receive(:health_check_request).and_return(["", nil])
|
481
|
+
allow(subject).to receive(:perform_request_to_url).and_return(root_response)
|
338
482
|
allow(subject).to receive(:elasticsearch?).and_return(true)
|
339
483
|
end
|
340
484
|
|
@@ -388,114 +532,71 @@ describe "#elasticsearch?" do
|
|
388
532
|
let(:adapter) { double("Manticore Adapter") }
|
389
533
|
let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://localhost:9200")] }
|
390
534
|
let(:options) { {:resurrect_delay => 2, :url_normalizer => proc {|u| u}} } # Shorten the delay a bit to speed up tests
|
391
|
-
let(:es_version_info) { [{ "number" => '0.0.0', "build_flavor" => 'default'}] }
|
392
|
-
let(:license_status) { 'active' }
|
393
535
|
|
394
536
|
subject { LogStash::Outputs::ElasticSearch::HttpClient::Pool.new(logger, adapter, initial_urls, options) }
|
395
537
|
|
396
|
-
let(:url) { ::LogStash::Util::SafeURI.new("http://localhost:9200") }
|
397
|
-
|
398
|
-
context "in case HTTP error code" do
|
399
|
-
it "should fail for 401" do
|
400
|
-
allow(adapter).to receive(:perform_request)
|
401
|
-
.with(anything, :get, "/", anything, anything)
|
402
|
-
.and_return(MockResponse.new(401))
|
403
|
-
|
404
|
-
expect(subject.elasticsearch?(url)).to be false
|
405
|
-
end
|
406
|
-
|
407
|
-
it "should fail for 403" do
|
408
|
-
allow(adapter).to receive(:perform_request)
|
409
|
-
.with(anything, :get, "/", anything, anything)
|
410
|
-
.and_return(status: 403)
|
411
|
-
expect(subject.elasticsearch?(url)).to be false
|
412
|
-
end
|
413
|
-
end
|
414
|
-
|
415
538
|
context "when connecting to a cluster which reply without 'version' field" do
|
416
539
|
it "should fail" do
|
417
|
-
|
418
|
-
|
419
|
-
.and_return(body: {"field" => "funky.com"}.to_json)
|
420
|
-
expect(subject.elasticsearch?(url)).to be false
|
540
|
+
resp = MockResponse.new(200, {"field" => "funky.com"} )
|
541
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
421
542
|
end
|
422
543
|
end
|
423
544
|
|
424
545
|
context "when connecting to a cluster with version < 6.0.0" do
|
425
546
|
it "should fail" do
|
426
|
-
|
427
|
-
|
428
|
-
.and_return(200, {"version" => { "number" => "5.0.0"}}.to_json)
|
429
|
-
expect(subject.elasticsearch?(url)).to be false
|
547
|
+
resp = MockResponse.new(200, {"version" => { "number" => "5.0.0" }})
|
548
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
430
549
|
end
|
431
550
|
end
|
432
551
|
|
433
552
|
context "when connecting to a cluster with version in [6.0.0..7.0.0)" do
|
434
553
|
it "must be successful with valid 'tagline'" do
|
435
|
-
|
436
|
-
|
437
|
-
.and_return(MockResponse.new(200, {"version" => {"number" => "6.5.0"}, "tagline" => "You Know, for Search"}))
|
438
|
-
expect(subject.elasticsearch?(url)).to be true
|
554
|
+
resp = MockResponse.new(200, {"version" => {"number" => "6.5.0"}, "tagline" => "You Know, for Search"} )
|
555
|
+
expect(subject.send(:elasticsearch?, resp)).to be true
|
439
556
|
end
|
440
557
|
|
441
558
|
it "should fail if invalid 'tagline'" do
|
442
|
-
|
443
|
-
|
444
|
-
.and_return(MockResponse.new(200, {"version" => {"number" => "6.5.0"}, "tagline" => "You don't know"}))
|
445
|
-
expect(subject.elasticsearch?(url)).to be false
|
559
|
+
resp = MockResponse.new(200, {"version" => {"number" => "6.5.0"}, "tagline" => "You don't know"} )
|
560
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
446
561
|
end
|
447
562
|
|
448
563
|
it "should fail if 'tagline' is not present" do
|
449
|
-
|
450
|
-
|
451
|
-
.and_return(MockResponse.new(200, {"version" => {"number" => "6.5.0"}}))
|
452
|
-
expect(subject.elasticsearch?(url)).to be false
|
564
|
+
resp = MockResponse.new(200, {"version" => {"number" => "6.5.0"}} )
|
565
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
453
566
|
end
|
454
567
|
end
|
455
568
|
|
456
569
|
context "when connecting to a cluster with version in [7.0.0..7.14.0)" do
|
457
570
|
it "must be successful is 'build_flavor' is 'default' and tagline is correct" do
|
458
|
-
|
459
|
-
|
460
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.5.0", "build_flavor": "default"}, "tagline": "You Know, for Search"}))
|
461
|
-
expect(subject.elasticsearch?(url)).to be true
|
571
|
+
resp = MockResponse.new(200, {"version": {"number": "7.5.0", "build_flavor": "default"}, "tagline": "You Know, for Search"} )
|
572
|
+
expect(subject.send(:elasticsearch?, resp)).to be true
|
462
573
|
end
|
463
574
|
|
464
575
|
it "should fail if 'build_flavor' is not 'default' and tagline is correct" do
|
465
|
-
|
466
|
-
|
467
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.5.0", "build_flavor": "oss"}, "tagline": "You Know, for Search"}))
|
468
|
-
expect(subject.elasticsearch?(url)).to be false
|
576
|
+
resp = MockResponse.new(200, {"version": {"number": "7.5.0", "build_flavor": "oss"}, "tagline": "You Know, for Search"} )
|
577
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
469
578
|
end
|
470
579
|
|
471
580
|
it "should fail if 'build_flavor' is not present and tagline is correct" do
|
472
|
-
|
473
|
-
|
474
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.5.0"}, "tagline": "You Know, for Search"}))
|
475
|
-
expect(subject.elasticsearch?(url)).to be false
|
581
|
+
resp = MockResponse.new(200, {"version": {"number": "7.5.0"}, "tagline": "You Know, for Search"} )
|
582
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
476
583
|
end
|
477
584
|
end
|
478
585
|
|
479
586
|
context "when connecting to a cluster with version >= 7.14.0" do
|
480
587
|
it "should fail if 'X-elastic-product' header is not present" do
|
481
|
-
|
482
|
-
|
483
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.14.0"}}))
|
484
|
-
expect(subject.elasticsearch?(url)).to be false
|
588
|
+
resp = MockResponse.new(200, {"version": {"number": "7.14.0"}} )
|
589
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
485
590
|
end
|
486
591
|
|
487
592
|
it "should fail if 'X-elastic-product' header is present but with bad value" do
|
488
|
-
|
489
|
-
|
490
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.14.0"}}, {'X-elastic-product' => 'not good'}))
|
491
|
-
expect(subject.elasticsearch?(url)).to be false
|
593
|
+
resp = MockResponse.new(200, {"version": {"number": "7.14.0"}}, {'X-elastic-product' => 'not good'} )
|
594
|
+
expect(subject.send(:elasticsearch?, resp)).to be false
|
492
595
|
end
|
493
596
|
|
494
597
|
it "must be successful when 'X-elastic-product' header is present with 'Elasticsearch' value" do
|
495
|
-
|
496
|
-
|
497
|
-
.and_return(MockResponse.new(200, {"version": {"number": "7.14.0"}}, {'X-elastic-product' => 'Elasticsearch'}))
|
498
|
-
expect(subject.elasticsearch?(url)).to be true
|
598
|
+
resp = MockResponse.new(200, {"version": {"number": "7.14.0"}}, {'X-elastic-product' => 'Elasticsearch'} )
|
599
|
+
expect(subject.send(:elasticsearch?, resp)).to be true
|
499
600
|
end
|
500
601
|
end
|
501
602
|
end
|
@@ -135,7 +135,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
|
|
135
135
|
}
|
136
136
|
|
137
137
|
it "returns the hash response" do
|
138
|
-
expect(subject.pool).to receive(:get).with(path
|
138
|
+
expect(subject.pool).to receive(:get).with(path).and_return(get_response)
|
139
139
|
expect(subject.get(path)["body"]).to eq(body)
|
140
140
|
end
|
141
141
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-elasticsearch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 11.
|
4
|
+
version: 11.18.0
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-09-
|
11
|
+
date: 2023-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|