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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37124c3a166313a2fb9f3831273def114770178158439a44ebfa1bc1d32f9d0f
4
- data.tar.gz: a09fd3ce2c54908fedc14dc2d780bb3ec48d7091d05e2e8fcb5789e4ec1e30b9
3
+ metadata.gz: b42642e174e8a6f0bf30c67cbdf25e27cc8197f2aeb56cb2268fb11f65426a03
4
+ data.tar.gz: 26ca32e908ef3d42ec4281259ea9a6bf31746031b12c14ad08fbfd189c069c3b
5
5
  SHA512:
6
- metadata.gz: caac996badd1bbdeb231fad3f40f96a50386baf78ee356587d5fc5d2b4a095f1073bee417a99aab081c13cb1a18802785abed618dc85db504f56145c620c46b6
7
- data.tar.gz: 697a89b810998154a44338e8e73f02351b72afa00fc35e1fc115a22ce5ecfeddd27d29bd8fdf4fb779a3b17fbaa2f4f361a2c30b68c0b5ce0cd49a6edeba1a1d
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
- BUILD_FLAVOUR_SERVERLESS = 'serverless'.freeze
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
- @build_flavour = Concurrent::AtomicReference.new
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
- response = perform_request_to_url(url, :head, @healthcheck_path)
236
- raise BadResponseCodeError.new(response.code, url, nil, response.body) unless (200..299).cover?(response.code)
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
- logger.debug("Running health check to see if an Elasticsearch connection is working",
244
- :healthcheck_url => url.sanitized.to_s, :path => @healthcheck_path)
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
- if !elasticsearch?(url)
250
- raise LogStash::ConfigurationError, "Could not connect to a compatible version of Elasticsearch"
251
- end
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
- # We reconnected to this node, check its ES version
256
- version_info = get_es_version(url)
257
- es_version = version_info.fetch('number', nil)
258
- build_flavour = version_info.fetch('build_flavor', nil)
259
-
260
- if es_version.nil?
261
- logger.warn("Failed to retrieve Elasticsearch version data from connected endpoint, connection aborted", :url => url.sanitized.to_s)
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
- set_build_flavour(build_flavour)
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 elasticsearch?(url)
309
+ def get_root_path(url, params={})
279
310
  begin
280
- response = perform_request_to_url(url, :get, ROOT_URI_PATH)
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
- return false if response.code == 401 || response.code == 403
283
- raise e
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 valid_tagline?(version_info)
310
- tagline = version_info['tagline']
311
- tagline == "You Know, for Search"
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
- @build_flavour.get == BUILD_FLAVOUR_SERVERLESS
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 set_build_flavour(flavour)
530
- @build_flavour.set(flavour)
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;
@@ -209,7 +209,7 @@ module LogStash; module Outputs; class ElasticSearch;
209
209
  end
210
210
 
211
211
  def get(path)
212
- response = @pool.get(path, nil)
212
+ response = @pool.get(path)
213
213
  LogStash::Json.load(response.body)
214
214
  end
215
215
 
@@ -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
- @logger.error("Failed to install template", message: e.message, exception: e.class, backtrace: e.backtrace)
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
- @logger.error("Unable to retrieve Elasticsearch cluster uuid", message: e.message, exception: e.class, backtrace: e.backtrace)
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.17.0'
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 "the resurrectionist" do
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
- it "should attempt to resurrect connections after the ressurrect delay" do
47
- expect(subject).to receive(:healthcheck!).once
48
- sleep(subject.resurrect_delay + 1)
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 url handling" do
59
+ describe "healthcheck path handling" do
52
60
  let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://localhost:9200")] }
53
- let(:success_response) { double("Response", :code => 200) }
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
- expect(adapter).to receive(:perform_request).with(anything, :head, "/", anything, anything) do |url, _, _, _, _|
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
- expect(adapter).to receive(:perform_request).with(anything, :head, eq(healthcheck_path), anything, anything) do |url, _, _, _, _|
77
- expect(url.path).to be_empty
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
- success_response
80
- end
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(:valid_response) { MockResponse.new(200, {"tagline" => "You Know, for Search",
255
- "version" => {
256
- "number" => '7.13.0',
257
- "build_flavor" => 'default'}
258
- }) }
259
-
260
- before(:each) do
261
- allow(subject).to receive(:perform_request_to_url).and_return(valid_response)
262
- subject.start
263
- end
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
- let(:es_version_info) { [ { "number" => '0.0.0', "build_flavor" => 'default'}, { "number" => '6.0.0', "build_flavor" => 'default'} ] }
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 flavour tracking" do
392
+ describe "build flavor tracking" do
279
393
  let(:initial_urls) { [::LogStash::Util::SafeURI.new("http://somehost:9200")] }
280
394
 
281
- let(:es_version_info) { [ { "number" => '8.9.0', "build_flavor" => "serverless" } ] }
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::BUILD_FLAVOUR_SERVERLESS} },
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(valid_response)
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 flavour" do
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
- allow(adapter).to receive(:perform_request)
418
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
427
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
436
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
443
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
450
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
459
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
466
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
473
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
482
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
489
- .with(anything, :get, "/", anything, anything)
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
- allow(adapter).to receive(:perform_request)
496
- .with(anything, :get, "/", anything, anything)
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, nil).and_return(get_response)
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.17.0
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-14 00:00:00.000000000 Z
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