logstash-output-elasticsearch 10.8.2-java → 11.0.1-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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/docs/index.asciidoc +134 -23
  4. data/lib/logstash/outputs/elasticsearch.rb +137 -63
  5. data/lib/logstash/outputs/elasticsearch/data_stream_support.rb +233 -0
  6. data/lib/logstash/outputs/elasticsearch/http_client.rb +59 -21
  7. data/lib/logstash/outputs/elasticsearch/http_client/pool.rb +47 -34
  8. data/lib/logstash/outputs/elasticsearch/ilm.rb +11 -12
  9. data/lib/logstash/outputs/elasticsearch/license_checker.rb +19 -22
  10. data/lib/logstash/outputs/elasticsearch/template_manager.rb +3 -5
  11. data/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb +157 -153
  12. data/lib/logstash/plugin_mixins/elasticsearch/common.rb +81 -60
  13. data/logstash-output-elasticsearch.gemspec +2 -2
  14. data/spec/es_spec_helper.rb +3 -6
  15. data/spec/integration/outputs/data_stream_spec.rb +61 -0
  16. data/spec/integration/outputs/ilm_spec.rb +22 -18
  17. data/spec/integration/outputs/ingest_pipeline_spec.rb +4 -2
  18. data/spec/integration/outputs/retry_spec.rb +14 -2
  19. data/spec/integration/outputs/sniffer_spec.rb +0 -1
  20. data/spec/spec_helper.rb +14 -0
  21. data/spec/unit/http_client_builder_spec.rb +9 -9
  22. data/spec/unit/outputs/elasticsearch/data_stream_support_spec.rb +542 -0
  23. data/spec/unit/outputs/elasticsearch/http_client/manticore_adapter_spec.rb +1 -0
  24. data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +27 -13
  25. data/spec/unit/outputs/elasticsearch/http_client_spec.rb +59 -41
  26. data/spec/unit/outputs/elasticsearch/template_manager_spec.rb +1 -3
  27. data/spec/unit/outputs/elasticsearch_proxy_spec.rb +4 -5
  28. data/spec/unit/outputs/elasticsearch_spec.rb +280 -47
  29. data/spec/unit/outputs/elasticsearch_ssl_spec.rb +1 -2
  30. data/spec/unit/outputs/error_whitelist_spec.rb +4 -3
  31. data/spec/unit/outputs/license_check_spec.rb +0 -16
  32. metadata +23 -16
@@ -1,5 +1,6 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
2
  require "logstash/outputs/elasticsearch/http_client"
3
+ require 'cabin'
3
4
 
4
5
  describe LogStash::Outputs::ElasticSearch::HttpClient::ManticoreAdapter do
5
6
  let(:logger) { Cabin::Channel.get }
@@ -1,6 +1,6 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
2
  require "logstash/outputs/elasticsearch/http_client"
3
- require "json"
3
+ require 'cabin'
4
4
 
5
5
  describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
6
6
  let(:logger) { Cabin::Channel.get }
@@ -9,7 +9,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
9
9
  let(:options) { {:resurrect_delay => 2, :url_normalizer => proc {|u| u}} } # Shorten the delay a bit to speed up tests
10
10
  let(:es_node_versions) { [ "0.0.0" ] }
11
11
  let(:oss) { true }
12
- let(:valid_license) { true }
12
+ let(:license_status) { 'active' }
13
13
 
14
14
  subject { described_class.new(logger, adapter, initial_urls, options) }
15
15
 
@@ -26,8 +26,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
26
26
  allow(::Manticore::Client).to receive(:new).and_return(manticore_double)
27
27
 
28
28
  allow(subject).to receive(:get_es_version).with(any_args).and_return(*es_node_versions)
29
- allow(subject.license_checker).to receive(:oss?).and_return(oss)
30
- allow(subject.license_checker).to receive(:valid_es_license?).and_return(valid_license)
29
+ allow(subject.license_checker).to receive(:license_status).and_return(license_status)
31
30
  end
32
31
 
33
32
  after do
@@ -68,7 +67,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
68
67
 
69
68
  context "and setting healthcheck_path" do
70
69
  let(:healthcheck_path) { "/my/health" }
71
- let(:options) { super.merge(:healthcheck_path => healthcheck_path) }
70
+ let(:options) { super().merge(:healthcheck_path => healthcheck_path) }
72
71
  it "performs the healthcheck to the healthcheck_path" do
73
72
  expect(adapter).to receive(:perform_request) do |url, method, req_path, _, _|
74
73
  expect(method).to eq(:head)
@@ -130,7 +129,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
130
129
  end
131
130
 
132
131
  context "when enabled" do
133
- let(:options) { super.merge(:sniffing => true)}
132
+ let(:options) { super().merge(:sniffing => true)}
134
133
 
135
134
  it "should start the sniffer" do
136
135
  expect(subject.sniffer_alive?).to eql(true)
@@ -247,7 +246,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
247
246
  end
248
247
 
249
248
  let(:options) do
250
- super.merge(:license_checker => license_checker)
249
+ super().merge(:license_checker => license_checker)
251
250
  end
252
251
 
253
252
  context 'when LicenseChecker#acceptable_license? returns false' do
@@ -283,21 +282,21 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
283
282
  let(:oss) { false }
284
283
 
285
284
  context "if ES doesn't return a valid license" do
286
- let(:valid_license) { false }
285
+ let(:license_status) { nil }
287
286
 
288
- it "marks the url as active" do
287
+ it "marks the url as dead" do
289
288
  subject.update_initial_urls
290
- expect(subject.alive_urls_count).to eq(1)
289
+ expect(subject.alive_urls_count).to eq(0)
291
290
  end
