logstash-output-elasticsearch 11.17.0-java → 11.18.0-java

Sign up to get free protection for your applications and to get access to all the features.
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