292
291
 
293
292
  it "logs a warning" do
294
- expect(subject.license_checker).to receive(:log_license_deprecation_warn).once
293
+ expect(subject.license_checker).to receive(:warn_no_license).once.and_call_original
295
294
  subject.update_initial_urls
296
295
  end
297
296
  end
298
297
 
299
298
  context "if ES returns a valid license" do
300
- let(:valid_license) { true }
299
+ let(:license_status) { 'active' }
301
300
 
302
301
  it "marks the url as active" do
303
302
  subject.update_initial_urls
@@ -305,7 +304,22 @@ describe LogStash::Outputs::ElasticSearch::HttpClient::Pool do
305
304
  end
306
305
 
307
306
  it "does not log a warning" do
308
- expect(subject.license_checker).to_not receive(:log_license_deprecation_warn)
307
+ expect(subject.license_checker).to_not receive(:warn_no_license)
308
+ expect(subject.license_checker).to_not receive(:warn_invalid_license)
309
+ subject.update_initial_urls
310
+ end
311
+ end
312
+
313
+ context "if ES returns an invalid license" do
314
+ let(:license_status) { 'invalid' }
315
+
316
+ it "marks the url as active" do
317
+ subject.update_initial_urls
318
+ expect(subject.alive_urls_count).to eq(1)
319
+ end
320
+
321
+ it "logs a warning" do
322
+ expect(subject.license_checker).to receive(:warn_invalid_license).and_call_original
309
323
  subject.update_initial_urls
310
324
  end
311
325
  end
@@ -1,7 +1,6 @@
1
- require_relative "../../../../spec/es_spec_helper"
2
- require "logstash/devutils/rspec/spec_helper"
1
+ require_relative "../../../../spec/spec_helper"
3
2
  require "logstash/outputs/elasticsearch/http_client"
4
- require "java"
3
+ require "cabin"
5
4
 
6
5
  describe LogStash::Outputs::ElasticSearch::HttpClient do
7
6
  let(:ssl) { nil }
@@ -48,7 +47,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
48
47
  describe "ssl" do
49
48
  context "when SSL is true" do
50
49
  let(:ssl) { true }
51
- let(:base_options) { super.merge(:hosts => [http_hostname_port]) }
50
+ let(:base_options) { super().merge(:hosts => [http_hostname_port]) }
52
51
 
53
52
  it "should refuse to handle an http url" do
54
53
  expect {
@@ -59,7 +58,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
59
58
 
60
59
  context "when SSL is false" do
61
60
  let(:ssl) { false }
62
- let(:base_options) { super.merge(:hosts => [https_hostname_port]) }
61
+ let(:base_options) { super().merge(:hosts => [https_hostname_port]) }
63
62
 
64
63
  it "should refuse to handle an https url" do
65
64
  expect {
@@ -69,7 +68,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
69
68
  end
70
69
 
71
70
  describe "ssl is nil" do
72
- let(:base_options) { super.merge(:hosts => [https_hostname_port]) }
71
+ let(:base_options) { super().merge(:hosts => [https_hostname_port]) }
73
72
  it "should handle an ssl url correctly when SSL is nil" do
74
73
  subject
75
74
  expect(subject.host_to_url(https_hostname_port).to_s).to eq(https_hostname_port.to_s + "/")
@@ -79,14 +78,14 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
79
78
 
80
79
  describe "path" do
81
80
  let(:url) { http_hostname_port_path }
82
- let(:base_options) { super.merge(:hosts => [url]) }
81
+ let(:base_options) { super().merge(:hosts => [url]) }
83
82
 
84
83
  it "should allow paths in a url" do
85
84
  expect(subject.host_to_url(url)).to eq(url)
86
85
  end
87
86
 
88
87
  context "with the path option set" do
89
- let(:base_options) { super.merge(:client_settings => {:path => "/otherpath"}) }
88
+ let(:base_options) { super().merge(:client_settings => {:path => "/otherpath"}) }
90
89
 
91
90
  it "should not allow paths in two places" do
92
91
  expect {
@@ -97,7 +96,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
97
96
 
98
97
  context "with a path missing a leading /" do
99
98
  let(:url) { http_hostname_port }
100
- let(:base_options) { super.merge(:client_settings => {:path => "otherpath"}) }
99
+ let(:base_options) { super().merge(:client_settings => {:path => "otherpath"}) }
101
100
 
102
101
 
103
102
  it "should automatically insert a / in front of path overlays" do
@@ -204,7 +203,7 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
204
203
  end
205
204
 
206
205
  describe "#bulk" do
207
- subject { described_class.new(base_options) }
206
+ subject(:http_client) { described_class.new(base_options) }
208
207
 
209
208
  require "json"
210
209
  let(:message) { "hey" }
@@ -212,42 +211,61 @@ describe LogStash::Outputs::ElasticSearch::HttpClient do
212
211
  ["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message}],
213
212
  ]}
214
213
 
215
- context "if a message is over TARGET_BULK_BYTES" do
216
- let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
217
- let(:message) { "a" * (target_bulk_bytes + 1) }
214
+ [true,false].each do |http_compression_enabled|
215
+ context "with `http_compression => #{http_compression_enabled}`" do
218
216
 
219
- it "should be handled properly" do
220
- allow(subject).to receive(:join_bulk_responses)
221
- expect(subject).to receive(:bulk_send).once do |data|
222
- expect(data.size).to be > target_bulk_bytes
217
+ let(:base_options) { super().merge(:client_settings => {:http_compression => http_compression_enabled}) }
218
+
219
+ before(:each) do
220
+ if http_compression_enabled
221
+ expect(http_client).to receive(:gzip_writer).at_least(:once).and_call_original
222
+ else
223
+ expect(http_client).to_not receive(:gzip_writer)
224
+ end
223
225
  end
224
- s = subject.send(:bulk, actions)
225
- end
226
- end
227
226
 
228
- context "with two messages" do
229
- let(:message1) { "hey" }
230
- let(:message2) { "you" }
231
- let(:actions) { [
232
- ["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message1}],
233
- ["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message2}],
234
- ]}
235
- it "executes one bulk_send operation" do
236
- allow(subject).to receive(:join_bulk_responses)
237
- expect(subject).to receive(:bulk_send).once
238
- s = subject.send(:bulk, actions)
239
- end
227
+ context "if a message is over TARGET_BULK_BYTES" do
228
+ let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
229
+ let(:message) { "a" * (target_bulk_bytes + 1) }
230
+
231
+ it "should be handled properly" do
232
+ allow(subject).to receive(:join_bulk_responses)
233
+ expect(subject).to receive(:bulk_send).once do |data|
234
+ if !http_compression_enabled
235
+ expect(data.size).to be > target_bulk_bytes
236
+ else
237
+ expect(Zlib::gunzip(data.string).size).to be > target_bulk_bytes
238
+ end
239
+ end
240
+ s = subject.send(:bulk, actions)
241
+ end
242
+ end
243
+
244
+ context "with two messages" do
245
+ let(:message1) { "hey" }
246
+ let(:message2) { "you" }
247
+ let(:actions) { [
248
+ ["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message1}],
249
+ ["index", {:_id=>nil, :_index=>"logstash"}, {"message"=> message2}],
250
+ ]}
251
+ it "executes one bulk_send operation" do
252
+ allow(subject).to receive(:join_bulk_responses)
253
+ expect(subject).to receive(:bulk_send).once
254
+ s = subject.send(:bulk, actions)
255
+ end
240
256
 
241
- context "if one exceeds TARGET_BULK_BYTES" do
242
- let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
243
- let(:message1) { "a" * (target_bulk_bytes + 1) }
244
- it "executes two bulk_send operations" do
245
- allow(subject).to receive(:join_bulk_responses)
246
- expect(subject).to receive(:bulk_send).twice
247
- s = subject.send(:bulk, actions)
257
+ context "if one exceeds TARGET_BULK_BYTES" do
258
+ let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
259
+ let(:message1) { "a" * (target_bulk_bytes + 1) }
260
+ it "executes two bulk_send operations" do
261
+ allow(subject).to receive(:join_bulk_responses)
262
+ expect(subject).to receive(:bulk_send).twice
263
+ s = subject.send(:bulk, actions)
264
+ end
265
+ end
248
266
  end
249
- end
250
- end
267
+ end
268
+ end
251
269
  end
252
270
 
253
271
  describe "sniffing" do
@@ -1,7 +1,5 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
- require "logstash/outputs/elasticsearch/http_client"
3
- require "java"
4
- require "json"
2
+ require "logstash/outputs/elasticsearch/template_manager"
5
3
 
6
4
  describe LogStash::Outputs::ElasticSearch::TemplateManager do
7
5
 
@@ -1,6 +1,5 @@
1
- require_relative "../../../spec/es_spec_helper"
1
+ require_relative "../../../spec/spec_helper"
2
2
  require 'stud/temporary'
3
- require "logstash/outputs/elasticsearch"
4
3
  require 'manticore/client'
5
4
 
6
5
  describe "Proxy option" do
@@ -24,7 +23,7 @@ describe "Proxy option" do
24
23
 
25
24
  context "when specified as a URI" do
26
25
  shared_examples("hash conversion") do |hash|
27
- let(:settings) { super.merge("proxy" => proxy)}
26
+ let(:settings) { super().merge("proxy" => proxy)}
28
27
 
29
28
  it "should set the proxy to the correct hash value" do
30
29
  expect(::Manticore::Client).to have_received(:new) do |options|
@@ -71,7 +70,7 @@ describe "Proxy option" do
71
70
  end
72
71
 
73
72
  context "when specified as ''" do
74
- let(:settings) { super.merge("proxy" => "${A_MISSING_ENV_VARIABLE:}")}
73
+ let(:settings) { super().merge("proxy" => "${A_MISSING_ENV_VARIABLE:}")}
75
74
 
76
75
  it "should not send the proxy option to manticore" do
77
76
  expect { subject.register }.not_to raise_error
@@ -85,7 +84,7 @@ describe "Proxy option" do
85
84
  end
86
85
 
87
86
  context "when specified as invalid uri" do
88
- let(:settings) { super.merge("proxy" => ":")}
87
+ let(:settings) { super().merge("proxy" => ":")}
89
88
 
90
89
  it "should fail" do
91
90
  # SafeURI isn't doing the proper exception wrapping for us, we can not simply :
@@ -1,27 +1,40 @@
1
- require_relative "../../../spec/es_spec_helper"
1
+ require_relative "../../../spec/spec_helper"
2
2
  require "base64"
3
3
  require "flores/random"
4
+ require 'concurrent/atomic/count_down_latch'
4
5
  require "logstash/outputs/elasticsearch"
5
6
 
6
7
  describe LogStash::Outputs::ElasticSearch do
7
- subject { described_class.new(options) }
8
+ subject(:elasticsearch_output_instance) { described_class.new(options) }
8
9
  let(:options) { {} }
9
10
  let(:maximum_seen_major_version) { [1,2,5,6,7,8].sample }
10
11
 
11
12
  let(:do_register) { true }
12
13
 
14
+ let(:stub_http_client_pool!) do
15
+ allow_any_instance_of(LogStash::Outputs::ElasticSearch::HttpClient::Pool).to receive(:start)
16
+ end
17
+
18
+ let(:after_successful_connection_thread_mock) do
19
+ double('after_successful_connection_thread', value: true)
20
+ end
21
+
13
22
  before(:each) do
14
23
  if do_register
15
- # Build the client and set mocks before calling register to avoid races.
16
- subject.build_client
24
+ stub_http_client_pool!
25
+
26
+ allow(subject).to receive(:finish_register) # stub-out thread completion (to avoid error log entries)
27
+
28
+ # emulate 'successful' ES connection on the same thread
29
+ allow(subject).to receive(:after_successful_connection) { |&block| block.call }.
30
+ and_return after_successful_connection_thread_mock
31
+ allow(subject).to receive(:stop_after_successful_connection_thread)
32
+
33
+ subject.register
17
34
 
18
- # Rspec mocks can't handle background threads, so... we can't use any
19
- allow(subject.client.pool).to receive(:start_resurrectionist)
20
- allow(subject.client.pool).to receive(:start_sniffer)
21
- allow(subject.client.pool).to receive(:healthcheck!)
22
35
  allow(subject.client).to receive(:maximum_seen_major_version).at_least(:once).and_return(maximum_seen_major_version)
23
36
  allow(subject.client).to receive(:get_xpack_info)
24
- subject.register
37
+
25
38
  subject.client.pool.adapter.manticore.respond_with(:body => "{}")
26
39
  end
27
40
  end
@@ -44,9 +57,15 @@ describe LogStash::Outputs::ElasticSearch do
44
57
  let(:manticore_urls) { subject.client.pool.urls }
45
58
  let(:manticore_url) { manticore_urls.first }
46
59
 
60
+ let(:stub_http_client_pool!) do
61
+ [:start_resurrectionist, :start_sniffer, :healthcheck!].each do |method|
62
+ allow_any_instance_of(LogStash::Outputs::ElasticSearch::HttpClient::Pool).to receive(method)
63
+ end
64
+ end
65
+
47
66
  describe "getting a document type" do
48
67
  context "if document_type isn't set" do
49
- let(:options) { super.merge("document_type" => nil)}
68
+ let(:options) { super().merge("document_type" => nil)}
50
69
  context "for 7.x elasticsearch clusters" do
51
70
  let(:maximum_seen_major_version) { 7 }
52
71
  it "should return '_doc'" do
@@ -70,7 +89,7 @@ describe LogStash::Outputs::ElasticSearch do
70
89
  end
71
90
 
72
91
  context "with 'document type set'" do
73
- let(:options) { super.merge("document_type" => "bar")}
92
+ let(:options) { super().merge("document_type" => "bar")}
74
93
  it "should get the event type from the 'document_type' setting" do
75
94
  expect(subject.send(:get_event_type, LogStash::Event.new())).to eql("bar")
76
95
  end
@@ -80,14 +99,14 @@ describe LogStash::Outputs::ElasticSearch do
80
99
  describe "building an event action tuple" do
81
100
  context "for 7.x elasticsearch clusters" do
82
101
  let(:maximum_seen_major_version) { 7 }
83
- it "should include '_type'" do
102
+ it "should not include '_type' when 'document_type' is not explicitly defined" do
84
103
  action_tuple = subject.send(:event_action_tuple, LogStash::Event.new("type" => "foo"))
85
104
  action_params = action_tuple[1]
86
- expect(action_params).to include(:_type => "_doc")
105
+ expect(action_params).not_to include(:_type => "_doc")
87
106
  end
88
107
 
89
108
  context "with 'document type set'" do
90
- let(:options) { super.merge("document_type" => "bar")}
109
+ let(:options) { super().merge("document_type" => "bar")}
91
110
  it "should get the event type from the 'document_type' setting" do
92
111
  action_tuple = subject.send(:event_action_tuple, LogStash::Event.new("type" => "foo"))
93
112
  action_params = action_tuple[1]
@@ -105,7 +124,7 @@ describe LogStash::Outputs::ElasticSearch do
105
124
  end
106
125
 
107
126
  context "with 'document type set'" do
108
- let(:options) { super.merge("document_type" => "bar")}
127
+ let(:options) { super().merge("document_type" => "bar")}
109
128
  it "should not include '_type'" do
110
129
  action_tuple = subject.send(:event_action_tuple, LogStash::Event.new("type" => "foo"))
111
130
  action_params = action_tuple[1]
@@ -127,7 +146,7 @@ describe LogStash::Outputs::ElasticSearch do
127
146
 
128
147
  context "as part of a URL" do
129
148
  let(:options) {
130
- super.merge("hosts" => ["http://#{user}:#{password.value}@localhost:9200"])
149
+ super().merge("hosts" => ["http://#{user}:#{password.value}@localhost:9200"])
131
150
  }
132
151
 
133
152
  include_examples("an authenticated config")
@@ -135,7 +154,7 @@ describe LogStash::Outputs::ElasticSearch do
135
154
 
136
155
  context "as a hash option" do
137
156
  let(:options) {
138
- super.merge!(
157
+ super().merge!(
139
158
  "user" => user,
140
159
  "password" => password
141
160
  )
@@ -175,7 +194,7 @@ describe LogStash::Outputs::ElasticSearch do
175
194
 
176
195
  context "with extra slashes" do
177
196
  let(:path) { "/slashed-path/ "}
178
- let(:options) { super.merge("path" => "/some-path/") }
197
+ let(:options) { super().merge("path" => "/some-path/") }
179
198
 
180
199
  it "should properly set the path on the HTTP client without adding slashes" do
181
200
  expect(manticore_url.path).to eql(options["path"])
@@ -234,13 +253,13 @@ describe LogStash::Outputs::ElasticSearch do
234
253
  end
235
254
 
236
255
  describe "without a port specified" do
237
- let(:options) { super.merge('hosts' => 'localhost') }
256
+ let(:options) { super().merge('hosts' => 'localhost') }
238
257
  it "should properly set the default port (9200) on the HTTP client" do
239
258
  expect(manticore_url.port).to eql(9200)
240
259
  end
241
260
  end
242
261
  describe "with a port other than 9200 specified" do
243
- let(:options) { super.merge('hosts' => 'localhost:9202') }
262
+ let(:options) { super().merge('hosts' => 'localhost:9202') }
244
263
  it "should properly set the specified port on the HTTP client" do
245
264
  expect(manticore_url.port).to eql(9202)
246
265
  end
@@ -253,8 +272,7 @@ describe LogStash::Outputs::ElasticSearch do
253
272
  before do
254
273
  allow(subject).to receive(:retrying_submit).with(anything)
255
274
  events.each_with_index do |e,i|
256
- et = events_tuples[i]
257
- allow(subject).to receive(:event_action_tuple).with(e).and_return(et)
275
+ allow(subject).to receive(:event_action_tuple).with(e).and_return(events_tuples[i])
258
276
  end
259
277
  subject.multi_receive(events)
260
278
  end
@@ -265,12 +283,14 @@ describe LogStash::Outputs::ElasticSearch do
265
283
  let(:event) { ::LogStash::Event.new("foo" => "bar") }
266
284
  let(:error) do
267
285
  ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(
268
- 429, double("url").as_null_object, double("request body"), double("response body")
286
+ 429, double("url").as_null_object, request_body, double("response body")
269
287
  )
270
288
  end
271
289
  let(:logger) { double("logger").as_null_object }
272
290
  let(:response) { { :errors => [], :items => [] } }
273
291
 
292
+ let(:request_body) { double(:request_body, :bytesize => 1023) }
293
+
274
294
  before(:each) do
275
295
 
276
296
  i = 0
@@ -296,6 +316,95 @@ describe LogStash::Outputs::ElasticSearch do
296
316
  expect(subject.logger).to have_received(:debug).with(/Encountered a retryable error/i, anything)
297
317
  end
298
318
  end
319
+
320
+ context "unexpected bulk response" do
321
+ let(:options) do
322
+ { "hosts" => "127.0.0.1:9999", "index" => "%{foo}", "manage_template" => false }
323
+ end
324
+
325
+ let(:events) { [ ::LogStash::Event.new("foo" => "bar1"), ::LogStash::Event.new("foo" => "bar2") ] }
326
+
327
+ let(:bulk_response) do
328
+ # shouldn't really happen but we've seen this happen - here ES returns more items than were sent
329
+ { "took"=>1, "ingest_took"=>9, "errors"=>true,
330
+ "items"=>[{"index"=>{"_index"=>"bar1", "_type"=>"_doc", "_id"=>nil, "status"=>500,
331
+ "error"=>{"type" => "illegal_state_exception",
332
+ "reason" => "pipeline with id [test-ingest] could not be loaded, caused by [ElasticsearchParseException[Error updating pipeline with id [test-ingest]]; nested: ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]; nested: IllegalArgumentException[no enrich index exists for policy with name [test-metadata1]];; ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]; nested: IllegalArgumentException[no enrich index exists for policy with name [test-metadata1]];; java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]]"
333
+ }
334
+ }
335
+ },
336
+ # NOTE: this is an artificial success (usually everything fails with a 500) but even if some doc where
337
+ # to succeed due the unexpected reponse items we can not clearly identify which actions to retry ...
338
+ {"index"=>{"_index"=>"bar2", "_type"=>"_doc", "_id"=>nil, "status"=>201}},
339
+ {"index"=>{"_index"=>"bar2", "_type"=>"_doc", "_id"=>nil, "status"=>500,
340
+ "error"=>{"type" => "illegal_state_exception",
341
+ "reason" => "pipeline with id [test-ingest] could not be loaded, caused by [ElasticsearchParseException[Error updating pipeline with id [test-ingest]]; nested: ElasticsearchException[java.lang.IllegalArgumentException: no enrich index exists for policy with name [test-metadata1]];"
342
+ }
343
+ }
344
+ }]
345
+ }
346
+ end
347
+
348
+ before(:each) do
349
+ allow(subject.client).to receive(:bulk_send).with(instance_of(StringIO), instance_of(Array)) do |stream, actions|
350
+ expect( stream.string ).to include '"foo":"bar1"'
351
+ expect( stream.string ).to include '"foo":"bar2"'
352
+ end.and_return(bulk_response, {"errors"=>false}) # let's make it go away (second call) to not retry indefinitely
353
+ end
354
+
355
+ it "should retry submit" do
356
+ allow(subject.logger).to receive(:error).with(/Encountered an unexpected error/i, anything)
357
+ allow(subject.client).to receive(:bulk).and_call_original # track count
358
+
359
+ subject.multi_receive(events)
360
+
361
+ expect(subject.client).to have_received(:bulk).twice
362
+ end
363
+
364
+ it "should log specific error message" do
365
+ expect(subject.logger).to receive(:error).with(/Encountered an unexpected error/i,
366
+ hash_including(:message => 'Sent 2 documents but Elasticsearch returned 3 responses (likely a bug with _bulk endpoint)'))
367
+
368
+ subject.multi_receive(events)
369
+ end
370
+ end
371
+ end
372
+
373
+ context '413 errors' do
374
+ let(:payload_size) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES + 1024 }
375
+ let(:event) { ::LogStash::Event.new("message" => ("a" * payload_size ) ) }
376
+
377
+ let(:logger_stub) { double("logger").as_null_object }
378
+
379
+ before(:each) do
380
+ allow(elasticsearch_output_instance.client).to receive(:logger).and_return(logger_stub)
381
+
382
+ allow(elasticsearch_output_instance.client).to receive(:bulk).and_call_original
383
+
384
+ max_bytes = payload_size * 3 / 4 # ensure a failure first attempt
385
+ allow(elasticsearch_output_instance.client.pool).to receive(:post) do |path, params, body|
386
+ if body.length > max_bytes
387
+ max_bytes *= 2 # ensure a successful retry
388
+ double("Response", :code => 413, :body => "")
389
+ else
390
+ double("Response", :code => 200, :body => '{"errors":false,"items":[{"index":{"status":200,"result":"created"}}]}')
391
+ end
392
+ end
393
+ end
394
+
395
+ it 'retries the 413 until it goes away' do
396
+ elasticsearch_output_instance.multi_receive([event])
397
+
398
+ expect(elasticsearch_output_instance.client).to have_received(:bulk).twice
399
+ end
400
+
401
+ it 'logs about payload quantity and size' do
402
+ elasticsearch_output_instance.multi_receive([event])
403
+
404
+ expect(logger_stub).to have_received(:warn)
405
+ .with(a_string_matching(/413 Payload Too Large/),
406
+ hash_including(:action_count => 1, :content_length => a_value > 20_000_000))
407
+ end
299
408
  end
300
409
 
301
410
  context "with timeout set" do
@@ -311,7 +420,7 @@ describe LogStash::Outputs::ElasticSearch do
311
420
 
312
421
  before do
313
422
  # Expect a timeout to be logged.
314
- expect(subject.logger).to receive(:error).with(/Attempted to send a bulk request to Elasticsearch/i, anything).at_least(:once)
423
+ expect(subject.logger).to receive(:error).with(/Attempted to send a bulk request/i, anything).at_least(:once)
315
424
  expect(subject.client).to receive(:bulk).at_least(:twice).and_call_original
316
425
  end
317
426
 
@@ -325,13 +434,14 @@ describe LogStash::Outputs::ElasticSearch do
325
434
  end
326
435
 
327
436
  describe "the action option" do
437
+
328
438
  context "with a sprintf action" do
329
439
  let(:options) { {"action" => "%{myactionfield}" } }
330
440
 
331
441
  let(:event) { LogStash::Event.new("myactionfield" => "update", "message" => "blah") }
332
442
 
333
443
  it "should interpolate the requested action value when creating an event_action_tuple" do
334
- expect(subject.event_action_tuple(event).first).to eql("update")
444
+ expect(subject.send(:event_action_tuple, event).first).to eql("update")
335
445
  end
336
446
  end
337
447
 
@@ -341,7 +451,7 @@ describe LogStash::Outputs::ElasticSearch do
341
451
  let(:event) { LogStash::Event.new("myactionfield" => "update", "message" => "blah") }
342
452
 
343
453
  it "should obtain specific action's params from event_action_tuple" do
344
- expect(subject.event_action_tuple(event)[1]).to include(:_upsert)
454
+ expect(subject.send(:event_action_tuple, event)[1]).to include(:_upsert)
345
455
  end
346
456
  end
347
457
 
@@ -349,6 +459,8 @@ describe LogStash::Outputs::ElasticSearch do
349
459
  let(:options) { {"action" => "SOME Garbaaage"} }
350
460
  let(:do_register) { false } # this is what we want to test, so we disable the before(:each) call
351
461
 
462
+ before { allow(subject).to receive(:finish_register) }
463
+
352
464
  it "should raise a configuration error" do
353
465
  expect { subject.register }.to raise_error(LogStash::ConfigurationError)
354
466
  end
@@ -356,13 +468,14 @@ describe LogStash::Outputs::ElasticSearch do
356
468
  end
357
469
 
358
470
  describe "the pipeline option" do
471
+
359
472
  context "with a sprintf and set pipeline" do
360
473
  let(:options) { {"pipeline" => "%{pipeline}" } }
361
474
 
362
475
  let(:event) { LogStash::Event.new("pipeline" => "my-ingest-pipeline") }
363
476
 
364
477
  it "should interpolate the pipeline value and set it" do
365
- expect(subject.event_action_tuple(event)[1]).to include(:pipeline => "my-ingest-pipeline")
478
+ expect(subject.send(:event_action_tuple, event)[1]).to include(:pipeline => "my-ingest-pipeline")
366
479
  end
367
480
  end
368
481
 
@@ -372,7 +485,7 @@ describe LogStash::Outputs::ElasticSearch do
372
485
  let(:event) { LogStash::Event.new("pipeline" => "") }
373
486
 
374
487
  it "should interpolate the pipeline value but not set it because it is empty" do
375
- expect(subject.event_action_tuple(event)[1]).not_to include(:pipeline)
488
+ expect(subject.send(:event_action_tuple, event)[1]).not_to include(:pipeline)
376
489
  end
377
490
  end
378
491
  end
@@ -410,30 +523,30 @@ describe LogStash::Outputs::ElasticSearch do
410
523
  let(:options) { { 'retry_on_conflict' => num_retries } }
411
524
 
412
525
  context "with a regular index" do
413
- let(:options) { super.merge("action" => "index") }
526
+ let(:options) { super().merge("action" => "index") }
414
527
 
415
528
  it "should not set the retry_on_conflict parameter when creating an event_action_tuple" do
416
529
  allow(subject.client).to receive(:maximum_seen_major_version).and_return(maximum_seen_major_version)
417
- action, params, event_data = subject.event_action_tuple(event)
418
- expect(params).not_to include({subject.retry_on_conflict_action_name => num_retries})
530
+ action, params, event_data = subject.send(:event_action_tuple, event)
531
+ expect(params).not_to include({subject.send(:retry_on_conflict_action_name) => num_retries})
419
532
  end
420
533
  end
421
534
 
422
535
  context "using a plain update" do
423
- let(:options) { super.merge("action" => "update", "retry_on_conflict" => num_retries, "document_id" => 1) }
536
+ let(:options) { super().merge("action" => "update", "retry_on_conflict" => num_retries, "document_id" => 1) }
424
537
 
425
538
  it "should set the retry_on_conflict parameter when creating an event_action_tuple" do
426
- action, params, event_data = subject.event_action_tuple(event)
427
- expect(params).to include({subject.retry_on_conflict_action_name => num_retries})
539
+ action, params, event_data = subject.send(:event_action_tuple, event)
540
+ expect(params).to include({subject.send(:retry_on_conflict_action_name) => num_retries})
428
541
  end
429
542
  end
430
543
 
431
544
  context "with a sprintf action that resolves to update" do
432
- let(:options) { super.merge("action" => "%{myactionfield}", "retry_on_conflict" => num_retries, "document_id" => 1) }
545
+ let(:options) { super().merge("action" => "%{myactionfield}", "retry_on_conflict" => num_retries, "document_id" => 1) }
433
546
 
434
547
  it "should set the retry_on_conflict parameter when creating an event_action_tuple" do
435
- action, params, event_data = subject.event_action_tuple(event)
436
- expect(params).to include({subject.retry_on_conflict_action_name => num_retries})
548
+ action, params, event_data = subject.send(:event_action_tuple, event)
549
+ expect(params).to include({subject.send(:retry_on_conflict_action_name) => num_retries})
437
550
  expect(action).to eq("update")
438
551
  end
439
552
  end
@@ -463,6 +576,8 @@ describe LogStash::Outputs::ElasticSearch do
463
576
  let(:do_register) { false }
464
577
 
465
578
  before :each do
579
+ allow(subject).to receive(:finish_register)
580
+
466
581
  allow(::Manticore::Client).to receive(:new).with(any_args).and_call_original
467
582
  end
468
583
 
@@ -486,6 +601,12 @@ describe LogStash::Outputs::ElasticSearch do
486
601
  let(:custom_parameters_hash) { { "id" => 1, "name" => "logstash" } }
487
602
  let(:custom_parameters_query) { custom_parameters_hash.map {|k,v| "#{k}=#{v}" }.join("&") }
488
603
 
604
+ let(:stub_http_client_pool!) do
605
+ [:start_resurrectionist, :start_sniffer, :healthcheck!].each do |method|
606
+ allow_any_instance_of(LogStash::Outputs::ElasticSearch::HttpClient::Pool).to receive(method)
607
+ end
608
+ end
609
+
489
610
  context "using non-url hosts" do
490
611
 
491
612
  let(:options) {
@@ -650,9 +771,9 @@ describe LogStash::Outputs::ElasticSearch do
650
771
 
651
772
  context 'when getting any other exception' do
652
773
  it 'should log at WARN level' do
653
- dlog = double_logger = double("logger").as_null_object
654
- subject.instance_variable_set(:@logger, dlog)
655
- expect(dlog).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
774
+ logger = double("logger").as_null_object
775
+ subject.instance_variable_set(:@logger, logger)
776
+ expect(logger).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
656
777
  mock_response = { 'index' => { 'error' => { 'type' => 'illegal_argument_exception' } } }
657
778
  subject.handle_dlq_status("Could not index event to Elasticsearch.",
658
779
  [:action, :params, :event], :some_status, mock_response)
@@ -661,9 +782,9 @@ describe LogStash::Outputs::ElasticSearch do
661
782
 
662
783
  context 'when the response does not include [error]' do
663
784
  it 'should not fail, but just log a warning' do
664
- dlog = double_logger = double("logger").as_null_object
665
- subject.instance_variable_set(:@logger, dlog)
666
- expect(dlog).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
785
+ logger = double("logger").as_null_object
786
+ subject.instance_variable_set(:@logger, logger)
787
+ expect(logger).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
667
788
  mock_response = { 'index' => {} }
668
789
  expect do
669
790
  subject.handle_dlq_status("Could not index event to Elasticsearch.",
@@ -683,16 +804,59 @@ describe LogStash::Outputs::ElasticSearch do
683
804
  # We should still log when sending to the DLQ.
684
805
  # This shall be solved by another issue, however: logstash-output-elasticsearch#772
685
806
  it 'should send the event to the DLQ instead, and not log' do
686
- expect(dlq_writer).to receive(:write).once.with(:event, /Could not index/)
807
+ event = LogStash::Event.new("foo" => "bar")
808
+ expect(dlq_writer).to receive(:write).once.with(event, /Could not index/)
687
809
  mock_response = { 'index' => { 'error' => { 'type' => 'illegal_argument_exception' } } }
688
- subject.handle_dlq_status("Could not index event to Elasticsearch.",
689
- [:action, :params, :event], :some_status, mock_response)
810
+ action = LogStash::Outputs::ElasticSearch::EventActionTuple.new(:action, :params, event)
811
+ subject.handle_dlq_status("Could not index event to Elasticsearch.", action, 404, mock_response)
690
812
  end
691
813
  end
814
+
815
+ context 'with response status 400' do
816
+
817
+ let(:options) { super().merge 'document_id' => '%{foo}' }
818
+
819
+ let(:events) { [ LogStash::Event.new("foo" => "bar") ] }
820
+
821
+ let(:dlq_writer) { subject.instance_variable_get(:@dlq_writer) }
822
+
823
+ let(:bulk_response) do
824
+ {
825
+ "took"=>1, "ingest_took"=>11, "errors"=>true, "items"=>
826
+ [{
827
+ "index"=>{"_index"=>"bar", "_type"=>"_doc", "_id"=>'bar', "status"=>400,
828
+ "error"=>{"type" => "illegal_argument_exception", "reason" => "TEST" }
829
+ }
830
+ }]
831
+ }
832
+ end
833
+
834
+ before(:each) do
835
+ allow(subject.client).to receive(:bulk_send).and_return(bulk_response)
836
+ end
837
+
838
+ it "should write event to DLQ" do
839
+ expect(dlq_writer).to receive(:write).and_wrap_original do |method, *args|
840
+ expect( args.size ).to eql 2
841
+
842
+ event, reason = *args
843
+ expect( event ).to be_a LogStash::Event
844
+ expect( event ).to be events.first
845
+ expect( reason ).to start_with 'Could not index event to Elasticsearch. status: 400, action: ["index"'
846
+ expect( reason ).to match /_id=>"bar".*"foo"=>"bar".*response:.*"reason"=>"TEST"/
847
+
848
+ method.call(*args) # won't hurt to call LogStash::Util::DummyDeadLetterQueueWriter
849
+ end.once
850
+
851
+ event_action_tuples = subject.map_events(events)
852
+ subject.send(:submit, event_action_tuples)
853
+ end
854
+
855
+ end if LOGSTASH_VERSION > '7.0'
692
856
  end
693
857
 
694
858
  describe "custom headers" do
695
- let(:manticore_options) { subject.client.pool.adapter.manticore.instance_variable_get(:@options) }
859
+ let(:manticore_options) { subject.client.pool.adapter.manticore.instance_variable_get(:@options) }
696
860
 
697
861
  context "when set" do
698
862
  let(:headers) { { "X-Thing" => "Test" } }
@@ -765,6 +929,75 @@ describe LogStash::Outputs::ElasticSearch do
765
929
  end
766
930
  end
767
931
 
932
+ describe "post-register ES setup" do
933
+ let(:do_register) { false }
934
+ let(:es_version) { '7.10.0' } # DS default on LS 8.x
935
+ let(:options) { { 'hosts' => '127.0.0.1:9999' } }
936
+ let(:logger) { subject.logger }
937
+
938
+ before do
939
+ allow(logger).to receive(:error) # expect tracking
940
+
941
+ allow(subject).to receive(:last_es_version).and_return es_version
942
+ # make successful_connection? return true:
943
+ allow(subject).to receive(:maximum_seen_major_version).and_return Integer(es_version.split('.').first)
944
+ allow(subject).to receive(:stop_after_successful_connection_thread)
945
+ end
946
+
947
+ it "logs inability to retrieve uuid" do
948
+ allow(subject).to receive(:install_template)
949
+ allow(subject).to receive(:ilm_in_use?).and_return nil
950
+ subject.register
951
+ subject.send :wait_for_successful_connection
952
+
953
+ expect(logger).to have_received(:error).with(/Unable to retrieve Elasticsearch cluster uuid/i, anything)
954
+ end if LOGSTASH_VERSION >= '7.0.0'
955
+
956
+ it "logs template install failure" do
957
+ allow(subject).to receive(:discover_cluster_uuid)
958
+ allow(subject).to receive(:ilm_in_use?).and_return nil
959
+ subject.register
960
+ subject.send :wait_for_successful_connection
961
+
962
+ expect(logger).to have_received(:error).with(/Failed to install template/i, anything)
963
+ end
964
+
965
+ context 'error raised' do
966
+
967
+ let(:es_version) { '7.8.0' }
968
+ let(:options) { super().merge('data_stream' => 'true') }
969
+ let(:latch) { Concurrent::CountDownLatch.new }
970
+
971
+ before do
972
+ allow(subject).to receive(:install_template)
973
+ allow(subject).to receive(:discover_cluster_uuid)
974
+ allow(subject).to receive(:ilm_in_use?).and_return nil
975
+ # executes from the after_successful_connection thread :
976
+ allow(subject).to receive(:finish_register) { latch.wait }.and_call_original
977
+ subject.register
978
+ end
979
+
980
+ it 'keeps logging on multi_receive' do
981
+ allow(subject).to receive(:retrying_submit)
982
+ latch.count_down; sleep(1.0)
983
+
984
+ expect_logged_error = lambda do |count|
985
+ expect(logger).to have_received(:error).with(
986
+ /Elasticsearch setup did not complete normally, please review previously logged errors/i,
987
+ hash_including(message: 'A data_stream configuration is only supported since Elasticsearch 7.9.0 (detected version 7.8.0), please upgrade your cluster')
988
+ ).exactly(count).times
989
+ end
990
+
991
+ subject.multi_receive [ LogStash::Event.new('foo' => 1) ]
992
+ expect_logged_error.call(1)
993
+
994
+ subject.multi_receive [ LogStash::Event.new('foo' => 2) ]
995
+ expect_logged_error.call(2)
996
+ end
997
+
998
+ end
999
+ end
1000
+
768
1001
  @private
769
1002
 
770
1003
  def stub_manticore_client!(manticore_double = nil)