fluent-plugin-opensearch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.editorconfig +9 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  6. data/.github/workflows/coverage.yaml +22 -0
  7. data/.github/workflows/issue-auto-closer.yml +12 -0
  8. data/.github/workflows/linux.yml +26 -0
  9. data/.github/workflows/macos.yml +26 -0
  10. data/.github/workflows/windows.yml +26 -0
  11. data/.gitignore +18 -0
  12. data/CONTRIBUTING.md +24 -0
  13. data/Gemfile +10 -0
  14. data/History.md +6 -0
  15. data/ISSUE_TEMPLATE.md +26 -0
  16. data/LICENSE.txt +201 -0
  17. data/PULL_REQUEST_TEMPLATE.md +9 -0
  18. data/README.OpenSearchGenID.md +116 -0
  19. data/README.OpenSearchInput.md +291 -0
  20. data/README.Troubleshooting.md +482 -0
  21. data/README.md +1556 -0
  22. data/Rakefile +37 -0
  23. data/fluent-plugin-opensearch.gemspec +38 -0
  24. data/gemfiles/Gemfile.elasticsearch.v6 +12 -0
  25. data/lib/fluent/log-ext.rb +64 -0
  26. data/lib/fluent/plugin/filter_opensearch_genid.rb +103 -0
  27. data/lib/fluent/plugin/in_opensearch.rb +351 -0
  28. data/lib/fluent/plugin/oj_serializer.rb +48 -0
  29. data/lib/fluent/plugin/opensearch_constants.rb +39 -0
  30. data/lib/fluent/plugin/opensearch_error.rb +31 -0
  31. data/lib/fluent/plugin/opensearch_error_handler.rb +166 -0
  32. data/lib/fluent/plugin/opensearch_fallback_selector.rb +36 -0
  33. data/lib/fluent/plugin/opensearch_index_template.rb +155 -0
  34. data/lib/fluent/plugin/opensearch_simple_sniffer.rb +36 -0
  35. data/lib/fluent/plugin/opensearch_tls.rb +96 -0
  36. data/lib/fluent/plugin/out_opensearch.rb +1124 -0
  37. data/lib/fluent/plugin/out_opensearch_data_stream.rb +214 -0
  38. data/test/helper.rb +61 -0
  39. data/test/plugin/test_alias_template.json +9 -0
  40. data/test/plugin/test_filter_opensearch_genid.rb +241 -0
  41. data/test/plugin/test_in_opensearch.rb +493 -0
  42. data/test/plugin/test_index_alias_template.json +11 -0
  43. data/test/plugin/test_index_template.json +25 -0
  44. data/test/plugin/test_oj_serializer.rb +45 -0
  45. data/test/plugin/test_opensearch_error_handler.rb +689 -0
  46. data/test/plugin/test_opensearch_fallback_selector.rb +100 -0
  47. data/test/plugin/test_opensearch_tls.rb +171 -0
  48. data/test/plugin/test_out_opensearch.rb +3953 -0
  49. data/test/plugin/test_out_opensearch_data_stream.rb +474 -0
  50. data/test/plugin/test_template.json +23 -0
  51. data/test/test_log-ext.rb +61 -0
  52. metadata +262 -0
@@ -0,0 +1,3953 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The fluent-plugin-opensearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright fluent-plugin-opensearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Uken Inc. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Uken Inc. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ require_relative '../helper'
28
+ require 'date'
29
+ require 'fluent/test/helpers'
30
+ require 'json'
31
+ require 'fluent/test/driver/output'
32
+ require 'flexmock/test_unit'
33
+ require 'fluent/plugin/out_opensearch'
34
+
35
+ class OpenSearchOutputTest < Test::Unit::TestCase
36
+ include FlexMock::TestCase
37
+ include Fluent::Test::Helpers
38
+
39
+ attr_accessor :index_cmds, :index_command_counts, :index_cmds_all_requests
40
+
41
+ def setup
42
+ Fluent::Test.setup
43
+ @driver = nil
44
+ log = Fluent::Engine.log
45
+ log.out.logs.slice!(0, log.out.logs.length)
46
+ end
47
+
48
+ def driver(conf='', os_version=1, client_version="\"1.2\"")
49
+ # For request stub to detect compatibility.
50
+ @os_version ||= os_version
51
+ @client_version ||= client_version
52
+ if @os_version
53
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
54
+ def detect_os_major_version
55
+ #{@os_version}
56
+ end
57
+ CODE
58
+ end
59
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
60
+ def client_library_version
61
+ #{@client_version}
62
+ end
63
+ CODE
64
+ @driver ||= Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput) {
65
+ # v0.12's test driver assume format definition. This simulates ObjectBufferedOutput format
66
+ if !defined?(Fluent::Plugin::Output)
67
+ def format(tag, time, record)
68
+ [time, record].to_msgpack
69
+ end
70
+ end
71
+ }.configure(conf)
72
+ end
73
+
74
+ def default_type_name
75
+ Fluent::Plugin::OpenSearchOutput::DEFAULT_TYPE_NAME
76
+ end
77
+
78
+ def sample_record(content={})
79
+ {'age' => 26, 'request_id' => '42', 'parent_id' => 'parent', 'routing_id' => 'routing'}.merge(content)
80
+ end
81
+
82
+ def nested_sample_record
83
+ {'nested' =>
84
+ {'age' => 26, 'parent_id' => 'parent', 'routing_id' => 'routing', 'request_id' => '42'}
85
+ }
86
+ end
87
+
88
+ def stub_opensearch_info(url="http://localhost:9200/", version="1.2.2")
89
+ body ="{\"version\":{\"number\":\"#{version}\", \"distribution\":\"opensearch\"},\"tagline\":\"The OpenSearch Project: https://opensearch.org/\"}"
90
+ stub_request(:get, url).to_return({:status => 200, :body => body, :headers => { 'Content-Type' => 'json' } })
91
+ end
92
+
93
+ def stub_opensearch(url="http://localhost:9200/_bulk")
94
+ stub_request(:post, url).with do |req|
95
+ @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
96
+ end
97
+ end
98
+
99
+ def stub_opensearch_all_requests(url="http://localhost:9200/_bulk")
100
+ @index_cmds_all_requests = Array.new
101
+ stub_request(:post, url).with do |req|
102
+ @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
103
+ @index_cmds_all_requests << @index_cmds
104
+ end
105
+ end
106
+
107
+ def stub_opensearch_unavailable(url="http://localhost:9200/_bulk")
108
+ stub_request(:post, url).to_return(:status => [503, "Service Unavailable"])
109
+ end
110
+
111
+ def stub_opensearch_timeout(url="http://localhost:9200/_bulk")
112
+ stub_request(:post, url).to_timeout
113
+ end
114
+
115
+ def stub_opensearch_with_store_index_command_counts(url="http://localhost:9200/_bulk")
116
+ if @index_command_counts == nil
117
+ @index_command_counts = {}
118
+ @index_command_counts.default = 0
119
+ end
120
+
121
+ stub_request(:post, url).with do |req|
122
+ index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
123
+ @index_command_counts[url] += index_cmds.size
124
+ end
125
+ end
126
+
127
+ def make_response_body(req, error_el = nil, error_status = nil, error = nil)
128
+ req_index_cmds = req.body.split("\n").map { |r| JSON.parse(r) }
129
+ items = []
130
+ count = 0
131
+ ids = 1
132
+ op = nil
133
+ index = nil
134
+ type = nil
135
+ id = nil
136
+ req_index_cmds.each do |cmd|
137
+ if count.even?
138
+ op = cmd.keys[0]
139
+ index = cmd[op]['_index']
140
+ type = cmd[op]['_type']
141
+ if cmd[op].has_key?('_id')
142
+ id = cmd[op]['_id']
143
+ else
144
+ # Note: this appears to be an undocumented feature of OpenSearch (and Elasticsearch)
145
+ # https://www.elastic.co/guide/en/elasticsearch/reference/2.4/docs-bulk.html
146
+ # When you submit an "index" write_operation, with no "_id" field in the
147
+ # metadata header, OpenSearch will turn this into a "create"
148
+ # operation in the response.
149
+ if "index" == op
150
+ op = "create"
151
+ end
152
+ id = ids
153
+ ids += 1
154
+ end
155
+ else
156
+ item = {
157
+ op => {
158
+ '_index' => index, '_type' => type, '_id' => id, '_version' => 1,
159
+ '_shards' => { 'total' => 1, 'successful' => 1, 'failed' => 0 },
160
+ 'status' => op == 'create' ? 201 : 200
161
+ }
162
+ }
163
+ items.push(item)
164
+ end
165
+ count += 1
166
+ end
167
+ if !error_el.nil? && !error_status.nil? && !error.nil?
168
+ op = items[error_el].keys[0]
169
+ items[error_el][op].delete('_version')
170
+ items[error_el][op].delete('_shards')
171
+ items[error_el][op]['error'] = error
172
+ items[error_el][op]['status'] = error_status
173
+ errors = true
174
+ else
175
+ errors = false
176
+ end
177
+ @index_cmds = items
178
+ body = { 'took' => 6, 'errors' => errors, 'items' => items }
179
+ return body.to_json
180
+ end
181
+
182
+ def stub_opensearch_bad_argument(url="http://localhost:9200/_bulk")
183
+ error = {
184
+ "type" => "mapper_parsing_exception",
185
+ "reason" => "failed to parse [...]",
186
+ "caused_by" => {
187
+ "type" => "illegal_argument_exception",
188
+ "reason" => "Invalid format: \"...\""
189
+ }
190
+ }
191
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 400, error), :headers => { 'Content-Type' => 'json' } } })
192
+ end
193
+
194
+ def stub_opensearch_bulk_error(url="http://localhost:9200/_bulk")
195
+ error = {
196
+ "type" => "some-unrecognized-error",
197
+ "reason" => "some message printed here ...",
198
+ }
199
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 500, error), :headers => { 'Content-Type' => 'json' } } })
200
+ end
201
+
202
+ def stub_opensearch_bulk_rejected(url="http://localhost:9200/_bulk")
203
+ error = {
204
+ "status" => 500,
205
+ "type" => "rejected_execution_exception",
206
+ "reason" => "rejected execution of org.opensearch.transport.TransportService$4@1a34d37a on OpenSearchThreadPoolExecutor[bulk, queue capacity = 50, org.opensearch.common.util.concurrent.OpenSearchThreadPoolExecutor@312a2162[Running, pool size = 32, active threads = 32, queued tasks = 50, completed tasks = 327053]]"
207
+ }
208
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 429, error), :headers => { 'Content-Type' => 'json' } } })
209
+ end
210
+
211
+ def stub_opensearch_out_of_memory(url="http://localhost:9200/_bulk")
212
+ error = {
213
+ "status" => 500,
214
+ "type" => "out_of_memory_error",
215
+ "reason" => "Java heap space"
216
+ }
217
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 500, error), :headers => { 'Content-Type' => 'json' } } })
218
+ end
219
+
220
+ def stub_opensearch_unexpected_response_op(url="http://localhost:9200/_bulk")
221
+ error = {
222
+ "category" => "some-other-type",
223
+ "reason" => "some-other-reason"
224
+ }
225
+ stub_request(:post, url).to_return(lambda { |req| bodystr = make_response_body(req, 0, 500, error); body = JSON.parse(bodystr); body['items'][0]['unknown'] = body['items'][0].delete('create'); { :status => 200, :body => body.to_json, :headers => { 'Content-Type' => 'json' } } })
226
+ end
227
+
228
+ def assert_logs_include(logs, msg, exp_matches=1)
229
+ matches = logs.grep(/#{msg}/)
230
+ assert_equal(exp_matches, matches.length, "Logs do not contain '#{msg}' '#{logs}'")
231
+ end
232
+
233
+ def assert_logs_include_compare_size(exp_matches=1, operator="<=", logs="", msg="")
234
+ matches = logs.grep(/#{msg}/)
235
+ assert_compare(exp_matches, operator, matches.length, "Logs do not contain '#{msg}' '#{logs}'")
236
+ end
237
+
238
+ def alias_endpoint
239
+ "_aliases"
240
+ end
241
+
242
+ def test_configure
243
+ config = %{
244
+ host logs.google.com
245
+ port 777
246
+ scheme https
247
+ path /os/
248
+ user john
249
+ password doe
250
+ }
251
+ instance = driver(config).instance
252
+
253
+ assert_equal 'logs.google.com', instance.host
254
+ assert_equal 777, instance.port
255
+ assert_equal :https, instance.scheme
256
+ assert_equal '/os/', instance.path
257
+ assert_equal 'john', instance.user
258
+ assert_equal 'doe', instance.password
259
+ assert_equal Fluent::Plugin::OpenSearchTLS::DEFAULT_VERSION, instance.ssl_version
260
+ assert_nil instance.ssl_max_version
261
+ assert_nil instance.ssl_min_version
262
+ if Fluent::Plugin::OpenSearchTLS::USE_TLS_MINMAX_VERSION
263
+ if defined?(OpenSSL::SSL::TLS1_3_VERSION)
264
+ assert_equal({max_version: OpenSSL::SSL::TLS1_3_VERSION, min_version: OpenSSL::SSL::TLS1_2_VERSION},
265
+ instance.ssl_version_options)
266
+ else
267
+ assert_equal({max_version: nil, min_version: OpenSSL::SSL::TLS1_2_VERSION},
268
+ instance.ssl_version_options)
269
+ end
270
+ else
271
+ assert_equal({version: Fluent::Plugin::OpensearchTLS::DEFAULT_VERSION},
272
+ instance.ssl_version_options)
273
+ end
274
+ assert_nil instance.client_key
275
+ assert_nil instance.client_cert
276
+ assert_nil instance.client_key_pass
277
+ assert_false instance.with_transporter_log
278
+ assert_equal "_doc", default_type_name
279
+ assert_equal :excon, instance.http_backend
280
+ assert_false instance.prefer_oj_serializer
281
+ assert_equal ["out_of_memory_error", "rejected_execution_exception"], instance.unrecoverable_error_types
282
+ assert_true instance.verify_os_version_at_startup
283
+ assert_equal Fluent::Plugin::OpenSearchOutput::DEFAULT_OPENSEARCH_VERSION, instance.default_opensearch_version
284
+ assert_false instance.log_os_400_reason
285
+ assert_equal(-1, Fluent::Plugin::OpenSearchOutput::DEFAULT_TARGET_BULK_BYTES)
286
+ assert_false instance.compression
287
+ assert_equal :no_compression, instance.compression_level
288
+ assert_true instance.http_backend_excon_nonblock
289
+
290
+ assert_nil instance.endpoint
291
+ end
292
+
293
+ test 'configure endpoint section' do
294
+ config = Fluent::Config::Element.new(
295
+ 'ROOT', '', {
296
+ '@type' => 'opensearch',
297
+ }, [
298
+ Fluent::Config::Element.new('endpoint', '', {
299
+ 'url' => "https://search-opensearch.aws.example.com/",
300
+ 'region' => "local",
301
+ 'access_key_id' => 'YOUR_AWESOME_KEY',
302
+ 'secret_access_key' => 'YOUR_AWESOME_SECRET',
303
+ }, []),
304
+ Fluent::Config::Element.new('buffer', 'tag', {}, [])
305
+
306
+ ])
307
+ instance = driver(config).instance
308
+
309
+ assert_equal "https://search-opensearch.aws.example.com", instance.endpoint.url
310
+ assert_equal "local", instance.endpoint.region
311
+ assert_equal "YOUR_AWESOME_KEY", instance.endpoint.access_key_id
312
+ assert_equal "YOUR_AWESOME_SECRET", instance.endpoint.secret_access_key
313
+ assert_nil instance.endpoint.assume_role_arn
314
+ assert_nil instance.endpoint.ecs_container_credentials_relative_uri
315
+ assert_equal "fluentd", instance.endpoint.assume_role_session_name
316
+ assert_nil instance.endpoint.assume_role_web_identity_token_file
317
+ assert_nil instance.endpoint.sts_credentials_region
318
+ end
319
+
320
+ test 'configure compression' do
321
+ config = %{
322
+ compression_level best_compression
323
+ }
324
+ instance = driver(config).instance
325
+
326
+ assert_equal true, instance.compression
327
+ end
328
+
329
+ test 'check compression strategy' do
330
+ config = %{
331
+ compression_level best_speed
332
+ }
333
+ instance = driver(config).instance
334
+
335
+ assert_equal Zlib::BEST_SPEED, instance.compression_strategy
336
+ end
337
+
338
+ test 'check content-encoding header with compression' do
339
+ config = %{
340
+ compression_level best_compression
341
+ }
342
+ instance = driver(config).instance
343
+
344
+ assert_equal nil, instance.client.transport.transport.options[:transport_options][:headers]["Content-Encoding"]
345
+
346
+ stub_request(:post, "http://localhost:9200/_bulk").
347
+ to_return(status: 200, body: "", headers: {})
348
+ stub_opensearch_info
349
+ driver.run(default_tag: 'test') do
350
+ driver.feed(sample_record)
351
+ end
352
+ compressable = instance.compressable_connection
353
+
354
+ assert_equal "gzip", instance.client(nil, compressable).transport.transport.options[:transport_options][:headers]["Content-Encoding"]
355
+ end
356
+
357
+ test 'check compression option is passed to transport' do
358
+ config = %{
359
+ compression_level best_compression
360
+ }
361
+ instance = driver(config).instance
362
+
363
+ assert_equal false, instance.client.transport.transport.options[:compression]
364
+
365
+ stub_request(:post, "http://localhost:9200/_bulk").
366
+ to_return(status: 200, body: "", headers: {})
367
+ stub_opensearch_info
368
+ driver.run(default_tag: 'test') do
369
+ driver.feed(sample_record)
370
+ end
371
+ compressable = instance.compressable_connection
372
+
373
+ assert_equal true, instance.client(nil, compressable).transport.transport.options[:compression]
374
+ end
375
+
376
+ test 'invalid specification of times of retrying template installation' do
377
+ config = %{
378
+ max_retry_putting_template -3
379
+ }
380
+ assert_raise(Fluent::ConfigError) {
381
+ driver(config)
382
+ }
383
+ end
384
+
385
+ test 'invalid specification of times of retrying get es version' do
386
+ config = %{
387
+ max_retry_get_os_version -3
388
+ }
389
+ assert_raise(Fluent::ConfigError) {
390
+ driver(config)
391
+ }
392
+ end
393
+
394
+ sub_test_case 'Check client.info response' do
395
+ def create_driver(conf='', os_version=1, client_version="\"1.20\"")
396
+ # For request stub to detect compatibility.
397
+ @client_version ||= client_version
398
+ @default_opensearch_version ||= os_version
399
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
400
+ def detect_os_major_version
401
+ @_os_info ||= client.info
402
+ begin
403
+ unless version = @_os_info.dig("version", "number")
404
+ version = @default_opensearch_version
405
+ end
406
+ rescue NoMethodError => e
407
+ log.warn "#{@_os_info} can not dig version information. Assuming OpenSearch #{@default_opensearch_version}", error: e
408
+ version = @default_opensearch_version
409
+ end
410
+ version.to_i
411
+ end
412
+ CODE
413
+
414
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
415
+ def client_library_version
416
+ #{@client_version}
417
+ end
418
+ CODE
419
+ @driver ||= Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput) {
420
+ # v0.12's test driver assume format definition. This simulates ObjectBufferedOutput format
421
+ if !defined?(Fluent::Plugin::Output)
422
+ def format(tag, time, record)
423
+ [time, record].to_msgpack
424
+ end
425
+ end
426
+ }.configure(conf)
427
+ end
428
+
429
+ def stub_opensearch_info_bad(url="http://localhost:9200/", version="6.4.2")
430
+ body ="{\"version\":{\"number\":\"#{version}\",\"build_flavor\":\"default\"},\"tagline\":\"You Know, for Search\"}"
431
+ stub_request(:get, url).to_return({:status => 200, :body => body, :headers => { 'Content-Type' => 'text/plain' } })
432
+ end
433
+
434
+ test 'handle invalid client.info' do
435
+ stub_opensearch_info_bad("https://logs.fluentd.com:24225/es//", "7.7.1")
436
+ config = %{
437
+ host logs.fluentd.com
438
+ port 24225
439
+ scheme https
440
+ path /es/
441
+ user john
442
+ password doe
443
+ default_elasticsearch_version 7
444
+ scheme https
445
+ @log_level info
446
+ }
447
+ assert_raise(NoMethodError) do
448
+ _d = create_driver(config, 1, "\"1.2.2\"")
449
+ end
450
+ end
451
+ end
452
+
453
+ sub_test_case 'Check TLS handshake stuck warning log' do
454
+ test 'warning TLS log' do
455
+ config = %{
456
+ scheme https
457
+ http_backend_excon_nonblock false
458
+ ssl_version TLSv1_2
459
+ @log_level info
460
+ }
461
+ driver(config)
462
+ logs = driver.logs
463
+ assert_logs_include(logs, /TLS handshake will be stucked with block connection.\n Consider to set `http_backend_excon_nonblock` as true\n/)
464
+ end
465
+ end
466
+
467
+ test 'Detected insecure security' do
468
+ config = %{
469
+ ssl_version TLSv1_1
470
+ @log_level warn
471
+ scheme https
472
+ }
473
+ driver(config, 6)
474
+ logs = driver.logs
475
+ assert_logs_include(logs, /Detected OpenSearch 1.x or above and enabled insecure security/, 1)
476
+ end
477
+
478
+ test 'Detected Elasticsearch 7 and secure security' do
479
+ config = %{
480
+ ssl_version TLSv1_2
481
+ @log_level warn
482
+ scheme https
483
+ }
484
+ driver(config, 7)
485
+ logs = driver.logs
486
+ assert_logs_include(logs, /Detected ES 6.x or above and enabled insecure security/, 0)
487
+ end
488
+
489
+ test 'Pass OpenSearch and client library are same' do
490
+ config = %{
491
+ @log_level warn
492
+ validate_client_version true
493
+ }
494
+ assert_nothing_raised do
495
+ driver(config, 1, "\"1.2.2\"")
496
+ end
497
+ end
498
+
499
+ test 'Detected Elasticsearch and client library mismatch' do
500
+ config = %{
501
+ @log_level warn
502
+ validate_client_version true
503
+ }
504
+ assert_raise_message(/Detected OpenSearch 1 but you use OpenSearch client 2.0/) do
505
+ driver(config, 1, "\"2.0.0\"")
506
+ end
507
+ end
508
+
509
+ sub_test_case "placeholder substitution needed?" do
510
+ data("host placeholder" => ["host", "host-${tag}.google.com"],
511
+ "index_name_placeholder" => ["index_name", "logstash-${tag}"],
512
+ "template_name_placeholder" => ["template_name", "logstash-${tag}"],
513
+ "customize_template" => ["customize_template", '{"<<TAG>>":"${tag}"}'],
514
+ "logstash_prefix_placeholder" => ["logstash_prefix", "fluentd-${tag}"],
515
+ "application_name_placeholder" => ["application_name", "fluentd-${tag}"],
516
+ )
517
+ test 'tag placeholder' do |data|
518
+ param, value = data
519
+ config = Fluent::Config::Element.new(
520
+ 'ROOT', '', {
521
+ '@type' => 'opensearch',
522
+ param => value
523
+ }, [
524
+ Fluent::Config::Element.new('buffer', 'tag', {}, [])
525
+ ])
526
+ driver(config)
527
+
528
+ assert_true driver.instance.placeholder_substitution_needed_for_template?
529
+ end
530
+
531
+
532
+ data("host placeholder" => ["host", "host-%Y%m%d.google.com"],
533
+ "index_name_placeholder" => ["index_name", "logstash-%Y%m%d"],
534
+ "template_name_placeholder" => ["template_name", "logstash-%Y%m%d"],
535
+ "customize_template" => ["customize_template", '{"<<TAG>>":"fluentd-%Y%m%d"}'],
536
+ "logstash_prefix_placeholder" => ["logstash_prefix", "fluentd-%Y%m%d"],
537
+ "application_name_placeholder" => ["application_name", "fluentd-%Y%m%d"],
538
+ )
539
+ test 'time placeholder' do |data|
540
+ param, value = data
541
+ config = Fluent::Config::Element.new(
542
+ 'ROOT', '', {
543
+ '@type' => 'opensearch',
544
+ param => value
545
+ }, [
546
+ Fluent::Config::Element.new('buffer', 'time', {
547
+ 'timekey' => '1d'
548
+ }, [])
549
+ ])
550
+ driver(config)
551
+
552
+ assert_true driver.instance.placeholder_substitution_needed_for_template?
553
+ end
554
+
555
+ data("host placeholder" => ["host", "host-${mykey}.google.com"],
556
+ "index_name_placeholder" => ["index_name", "logstash-${mykey}"],
557
+ "template_name_placeholder" => ["template_name", "logstash-${mykey}"],
558
+ "customize_template" => ["customize_template", '{"<<TAG>>":"${mykey}"}'],
559
+ "logstash_prefix_placeholder" => ["logstash_prefix", "fluentd-${mykey}"],
560
+ "logstash_dateformat_placeholder" => ["logstash_dateformat", "${mykey}"],
561
+ "application_name_placeholder" => ["application_name", "fluentd-${mykey}"],
562
+ )
563
+ test 'custom placeholder' do |data|
564
+ param, value = data
565
+ config = Fluent::Config::Element.new(
566
+ 'ROOT', '', {
567
+ '@type' => 'elasticsearch',
568
+ param => value
569
+ }, [
570
+ Fluent::Config::Element.new('buffer', 'mykey', {
571
+ 'chunk_keys' => 'mykey',
572
+ 'timekey' => '1d',
573
+ }, [])
574
+ ])
575
+ driver(config)
576
+
577
+ assert_true driver.instance.placeholder_substitution_needed_for_template?
578
+ end
579
+
580
+ data("host placeholder" => ["host", "host-${tag}.google.com"],
581
+ "index_name_placeholder" => ["index_name", "logstash-${es_index}-%Y%m%d"],
582
+ "template_name_placeholder" => ["template_name", "logstash-${tag}-%Y%m%d"],
583
+ "customize_template" => ["customize_template", '{"<<TAG>>":"${os_index}"}'],
584
+ "logstash_prefix_placeholder" => ["logstash_prefix", "fluentd-${os_index}-%Y%m%d"],
585
+ "logstash_dateformat_placeholder" => ["logstash_dateformat", "${os_index}"],
586
+ "application_name_placeholder" => ["application_name", "fluentd-${tag}-${os_index}-%Y%m%d"],
587
+ )
588
+ test 'mixed placeholder' do |data|
589
+ param, value = data
590
+ config = Fluent::Config::Element.new(
591
+ 'ROOT', '', {
592
+ '@type' => 'opensearch',
593
+ param => value
594
+ }, [
595
+ Fluent::Config::Element.new('buffer', 'tag,time,os_index', {
596
+ 'chunk_keys' => 'os_index',
597
+ 'timekey' => '1d',
598
+ }, [])
599
+ ])
600
+ driver(config)
601
+
602
+ assert_true driver.instance.placeholder_substitution_needed_for_template?
603
+ end
604
+ end
605
+
606
+ sub_test_case 'chunk_keys requirement' do
607
+ test 'tag in chunk_keys' do
608
+ assert_nothing_raised do
609
+ driver(Fluent::Config::Element.new(
610
+ 'ROOT', '', {
611
+ '@type' => 'opensearch',
612
+ 'host' => 'log.google.com',
613
+ 'port' => 777,
614
+ 'scheme' => 'https',
615
+ 'path' => '/os/',
616
+ 'user' => 'john',
617
+ 'password' => 'doe',
618
+ }, [
619
+ Fluent::Config::Element.new('buffer', 'tag', {
620
+ 'chunk_keys' => 'tag'
621
+ }, [])
622
+ ]
623
+ ))
624
+ end
625
+ end
626
+
627
+ test '_index in chunk_keys' do
628
+ assert_nothing_raised do
629
+ driver(Fluent::Config::Element.new(
630
+ 'ROOT', '', {
631
+ '@type' => 'opensearch',
632
+ 'host' => 'log.google.com',
633
+ 'port' => 777,
634
+ 'scheme' => 'https',
635
+ 'path' => '/os/',
636
+ 'user' => 'john',
637
+ 'password' => 'doe',
638
+ }, [
639
+ Fluent::Config::Element.new('buffer', '_index', {
640
+ 'chunk_keys' => '_index'
641
+ }, [])
642
+ ]
643
+ ))
644
+ end
645
+ end
646
+
647
+ test 'lack of tag and _index in chunk_keys' do
648
+ assert_raise_message(/'tag' or '_index' in chunk_keys is required./) do
649
+ driver(Fluent::Config::Element.new(
650
+ 'ROOT', '', {
651
+ '@type' => 'opensearch',
652
+ 'host' => 'log.google.com',
653
+ 'port' => 777,
654
+ 'scheme' => 'https',
655
+ 'path' => '/os/',
656
+ 'user' => 'john',
657
+ 'password' => 'doe',
658
+ }, [
659
+ Fluent::Config::Element.new('buffer', 'mykey', {
660
+ 'chunk_keys' => 'mykey'
661
+ }, [])
662
+ ]
663
+ ))
664
+ end
665
+ end
666
+ end
667
+
668
+ test 'Detected exclusive features which are host placeholder, template installation, and verify OpenSearch version at startup' do
669
+ cwd = File.dirname(__FILE__)
670
+ template_file = File.join(cwd, 'test_template.json')
671
+
672
+ assert_raise_message(/host placeholder, template installation, and verify OpenSearch version at startup are exclusive feature at same time./) do
673
+ config = %{
674
+ host logs-${tag}.google.com
675
+ port 777
676
+ scheme https
677
+ path /os/
678
+ user john
679
+ password doe
680
+ template_name logstash
681
+ template_file #{template_file}
682
+ verify_os_version_at_startup true
683
+ default_opensearch_version 1
684
+ }
685
+ driver(config)
686
+ end
687
+ end
688
+
689
+ class GetOpenSearchVersionTest < self
690
+ def create_driver(conf='', client_version="\"1.0\"")
691
+ # For request stub to detect compatibility.
692
+ @client_version ||= client_version
693
+ # Ensure original implementation existence.
694
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
695
+ def detect_os_major_version
696
+ @_os_info ||= client.info
697
+ unless version = @_os_info.dig("version", "number")
698
+ version = @default_opensearch_version
699
+ end
700
+ version.to_i
701
+ end
702
+ CODE
703
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
704
+ def client_library_version
705
+ #{@client_version}
706
+ end
707
+ CODE
708
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput).configure(conf)
709
+ end
710
+
711
+ def test_retry_get_os_version
712
+ config = %{
713
+ host logs.google.com
714
+ port 778
715
+ scheme https
716
+ path /os/
717
+ user john
718
+ password doe
719
+ verify_os_version_at_startup true
720
+ max_retry_get_os_version 3
721
+ }
722
+
723
+ connection_resets = 0
724
+ stub_request(:get, "https://logs.google.com:778/os//").
725
+ with(basic_auth: ['john', 'doe']) do |req|
726
+ connection_resets += 1
727
+ raise Faraday::ConnectionFailed, "Test message"
728
+ end
729
+
730
+ assert_raise(Fluent::Plugin::OpenSearchError::RetryableOperationExhaustedFailure) do
731
+ create_driver(config)
732
+ end
733
+
734
+ assert_equal(4, connection_resets)
735
+ end
736
+ end
737
+
738
+ class GetOpenSearchVersionWithFallbackTest < self
739
+ def create_driver(conf='', client_version="\"1.2\"")
740
+ # For request stub to detect compatibility.
741
+ @client_version ||= client_version
742
+ # Ensure original implementation existence.
743
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
744
+ def detect_os_major_version
745
+ @_os_info ||= client.info
746
+ unless version = @_os_info.dig("version", "number")
747
+ version = @default_opensearch_version
748
+ end
749
+ version.to_i
750
+ end
751
+ CODE
752
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
753
+ def client_library_version
754
+ #{@client_version}
755
+ end
756
+ CODE
757
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput).configure(conf)
758
+ end
759
+
760
+ data("OpenSearch 1" => ["1.2", 1])
761
+ def test_retry_get_os_version_without_fail_on_detecting_os_version_retry_exceeded(data)
762
+ client_version, os_major_version = data
763
+ config = %{
764
+ host logs.google.com
765
+ port 778
766
+ scheme https
767
+ path /os/
768
+ user john
769
+ password doe
770
+ verify_os_version_at_startup true
771
+ max_retry_get_os_version 2
772
+ fail_on_detecting_os_version_retry_exceed false
773
+ default_opensearch_version #{os_major_version}
774
+ @log_level info
775
+ }
776
+
777
+ connection_resets = 0
778
+ stub_request(:get, "https://logs.google.com:778/os//").
779
+ with(basic_auth: ['john', 'doe']) do |req|
780
+ connection_resets += 1
781
+ raise Faraday::ConnectionFailed, "Test message"
782
+ end
783
+
784
+ d = create_driver(config, client_version)
785
+
786
+ assert_equal os_major_version, d.instance.default_opensearch_version
787
+ assert_equal 3, connection_resets
788
+ assert_equal os_major_version, d.instance.instance_variable_get(:@last_seen_major_version)
789
+ end
790
+ end
791
+
792
+ data("legacy_template" => [true, "_template"],
793
+ "new_template" => [false, "_index_template"])
794
+ def test_template_already_present(data)
795
+ use_legacy_template_flag, endpoint = data
796
+ config = %{
797
+ host logs.google.com
798
+ port 777
799
+ scheme https
800
+ path /os/
801
+ user john
802
+ password doe
803
+ template_name logstash
804
+ template_file /abc123
805
+ use_legacy_template #{use_legacy_template_flag}
806
+ }
807
+
808
+ # connection start
809
+ stub_request(:head, "https://logs.google.com:777/os//").
810
+ with(basic_auth: ['john', 'doe']).
811
+ to_return(:status => 200, :body => "", :headers => {})
812
+ # check if template exists
813
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash").
814
+ with(basic_auth: ['john', 'doe']).
815
+ to_return(:status => 200, :body => "", :headers => {})
816
+ stub_opensearch_info("https://logs.google.com:777/os//")
817
+
818
+ driver(config)
819
+
820
+ assert_not_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash")
821
+ end
822
+
823
+ data("legacy_template" => [true, "_template"],
824
+ "new_template" => [false, "_index_template"])
825
+ def test_template_create(data)
826
+ use_legacy_template_flag, endpoint = data
827
+ cwd = File.dirname(__FILE__)
828
+ template_file = if use_legacy_template_flag
829
+ File.join(cwd, 'test_template.json')
830
+ else
831
+ File.join(cwd, 'test_index_template.json')
832
+ end
833
+
834
+ config = %{
835
+ host logs.google.com
836
+ port 777
837
+ scheme https
838
+ path /os/
839
+ user john
840
+ password doe
841
+ template_name logstash
842
+ template_file #{template_file}
843
+ use_legacy_template #{use_legacy_template_flag}
844
+ }
845
+
846
+ # connection start
847
+ stub_request(:head, "https://logs.google.com:777/os//").
848
+ with(basic_auth: ['john', 'doe']).
849
+ to_return(:status => 200, :body => "", :headers => {})
850
+ # check if template exists
851
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash").
852
+ with(basic_auth: ['john', 'doe']).
853
+ to_return(:status => 404, :body => "", :headers => {})
854
+ # creation
855
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash").
856
+ with(basic_auth: ['john', 'doe']).
857
+ to_return(:status => 200, :body => "", :headers => {})
858
+ stub_opensearch_info("https://logs.google.com:777/os//")
859
+
860
+ driver(config)
861
+
862
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash", times: 1)
863
+ end
864
+
865
+ data("legacy_template" => [true, "_template"],
866
+ "new_template" => [false, "_index_template"])
867
+ def test_custom_template_create(data)
868
+ use_legacy_template_flag, endpoint = data
869
+ cwd = File.dirname(__FILE__)
870
+ template_file = if use_legacy_template_flag
871
+ File.join(cwd, 'test_alias_template.json')
872
+ else
873
+ File.join(cwd, 'test_index_alias_template.json')
874
+ end
875
+
876
+ config = %{
877
+ host logs.google.com
878
+ port 777
879
+ scheme https
880
+ path /os/
881
+ user john
882
+ password doe
883
+ template_name myapp_alias_template
884
+ template_file #{template_file}
885
+ customize_template {"--appid--": "myapp-logs","--index_prefix--":"mylogs"}
886
+ use_legacy_template #{use_legacy_template_flag}
887
+ }
888
+
889
+ # connection start
890
+ stub_request(:head, "https://logs.google.com:777/os//").
891
+ with(basic_auth: ['john', 'doe']).
892
+ to_return(:status => 200, :body => "", :headers => {})
893
+ # check if template exists
894
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template").
895
+ with(basic_auth: ['john', 'doe']).
896
+ to_return(:status => 404, :body => "", :headers => {})
897
+ # creation
898
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template").
899
+ with(basic_auth: ['john', 'doe']).
900
+ to_return(:status => 200, :body => "", :headers => {})
901
+ stub_opensearch_info("https://logs.google.com:777/os//")
902
+
903
+ driver(config)
904
+
905
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template", times: 1)
906
+ end
907
+
908
+ data("legacy_template" => [true, "_template"],
909
+ "new_template" => [false, "_index_template"])
910
+ def test_custom_template_create_with_customize_template_related_placeholders(data)
911
+ use_legacy_template_flag, endpoint = data
912
+ cwd = File.dirname(__FILE__)
913
+ template_file = if use_legacy_template_flag
914
+ File.join(cwd, 'test_alias_template.json')
915
+ else
916
+ File.join(cwd, 'test_index_alias_template.json')
917
+ end
918
+
919
+ config = %{
920
+ host logs.google.com
921
+ port 777
922
+ scheme https
923
+ path /os/
924
+ user john
925
+ password doe
926
+ template_name myapp_alias_template-${tag}
927
+ template_file #{template_file}
928
+ customize_template {"--appid--": "${tag}-logs","--index_prefix--":"${tag}"}
929
+ use_legacy_template #{use_legacy_template_flag}
930
+ }
931
+
932
+ # connection start
933
+ stub_request(:head, "https://logs.google.com:777/os//").
934
+ with(basic_auth: ['john', 'doe']).
935
+ to_return(:status => 200, :body => "", :headers => {})
936
+ # check if template exists
937
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template-test.template").
938
+ with(basic_auth: ['john', 'doe']).
939
+ to_return(:status => 404, :body => "", :headers => {})
940
+ # creation
941
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template-test.template").
942
+ with(basic_auth: ['john', 'doe']).
943
+ to_return(:status => 200, :body => "", :headers => {})
944
+
945
+ stub_request(:put, "https://logs.google.com:777/os//%3Cfluentd-test-default-000001%3E").
946
+ to_return(status: 200, body: "", headers: {})
947
+
948
+ driver(config)
949
+
950
+ stub_opensearch("https://logs.google.com:777/os//_bulk")
951
+ stub_opensearch_info("https://logs.google.com:777/os//")
952
+ driver.run(default_tag: 'test.template') do
953
+ driver.feed(sample_record)
954
+ end
955
+
956
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template-test.template", times: 1)
957
+ end
958
+
959
+ data("legacy_template" => [true, "_template"],
960
+ "new_template" => [false, "_index_template"])
961
+ def test_custom_template_installation_for_host_placeholder(data)
962
+ use_legacy_template_flag, endpoint = data
963
+ cwd = File.dirname(__FILE__)
964
+ template_file = if use_legacy_template_flag
965
+ File.join(cwd, 'test_template.json')
966
+ else
967
+ File.join(cwd, 'test_index_template.json')
968
+ end
969
+
970
+ config = %{
971
+ host logs-${tag}.google.com
972
+ port 777
973
+ scheme https
974
+ path /os/
975
+ user john
976
+ password doe
977
+ template_name logstash
978
+ template_file #{template_file}
979
+ verify_os_version_at_startup false
980
+ default_elasticsearch_version 6
981
+ customize_template {"--appid--": "myapp-logs","--index_prefix--":"mylogs"}
982
+ use_legacy_template #{use_legacy_template_flag}
983
+ }
984
+
985
+ # connection start
986
+ stub_request(:head, "https://logs-test.google.com:777/os//").
987
+ with(basic_auth: ['john', 'doe']).
988
+ to_return(:status => 200, :body => "", :headers => {})
989
+ # check if template exists
990
+ stub_request(:get, "https://logs-test.google.com:777/os//#{endpoint}/logstash").
991
+ with(basic_auth: ['john', 'doe']).
992
+ to_return(:status => 404, :body => "", :headers => {})
993
+ stub_request(:put, "https://logs-test.google.com:777/os//#{endpoint}/logstash").
994
+ with(basic_auth: ['john', 'doe']).
995
+ to_return(status: 200, body: "", headers: {})
996
+
997
+ driver(config)
998
+
999
+ stub_opensearch("https://logs-test.google.com:777/os//_bulk")
1000
+ stub_opensearch_info("https://logs-test.google.com:777/os//")
1001
+ driver.run(default_tag: 'test') do
1002
+ driver.feed(sample_record)
1003
+ end
1004
+ end
1005
+
1006
+ data("legacy_template" => [true, "_template"],
1007
+ "new_template" => [false, "_index_template"])
1008
+ def test_template_overwrite(data)
1009
+ use_legacy_template_flag, endpoint = data
1010
+ cwd = File.dirname(__FILE__)
1011
+ template_file = if use_legacy_template_flag
1012
+ File.join(cwd, 'test_template.json')
1013
+ else
1014
+ File.join(cwd, 'test_index_template.json')
1015
+ end
1016
+
1017
+ config = %{
1018
+ host logs.google.com
1019
+ port 777
1020
+ scheme https
1021
+ path /os/
1022
+ user john
1023
+ password doe
1024
+ template_name logstash
1025
+ template_file #{template_file}
1026
+ template_overwrite true
1027
+ use_legacy_template #{use_legacy_template_flag}
1028
+ }
1029
+
1030
+ # connection start
1031
+ stub_request(:head, "https://logs.google.com:777/os//").
1032
+ with(basic_auth: ['john', 'doe']).
1033
+ to_return(:status => 200, :body => "", :headers => {})
1034
+ # check if template exists
1035
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash").
1036
+ with(basic_auth: ['john', 'doe']).
1037
+ to_return(:status => 200, :body => "", :headers => {})
1038
+ # creation
1039
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash").
1040
+ with(basic_auth: ['john', 'doe']).
1041
+ to_return(:status => 200, :body => "", :headers => {})
1042
+ stub_opensearch_info("https://logs.google.com:777/os//")
1043
+
1044
+ driver(config)
1045
+
1046
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash", times: 1)
1047
+ end
1048
+
1049
+ data("legacy_template" => [true, "_template"],
1050
+ "new_template" => [false, "_index_template"])
1051
+ def test_custom_template_overwrite(data)
1052
+ use_legacy_template_flag, endpoint = data
1053
+ cwd = File.dirname(__FILE__)
1054
+ template_file = if use_legacy_template_flag
1055
+ File.join(cwd, 'test_template.json')
1056
+ else
1057
+ File.join(cwd, 'test_index_template.json')
1058
+ end
1059
+
1060
+ config = %{
1061
+ host logs.google.com
1062
+ port 777
1063
+ scheme https
1064
+ path /os/
1065
+ user john
1066
+ password doe
1067
+ template_name myapp_alias_template
1068
+ template_file #{template_file}
1069
+ template_overwrite true
1070
+ customize_template {"--appid--": "myapp-logs","--index_prefix--":"mylogs"}
1071
+ use_legacy_template #{use_legacy_template_flag}
1072
+ }
1073
+
1074
+ # connection start
1075
+ stub_request(:head, "https://logs.google.com:777/os//").
1076
+ with(basic_auth: ['john', 'doe']).
1077
+ to_return(:status => 200, :body => "", :headers => {})
1078
+ # check if template exists
1079
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template").
1080
+ with(basic_auth: ['john', 'doe']).
1081
+ to_return(:status => 200, :body => "", :headers => {})
1082
+ # creation
1083
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template").
1084
+ with(basic_auth: ['john', 'doe']).
1085
+ to_return(:status => 200, :body => "", :headers => {})
1086
+ stub_opensearch_info("https://logs.google.com:777/os//")
1087
+
1088
+ driver(config)
1089
+
1090
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/myapp_alias_template", times: 1)
1091
+ end
1092
+
1093
+ def test_template_create_invalid_filename
1094
+ config = %{
1095
+ host logs.google.com
1096
+ port 777
1097
+ scheme https
1098
+ path /os/
1099
+ user john
1100
+ password doe
1101
+ template_name logstash
1102
+ template_file /abc123
1103
+ }
1104
+
1105
+ # connection start
1106
+ stub_request(:head, "https://logs.google.com:777/os//").
1107
+ with(basic_auth: ['john', 'doe']).
1108
+ to_return(:status => 200, :body => "", :headers => {})
1109
+ # check if template exists
1110
+ stub_request(:get, "https://logs.google.com:777/os//_template/logstash").
1111
+ with(basic_auth: ['john', 'doe']).
1112
+ to_return(:status => 404, :body => "", :headers => {})
1113
+ stub_opensearch_info("https://logs.google.com:777/os//")
1114
+
1115
+ assert_raise(RuntimeError) {
1116
+ driver(config)
1117
+ }
1118
+ end
1119
+
1120
+ data("legacy_template" => [true, "_template"],
1121
+ "new_template" => [false, "_index_template"])
1122
+ def test_template_create_for_host_placeholder(data)
1123
+ use_legacy_template_flag, endpoint = data
1124
+ cwd = File.dirname(__FILE__)
1125
+ template_file = if use_legacy_template_flag
1126
+ File.join(cwd, 'test_template.json')
1127
+ else
1128
+ File.join(cwd, 'test_index_template.json')
1129
+ end
1130
+
1131
+ config = %{
1132
+ host logs-${tag}.google.com
1133
+ port 777
1134
+ scheme https
1135
+ path /os/
1136
+ user john
1137
+ password doe
1138
+ template_name logstash
1139
+ template_file #{template_file}
1140
+ verify_os_version_at_startup false
1141
+ default_elasticsearch_version 6
1142
+ use_legacy_template #{use_legacy_template_flag}
1143
+ }
1144
+
1145
+ # connection start
1146
+ stub_request(:head, "https://logs-test.google.com:777/os//").
1147
+ with(basic_auth: ['john', 'doe']).
1148
+ to_return(:status => 200, :body => "", :headers => {})
1149
+ # check if template exists
1150
+ stub_request(:get, "https://logs-test.google.com:777/os//#{endpoint}/logstash").
1151
+ with(basic_auth: ['john', 'doe']).
1152
+ to_return(:status => 404, :body => "", :headers => {})
1153
+ stub_request(:put, "https://logs-test.google.com:777/os//#{endpoint}/logstash").
1154
+ with(basic_auth: ['john', 'doe']).
1155
+ to_return(status: 200, body: "", headers: {})
1156
+ stub_request(:post, "https://logs-test.google.com:777/os//_bulk").
1157
+ with(basic_auth: ['john', 'doe']).
1158
+ to_return(status: 200, body: "", headers: {})
1159
+
1160
+ driver(config)
1161
+
1162
+ stub_opensearch("https://logs-test.google.com:777/os//_bulk")
1163
+ stub_opensearch_info("https://logs-test.google.com:777/os//")
1164
+ driver.run(default_tag: 'test') do
1165
+ driver.feed(sample_record)
1166
+ end
1167
+ end
1168
+
1169
+ data("legacy_template" => [true, "_template"],
1170
+ "new_template" => [false, "_index_template"])
1171
+ def test_template_retry_install_fails(data)
1172
+ use_legacy_template_flag, endpoint = data
1173
+ cwd = File.dirname(__FILE__)
1174
+ template_file = if use_legacy_template_flag
1175
+ File.join(cwd, 'test_template.json')
1176
+ else
1177
+ File.join(cwd, 'test_index_template.json')
1178
+ end
1179
+
1180
+ config = %{
1181
+ host logs.google.com
1182
+ port 778
1183
+ scheme https
1184
+ path /os/
1185
+ user john
1186
+ password doe
1187
+ template_name logstash
1188
+ template_file #{template_file}
1189
+ max_retry_putting_template 3
1190
+ use_legacy_template #{use_legacy_template_flag}
1191
+ }
1192
+
1193
+ connection_resets = 0
1194
+ # check if template exists
1195
+ stub_request(:get, "https://logs.google.com:778/os//#{endpoint}/logstash")
1196
+ .with(basic_auth: ['john', 'doe']) do |req|
1197
+ connection_resets += 1
1198
+ raise Faraday::ConnectionFailed, "Test message"
1199
+ end
1200
+ stub_opensearch_info("https://logs.google.com:778/os//")
1201
+
1202
+ assert_raise(Fluent::Plugin::OpenSearchError::RetryableOperationExhaustedFailure) do
1203
+ driver(config)
1204
+ end
1205
+
1206
+ assert_equal(4, connection_resets)
1207
+ end
1208
+
1209
+ transport_errors_handled_separately = [OpenSearch::Transport::Transport::Errors::NotFound]
1210
+ transport_errors = OpenSearch::Transport::Transport::Errors.constants.map { |err| [err, OpenSearch::Transport::Transport::Errors.const_get(err)] }
1211
+ transport_errors_hash = Hash[transport_errors.select { |err| !transport_errors_handled_separately.include?(err[1]) } ]
1212
+
1213
+ data(transport_errors_hash)
1214
+ def test_template_retry_transport_errors(error)
1215
+ endpoint, use_legacy_template_flag = ["_index_template".freeze, false]
1216
+ cwd = File.dirname(__FILE__)
1217
+ template_file = File.join(cwd, 'test_index_template.json')
1218
+
1219
+ config = %{
1220
+ host logs.google.com
1221
+ port 778
1222
+ scheme https
1223
+ path /os/
1224
+ user john
1225
+ password doe
1226
+ template_name logstash
1227
+ template_file #{template_file}
1228
+ max_retry_putting_template 0
1229
+ use_legacy_template #{use_legacy_template_flag}
1230
+ }
1231
+
1232
+ retries = 0
1233
+ stub_request(:get, "https://logs.google.com:778/os//#{endpoint}/logstash")
1234
+ .with(basic_auth: ['john', 'doe']) do |req|
1235
+ retries += 1
1236
+ raise error
1237
+ end
1238
+ stub_opensearch_info("https://logs.google.com:778/os//")
1239
+
1240
+ assert_raise(Fluent::Plugin::OpenSearchError::RetryableOperationExhaustedFailure) do
1241
+ driver(config)
1242
+ end
1243
+
1244
+ assert_equal(1, retries)
1245
+ end
1246
+
1247
+ data("legacy_template" => [true, "_template"],
1248
+ "new_template" => [false, "_index_template"])
1249
+ def test_template_retry_install_does_not_fail(data)
1250
+ use_legacy_template_flag, endpoint = data
1251
+ cwd = File.dirname(__FILE__)
1252
+ template_file = if use_legacy_template_flag
1253
+ File.join(cwd, 'test_template.json')
1254
+ else
1255
+ File.join(cwd, 'test_index_template.json')
1256
+ end
1257
+
1258
+ config = %{
1259
+ host logs.google.com
1260
+ port 778
1261
+ scheme https
1262
+ path /os/
1263
+ user john
1264
+ password doe
1265
+ template_name logstash
1266
+ template_file #{template_file}
1267
+ max_retry_putting_template 3
1268
+ fail_on_putting_template_retry_exceed false
1269
+ use_legacy_template #{use_legacy_template_flag}
1270
+ }
1271
+
1272
+ connection_resets = 0
1273
+ # check if template exists
1274
+ stub_request(:get, "https://logs.google.com:778/os//#{endpoint}/logstash")
1275
+ .with(basic_auth: ['john', 'doe']) do |req|
1276
+ connection_resets += 1
1277
+ raise Faraday::ConnectionFailed, "Test message"
1278
+ end
1279
+ stub_opensearch_info("https://logs.google.com:778/os//")
1280
+
1281
+ driver(config)
1282
+
1283
+ assert_equal(4, connection_resets)
1284
+ end
1285
+
1286
+ data("legacy_template" => [true, "_template"],
1287
+ "new_template" => [false, "_index_template"])
1288
+ def test_templates_create(data)
1289
+ use_legacy_template_flag, endpoint = data
1290
+ cwd = File.dirname(__FILE__)
1291
+ template_file = if use_legacy_template_flag
1292
+ File.join(cwd, 'test_template.json')
1293
+ else
1294
+ File.join(cwd, 'test_index_template.json')
1295
+ end
1296
+
1297
+ config = %{
1298
+ host logs.google.com
1299
+ port 777
1300
+ scheme https
1301
+ path /os/
1302
+ user john
1303
+ password doe
1304
+ templates {"logstash1":"#{template_file}", "logstash2":"#{template_file}","logstash3":"#{template_file}" }
1305
+ use_legacy_template #{use_legacy_template_flag}
1306
+ }
1307
+
1308
+ stub_request(:head, "https://logs.google.com:777/os//").
1309
+ with(basic_auth: ['john', 'doe']).
1310
+ to_return(:status => 200, :body => "", :headers => {})
1311
+ # check if template exists
1312
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1313
+ with(basic_auth: ['john', 'doe']).
1314
+ to_return(:status => 404, :body => "", :headers => {})
1315
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1316
+ with(basic_auth: ['john', 'doe']).
1317
+ to_return(:status => 404, :body => "", :headers => {})
1318
+
1319
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash3").
1320
+ with(basic_auth: ['john', 'doe']).
1321
+ to_return(:status => 200, :body => "", :headers => {}) #exists
1322
+
1323
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1324
+ with(basic_auth: ['john', 'doe']).
1325
+ to_return(:status => 200, :body => "", :headers => {})
1326
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1327
+ with(basic_auth: ['john', 'doe']).
1328
+ to_return(:status => 200, :body => "", :headers => {})
1329
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash3").
1330
+ with(basic_auth: ['john', 'doe']).
1331
+ to_return(:status => 200, :body => "", :headers => {})
1332
+ stub_opensearch_info("https://logs.google.com:777/os//")
1333
+
1334
+ driver(config)
1335
+
1336
+ assert_requested( :put, "https://logs.google.com:777/os//#{endpoint}/logstash1", times: 1)
1337
+ assert_requested( :put, "https://logs.google.com:777/os//#{endpoint}/logstash2", times: 1)
1338
+ assert_not_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash3") #exists
1339
+ end
1340
+
1341
+ data("legacy_template" => [true, "_template"],
1342
+ "new_template" => [false, "_index_template"])
1343
+ def test_templates_overwrite(data)
1344
+ use_legacy_template_flag, endpoint = data
1345
+ cwd = File.dirname(__FILE__)
1346
+ template_file = if use_legacy_template_flag
1347
+ File.join(cwd, 'test_template.json')
1348
+ else
1349
+ File.join(cwd, 'test_index_template.json')
1350
+ end
1351
+
1352
+ config = %{
1353
+ host logs.google.com
1354
+ port 777
1355
+ scheme https
1356
+ path /os/
1357
+ user john
1358
+ password doe
1359
+ templates {"logstash1":"#{template_file}", "logstash2":"#{template_file}","logstash3":"#{template_file}" }
1360
+ template_overwrite true
1361
+ use_legacy_template #{use_legacy_template_flag}
1362
+ }
1363
+
1364
+ stub_request(:head, "https://logs.google.com:777/os//").
1365
+ with(basic_auth: ['john', 'doe']).
1366
+ to_return(:status => 200, :body => "", :headers => {})
1367
+ # check if template exists
1368
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1369
+ with(basic_auth: ['john', 'doe']).
1370
+ to_return(:status => 200, :body => "", :headers => {})
1371
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1372
+ with(basic_auth: ['john', 'doe']).
1373
+ to_return(:status => 200, :body => "", :headers => {})
1374
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash3").
1375
+ with(basic_auth: ['john', 'doe']).
1376
+ to_return(:status => 200, :body => "", :headers => {}) #exists
1377
+
1378
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1379
+ with(basic_auth: ['john', 'doe']).
1380
+ to_return(:status => 200, :body => "", :headers => {})
1381
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1382
+ with(basic_auth: ['john', 'doe']).
1383
+ to_return(:status => 200, :body => "", :headers => {})
1384
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash3").
1385
+ with(basic_auth: ['john', 'doe']).
1386
+ to_return(:status => 200, :body => "", :headers => {})
1387
+ stub_opensearch_info("https://logs.google.com:777/os//")
1388
+
1389
+ driver(config)
1390
+
1391
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1", times: 1)
1392
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2", times: 1)
1393
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash3", times: 1)
1394
+ end
1395
+
1396
+ data("legacy_template" => [true, "_template"],
1397
+ "new_template" => [false, "_index_template"])
1398
+ def test_templates_are_also_used(data)
1399
+ use_legacy_template_flag, endpoint = data
1400
+ cwd = File.dirname(__FILE__)
1401
+ template_file = if use_legacy_template_flag
1402
+ File.join(cwd, 'test_template.json')
1403
+ else
1404
+ File.join(cwd, 'test_index_template.json')
1405
+ end
1406
+
1407
+ config = %{
1408
+ host logs.google.com
1409
+ port 777
1410
+ scheme https
1411
+ path /os/
1412
+ user john
1413
+ password doe
1414
+ template_name logstash
1415
+ template_file #{template_file}
1416
+ templates {"logstash1":"#{template_file}", "logstash2":"#{template_file}" }
1417
+ use_legacy_template #{use_legacy_template_flag}
1418
+ }
1419
+ # connection start
1420
+ stub_request(:head, "https://logs.google.com:777/os//").
1421
+ with(basic_auth: ['john', 'doe']).
1422
+ to_return(:status => 200, :body => "", :headers => {})
1423
+ # check if template exists
1424
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash").
1425
+ with(basic_auth: ['john', 'doe']).
1426
+ to_return(:status => 404, :body => "", :headers => {})
1427
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1428
+ with(basic_auth: ['john', 'doe']).
1429
+ to_return(:status => 404, :body => "", :headers => {})
1430
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1431
+ with(basic_auth: ['john', 'doe']).
1432
+ to_return(:status => 404, :body => "", :headers => {})
1433
+ #creation
1434
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash").
1435
+ with(basic_auth: ['john', 'doe']).
1436
+ to_return(:status => 200, :body => "", :headers => {})
1437
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1438
+ with(basic_auth: ['john', 'doe']).
1439
+ to_return(:status => 200, :body => "", :headers => {})
1440
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1441
+ with(basic_auth: ['john', 'doe']).
1442
+ to_return(:status => 200, :body => "", :headers => {})
1443
+ stub_opensearch_info("https://logs.google.com:777/os//")
1444
+
1445
+ driver(config)
1446
+
1447
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash", times: 1)
1448
+
1449
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1")
1450
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2")
1451
+ end
1452
+
1453
+ data("legacy_template" => [true, "_template"],
1454
+ "new_template" => [false, "_index_template"])
1455
+ def test_templates_can_be_partially_created_if_error_occurs(data)
1456
+ use_legacy_template_flag, endpoint = data
1457
+ cwd = File.dirname(__FILE__)
1458
+ template_file = if use_legacy_template_flag
1459
+ File.join(cwd, 'test_template.json')
1460
+ else
1461
+ File.join(cwd, 'test_index_template.json')
1462
+ end
1463
+
1464
+ config = %{
1465
+ host logs.google.com
1466
+ port 777
1467
+ scheme https
1468
+ path /os/
1469
+ user john
1470
+ password doe
1471
+ templates {"logstash1":"#{template_file}", "logstash2":"/abc" }
1472
+ use_legacy_template #{use_legacy_template_flag}
1473
+ }
1474
+ stub_request(:head, "https://logs.google.com:777/os//").
1475
+ with(basic_auth: ['john', 'doe']).
1476
+ to_return(:status => 200, :body => "", :headers => {})
1477
+ # check if template exists
1478
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1479
+ with(basic_auth: ['john', 'doe']).
1480
+ to_return(:status => 404, :body => "", :headers => {})
1481
+ stub_request(:get, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1482
+ with(basic_auth: ['john', 'doe']).
1483
+ to_return(:status => 404, :body => "", :headers => {})
1484
+
1485
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1").
1486
+ with(basic_auth: ['john', 'doe']).
1487
+ to_return(:status => 200, :body => "", :headers => {})
1488
+ stub_request(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2").
1489
+ with(basic_auth: ['john', 'doe']).
1490
+ to_return(:status => 200, :body => "", :headers => {})
1491
+ stub_opensearch_info("https://logs.google.com:777/os//")
1492
+
1493
+ assert_raise(RuntimeError) {
1494
+ driver(config)
1495
+ }
1496
+
1497
+ assert_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash1", times: 1)
1498
+ assert_not_requested(:put, "https://logs.google.com:777/os//#{endpoint}/logstash2")
1499
+ end
1500
+
1501
+ def test_legacy_hosts_list
1502
+ config = %{
1503
+ hosts host1:50,host2:100,host3
1504
+ scheme https
1505
+ path /os/
1506
+ port 123
1507
+ }
1508
+ stub_opensearch_info("https://host1:50")
1509
+ stub_opensearch_info("https://host2:100")
1510
+ stub_opensearch_info("https://host3:123")
1511
+ instance = driver(config).instance
1512
+
1513
+ assert_equal 3, instance.get_connection_options[:hosts].length
1514
+ host1, host2, host3 = instance.get_connection_options[:hosts]
1515
+
1516
+ assert_equal 'host1', host1[:host]
1517
+ assert_equal 50, host1[:port]
1518
+ assert_equal 'https', host1[:scheme]
1519
+ assert_equal '/os/', host2[:path]
1520
+ assert_equal 'host3', host3[:host]
1521
+ assert_equal 123, host3[:port]
1522
+ assert_equal 'https', host3[:scheme]
1523
+ assert_equal '/os/', host3[:path]
1524
+ end
1525
+
1526
+ def test_hosts_list
1527
+ config = %{
1528
+ hosts https://john:password@host1:443/elastic/,http://host2
1529
+ path /default_path
1530
+ user default_user
1531
+ password default_password
1532
+ }
1533
+ stub_opensearch_info("https://john:password@host1:443/elastic/")
1534
+ stub_opensearch_info("http://host2")
1535
+ instance = driver(config).instance
1536
+
1537
+ assert_equal 2, instance.get_connection_options[:hosts].length
1538
+ host1, host2 = instance.get_connection_options[:hosts]
1539
+
1540
+ assert_equal 'host1', host1[:host]
1541
+ assert_equal 443, host1[:port]
1542
+ assert_equal 'https', host1[:scheme]
1543
+ assert_equal 'john', host1[:user]
1544
+ assert_equal 'password', host1[:password]
1545
+ assert_equal '/elastic/', host1[:path]
1546
+
1547
+ assert_equal 'host2', host2[:host]
1548
+ assert_equal 'http', host2[:scheme]
1549
+ assert_equal 'default_user', host2[:user]
1550
+ assert_equal 'default_password', host2[:password]
1551
+ assert_equal '/default_path', host2[:path]
1552
+ end
1553
+
1554
+ def test_hosts_list_with_escape_placeholders
1555
+ config = %{
1556
+ hosts https://%{j+hn}:%{passw@rd}@host1:443/elastic/,http://host2
1557
+ path /default_path
1558
+ user default_user
1559
+ password default_password
1560
+ }
1561
+ stub_opensearch_info("https://j%2Bhn:passw%40rd@host1:443/elastic/")
1562
+ stub_opensearch_info("http://host2")
1563
+
1564
+ instance = driver(config).instance
1565
+
1566
+ assert_equal 2, instance.get_connection_options[:hosts].length
1567
+ host1, host2 = instance.get_connection_options[:hosts]
1568
+
1569
+ assert_equal 'host1', host1[:host]
1570
+ assert_equal 443, host1[:port]
1571
+ assert_equal 'https', host1[:scheme]
1572
+ assert_equal 'j%2Bhn', host1[:user]
1573
+ assert_equal 'passw%40rd', host1[:password]
1574
+ assert_equal '/elastic/', host1[:path]
1575
+
1576
+ assert_equal 'host2', host2[:host]
1577
+ assert_equal 'http', host2[:scheme]
1578
+ assert_equal 'default_user', host2[:user]
1579
+ assert_equal 'default_password', host2[:password]
1580
+ assert_equal '/default_path', host2[:path]
1581
+ end
1582
+
1583
+ class IPv6AdressStringHostsTest < self
1584
+ def test_legacy_hosts_list
1585
+ config = %{
1586
+ hosts "[2404:7a80:d440:3000:192a:a292:bd7f:ca19]:50,host2:100,host3"
1587
+ scheme https
1588
+ path /os/
1589
+ port 123
1590
+ }
1591
+ instance = driver(config).instance
1592
+
1593
+ assert_raise(URI::InvalidURIError) do
1594
+ instance.get_connection_options[:hosts].length
1595
+ end
1596
+ end
1597
+
1598
+ def test_hosts_list
1599
+ config = %{
1600
+ hosts https://john:password@[2404:7a80:d440:3000:192a:a292:bd7f:ca19]:443/opensearch/,http://host2
1601
+ path /default_path
1602
+ user default_user
1603
+ password default_password
1604
+ }
1605
+ instance = driver(config).instance
1606
+
1607
+ assert_equal 2, instance.get_connection_options[:hosts].length
1608
+ host1, host2 = instance.get_connection_options[:hosts]
1609
+
1610
+ assert_equal '[2404:7a80:d440:3000:192a:a292:bd7f:ca19]', host1[:host]
1611
+ assert_equal 443, host1[:port]
1612
+ assert_equal 'https', host1[:scheme]
1613
+ assert_equal 'john', host1[:user]
1614
+ assert_equal 'password', host1[:password]
1615
+ assert_equal '/opensearch/', host1[:path]
1616
+
1617
+ assert_equal 'host2', host2[:host]
1618
+ assert_equal 'http', host2[:scheme]
1619
+ assert_equal 'default_user', host2[:user]
1620
+ assert_equal 'default_password', host2[:password]
1621
+ assert_equal '/default_path', host2[:path]
1622
+ end
1623
+
1624
+ def test_hosts_list_with_escape_placeholders
1625
+ config = %{
1626
+ hosts https://%{j+hn}:%{passw@rd}@[2404:7a80:d440:3000:192a:a292:bd7f:ca19]:443/opensearch/,http://host2
1627
+ path /default_path
1628
+ user default_user
1629
+ password default_password
1630
+ }
1631
+ instance = driver(config).instance
1632
+
1633
+ assert_equal 2, instance.get_connection_options[:hosts].length
1634
+ host1, host2 = instance.get_connection_options[:hosts]
1635
+
1636
+ assert_equal '[2404:7a80:d440:3000:192a:a292:bd7f:ca19]', host1[:host]
1637
+ assert_equal 443, host1[:port]
1638
+ assert_equal 'https', host1[:scheme]
1639
+ assert_equal 'j%2Bhn', host1[:user]
1640
+ assert_equal 'passw%40rd', host1[:password]
1641
+ assert_equal '/opensearch/', host1[:path]
1642
+
1643
+ assert_equal 'host2', host2[:host]
1644
+ assert_equal 'http', host2[:scheme]
1645
+ assert_equal 'default_user', host2[:user]
1646
+ assert_equal 'default_password', host2[:password]
1647
+ assert_equal '/default_path', host2[:path]
1648
+ end
1649
+ end
1650
+
1651
+ def test_single_host_params_and_defaults
1652
+ config = %{
1653
+ host logs.google.com
1654
+ user john
1655
+ password doe
1656
+ }
1657
+ instance = driver(config).instance
1658
+
1659
+ assert_equal 1, instance.get_connection_options[:hosts].length
1660
+ host1 = instance.get_connection_options[:hosts][0]
1661
+
1662
+ assert_equal 'logs.google.com', host1[:host]
1663
+ assert_equal 9200, host1[:port]
1664
+ assert_equal 'http', host1[:scheme]
1665
+ assert_equal 'john', host1[:user]
1666
+ assert_equal 'doe', host1[:password]
1667
+ assert_equal nil, host1[:path]
1668
+ end
1669
+
1670
+ def test_single_host_params_and_defaults_with_escape_placeholders
1671
+ config = %{
1672
+ host logs.google.com
1673
+ user %{j+hn}
1674
+ password %{d@e}
1675
+ }
1676
+ instance = driver(config).instance
1677
+
1678
+ assert_equal 1, instance.get_connection_options[:hosts].length
1679
+ host1 = instance.get_connection_options[:hosts][0]
1680
+
1681
+ assert_equal 'logs.google.com', host1[:host]
1682
+ assert_equal 9200, host1[:port]
1683
+ assert_equal 'http', host1[:scheme]
1684
+ assert_equal 'j%2Bhn', host1[:user]
1685
+ assert_equal 'd%40e', host1[:password]
1686
+ assert_equal nil, host1[:path]
1687
+ end
1688
+
1689
+ def test_host_and_port_are_ignored_if_specify_hosts
1690
+ config = %{
1691
+ host logs.google.com
1692
+ port 9200
1693
+ hosts host1:50,host2:100
1694
+ }
1695
+ instance = driver(config).instance
1696
+
1697
+ params = instance.get_connection_options[:hosts]
1698
+ hosts = params.map { |p| p[:host] }
1699
+ ports = params.map { |p| p[:port] }
1700
+ assert(hosts.none? { |h| h == 'logs.google.com' })
1701
+ assert(ports.none? { |p| p == 9200 })
1702
+ end
1703
+
1704
+ class IPv6AdressStringHostTest < self
1705
+ def test_single_host_params_and_defaults
1706
+ config = %{
1707
+ host 2404:7a80:d440:3000:192a:a292:bd7f:ca19
1708
+ user john
1709
+ password doe
1710
+ }
1711
+ instance = driver(config).instance
1712
+
1713
+ assert_equal 1, instance.get_connection_options[:hosts].length
1714
+ host1 = instance.get_connection_options[:hosts][0]
1715
+
1716
+ assert_equal '[2404:7a80:d440:3000:192a:a292:bd7f:ca19]', host1[:host]
1717
+ assert_equal 9200, host1[:port]
1718
+ assert_equal 'http', host1[:scheme]
1719
+ assert_equal 'john', host1[:user]
1720
+ assert_equal 'doe', host1[:password]
1721
+ assert_equal nil, host1[:path]
1722
+ end
1723
+
1724
+ def test_single_host_params_and_defaults_with_escape_placeholders
1725
+ config = %{
1726
+ host 2404:7a80:d440:3000:192a:a292:bd7f:ca19
1727
+ user %{j+hn}
1728
+ password %{d@e}
1729
+ }
1730
+ instance = driver(config).instance
1731
+
1732
+ assert_equal 1, instance.get_connection_options[:hosts].length
1733
+ host1 = instance.get_connection_options[:hosts][0]
1734
+
1735
+ assert_equal '[2404:7a80:d440:3000:192a:a292:bd7f:ca19]', host1[:host]
1736
+ assert_equal 9200, host1[:port]
1737
+ assert_equal 'http', host1[:scheme]
1738
+ assert_equal 'j%2Bhn', host1[:user]
1739
+ assert_equal 'd%40e', host1[:password]
1740
+ assert_equal nil, host1[:path]
1741
+ end
1742
+ end
1743
+
1744
+ def test_password_is_required_if_specify_user
1745
+ config = %{
1746
+ user john
1747
+ }
1748
+
1749
+ assert_raise(Fluent::ConfigError) do
1750
+ driver(config)
1751
+ end
1752
+ end
1753
+
1754
+ def test_content_type_header
1755
+ stub_request(:head, "http://localhost:9200/").
1756
+ to_return(:status => 200, :body => "", :headers => {})
1757
+ elastic_request = stub_request(:post, "http://localhost:9200/_bulk").
1758
+ with(headers: { "Content-Type" => "application/x-ndjson" })
1759
+ stub_opensearch_info
1760
+ driver.run(default_tag: 'test') do
1761
+ driver.feed(sample_record)
1762
+ end
1763
+ assert_requested(elastic_request)
1764
+ end
1765
+
1766
+ def test_custom_headers
1767
+ stub_request(:head, "http://localhost:9200/").
1768
+ to_return(:status => 200, :body => "", :headers => {})
1769
+ elastic_request = stub_request(:post, "http://localhost:9200/_bulk").
1770
+ with(headers: {'custom' => 'header1','and_others' => 'header2' })
1771
+ stub_opensearch_info
1772
+ driver.configure(%[custom_headers {"custom":"header1", "and_others":"header2"}])
1773
+ driver.run(default_tag: 'test') do
1774
+ driver.feed(sample_record)
1775
+ end
1776
+ assert_requested(elastic_request)
1777
+ end
1778
+
1779
+ def test_write_message_with_bad_chunk
1780
+ driver.configure("target_index_key bad_value\n@log_level debug\n")
1781
+ stub_opensearch
1782
+ stub_opensearch_info
1783
+ driver.run(default_tag: 'test') do
1784
+ driver.feed({'bad_value'=>"\255"})
1785
+ end
1786
+ error_log = driver.error_events.map {|e| e.last.message }
1787
+
1788
+ assert_logs_include(error_log, /(input string invalid)|(invalid byte sequence in UTF-8)/)
1789
+ end
1790
+
1791
+ data('OpenSearch 1' => [1, 'fluentd'],
1792
+ )
1793
+ def test_writes_to_default_index(data)
1794
+ version, index_name = data
1795
+ stub_opensearch
1796
+ stub_opensearch_info
1797
+ driver("", version)
1798
+ driver.run(default_tag: 'test') do
1799
+ driver.feed(sample_record)
1800
+ end
1801
+ assert_equal(index_name, index_cmds.first['index']['_index'])
1802
+ end
1803
+
1804
+ # gzip compress data
1805
+ def gzip(string, strategy)
1806
+ wio = StringIO.new("w")
1807
+ w_gz = Zlib::GzipWriter.new(wio, strategy = strategy)
1808
+ w_gz.write(string)
1809
+ w_gz.close
1810
+ wio.string
1811
+ end
1812
+
1813
+
1814
+ def test_writes_to_default_index_with_compression
1815
+ config = %[
1816
+ compression_level default_compression
1817
+ ]
1818
+
1819
+ bodystr = %({
1820
+ "took" : 500,
1821
+ "errors" : false,
1822
+ "items" : [
1823
+ {
1824
+ "create": {
1825
+ "_index" : "fluentd",
1826
+ "_type" : "fluentd"
1827
+ }
1828
+ }
1829
+ ]
1830
+ })
1831
+
1832
+ compressed_body = gzip(bodystr, Zlib::DEFAULT_COMPRESSION)
1833
+
1834
+ elastic_request = stub_request(:post, "http://localhost:9200/_bulk").
1835
+ to_return(:status => 200, :headers => {'Content-Type' => 'Application/json'}, :body => compressed_body)
1836
+ stub_opensearch_info("http://localhost:9200/")
1837
+
1838
+ driver(config)
1839
+ driver.run(default_tag: 'test') do
1840
+ driver.feed(sample_record)
1841
+ end
1842
+
1843
+ assert_requested(elastic_request)
1844
+ end
1845
+
1846
+ data('OpenSearch 1' => [1, Fluent::Plugin::OpenSearchOutput::DEFAULT_TYPE_NAME],
1847
+ )
1848
+ def test_writes_to_default_type(data)
1849
+ version, index_type = data
1850
+ stub_opensearch
1851
+ stub_opensearch_info
1852
+ driver("", version)
1853
+ driver.run(default_tag: 'test') do
1854
+ driver.feed(sample_record)
1855
+ end
1856
+ assert_equal(index_type, index_cmds.first['index']['_type'])
1857
+ end
1858
+
1859
+ def test_writes_to_speficied_index
1860
+ driver.configure("index_name myindex\n")
1861
+ stub_opensearch
1862
+ stub_opensearch_info
1863
+ driver.run(default_tag: 'test') do
1864
+ driver.feed(sample_record)
1865
+ end
1866
+ assert_equal('myindex', index_cmds.first['index']['_index'])
1867
+ end
1868
+
1869
+ def test_writes_with_huge_records
1870
+ driver.configure(Fluent::Config::Element.new(
1871
+ 'ROOT', '', {
1872
+ '@type' => 'opensearch',
1873
+ 'bulk_message_request_threshold' => 20 * 1024 * 1024,
1874
+ }, [
1875
+ Fluent::Config::Element.new('buffer', 'tag', {
1876
+ 'chunk_keys' => ['tag', 'time'],
1877
+ 'chunk_limit_size' => '64MB',
1878
+ }, [])
1879
+ ]
1880
+ ))
1881
+ request = stub_opensearch
1882
+ stub_opensearch_info
1883
+ driver.run(default_tag: 'test') do
1884
+ driver.feed(sample_record('huge_record' => ("a" * 20 * 1024 * 1024)))
1885
+ driver.feed(sample_record('huge_record' => ("a" * 20 * 1024 * 1024)))
1886
+ end
1887
+ assert_requested(request, times: 2)
1888
+ end
1889
+
1890
+ def test_writes_with_record_metadata
1891
+ chunk_id_key = "metadata_key".freeze
1892
+ driver.configure(Fluent::Config::Element.new(
1893
+ 'ROOT', '', {
1894
+ '@type' => 'opensearch',
1895
+ }, [
1896
+ Fluent::Config::Element.new('metadata', '', {
1897
+ 'include_chunk_id' => true,
1898
+ 'chunk_id_key' => chunk_id_key,
1899
+ }, [])
1900
+ ]
1901
+ ))
1902
+ stub_request(:post, "http://localhost:9200/_bulk").
1903
+ with(
1904
+ body: /{"index":{"_index":"fluentd","_type":"_doc"}}\n{"age":26,"request_id":"42","parent_id":"parent","routing_id":"routing","#{chunk_id_key}":".*"}\n/) do |req|
1905
+ @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
1906
+ end
1907
+ stub_opensearch_info
1908
+ driver.run(default_tag: 'test', shutdown: false) do
1909
+ driver.feed(sample_record)
1910
+ end
1911
+ assert_true index_cmds[1].has_key?(chunk_id_key)
1912
+ first_chunk_id = index_cmds[1].fetch(chunk_id_key)
1913
+
1914
+ driver.run(default_tag: 'test') do
1915
+ driver.feed(sample_record)
1916
+ end
1917
+ assert_true index_cmds[1].has_key?(chunk_id_key)
1918
+ second_chunk_id = index_cmds[1].fetch(chunk_id_key)
1919
+ assert do
1920
+ first_chunk_id != second_chunk_id
1921
+ end
1922
+ end
1923
+
1924
+ def test_writes_with_huge_records_but_uncheck
1925
+ driver.configure(Fluent::Config::Element.new(
1926
+ 'ROOT', '', {
1927
+ '@type' => 'opensearch',
1928
+ 'bulk_message_request_threshold' => -1,
1929
+ }, [
1930
+ Fluent::Config::Element.new('buffer', 'tag', {
1931
+ 'chunk_keys' => ['tag', 'time'],
1932
+ 'chunk_limit_size' => '64MB',
1933
+ }, [])
1934
+ ]
1935
+ ))
1936
+ request = stub_opensearch
1937
+ stub_opensearch_info
1938
+ driver.run(default_tag: 'test') do
1939
+ driver.feed(sample_record('huge_record' => ("a" * 20 * 1024 * 1024)))
1940
+ driver.feed(sample_record('huge_record' => ("a" * 20 * 1024 * 1024)))
1941
+ end
1942
+ assert_false(driver.instance.split_request?({}, nil))
1943
+ assert_requested(request, times: 1)
1944
+ end
1945
+
1946
+ class IndexNamePlaceholdersTest < self
1947
+ def test_writes_to_speficied_index_with_tag_placeholder
1948
+ driver.configure("index_name myindex.${tag}\n")
1949
+ stub_opensearch
1950
+ stub_opensearch_info
1951
+ driver.run(default_tag: 'test') do
1952
+ driver.feed(sample_record)
1953
+ end
1954
+ assert_equal('myindex.test', index_cmds.first['index']['_index'])
1955
+ end
1956
+
1957
+ def test_writes_to_speficied_index_with_time_placeholder
1958
+ driver.configure(Fluent::Config::Element.new(
1959
+ 'ROOT', '', {
1960
+ '@type' => 'opensearch',
1961
+ 'index_name' => 'myindex.%Y.%m.%d',
1962
+ }, [
1963
+ Fluent::Config::Element.new('buffer', 'tag,time', {
1964
+ 'chunk_keys' => ['tag', 'time'],
1965
+ 'timekey' => 3600,
1966
+ }, [])
1967
+ ]
1968
+ ))
1969
+ stub_opensearch
1970
+ stub_opensearch_info
1971
+ time = Time.parse Date.today.iso8601
1972
+ driver.run(default_tag: 'test') do
1973
+ driver.feed(time.to_i, sample_record)
1974
+ end
1975
+ assert_equal("myindex.#{time.utc.strftime("%Y.%m.%d")}", index_cmds.first['index']['_index'])
1976
+ end
1977
+
1978
+ def test_writes_to_speficied_index_with_custom_key_placeholder
1979
+ driver.configure(Fluent::Config::Element.new(
1980
+ 'ROOT', '', {
1981
+ '@type' => 'opensearch',
1982
+ 'index_name' => 'myindex.${pipeline_id}',
1983
+ }, [
1984
+ Fluent::Config::Element.new('buffer', 'tag,pipeline_id', {}, [])
1985
+ ]
1986
+ ))
1987
+ time = Time.parse Date.today.iso8601
1988
+ pipeline_id = "mypipeline"
1989
+ logstash_index = "myindex.#{pipeline_id}"
1990
+ stub_opensearch
1991
+ stub_opensearch_info
1992
+ driver.run(default_tag: 'test') do
1993
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => pipeline_id}))
1994
+ end
1995
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
1996
+ end
1997
+ end
1998
+
1999
+ def test_writes_to_speficied_index_uppercase
2000
+ driver.configure("index_name MyIndex\n")
2001
+ stub_opensearch
2002
+ stub_opensearch_info
2003
+ driver.run(default_tag: 'test') do
2004
+ driver.feed(sample_record)
2005
+ end
2006
+ # Allthough index_name has upper-case characters,
2007
+ # it should be set as lower-case when sent to elasticsearch.
2008
+ assert_equal('myindex', index_cmds.first['index']['_index'])
2009
+ end
2010
+
2011
+ def test_writes_to_target_index_key
2012
+ driver.configure("target_index_key @target_index\n")
2013
+ stub_opensearch
2014
+ stub_opensearch_info
2015
+ record = sample_record.clone
2016
+ driver.run(default_tag: 'test') do
2017
+ driver.feed(sample_record.merge('@target_index' => 'local-override'))
2018
+ end
2019
+ assert_equal('local-override', index_cmds.first['index']['_index'])
2020
+ assert_nil(index_cmds[1]['@target_index'])
2021
+ end
2022
+
2023
+ def test_writes_to_target_index_key_logstash
2024
+ driver.configure("target_index_key @target_index
2025
+ logstash_format true")
2026
+ time = Time.parse Date.today.iso8601
2027
+ stub_opensearch
2028
+ stub_opensearch_info
2029
+ driver.run(default_tag: 'test') do
2030
+ driver.feed(time.to_i, sample_record.merge('@target_index' => 'local-override'))
2031
+ end
2032
+ assert_equal('local-override', index_cmds.first['index']['_index'])
2033
+ end
2034
+
2035
+ def test_writes_to_target_index_key_logstash_uppercase
2036
+ driver.configure("target_index_key @target_index
2037
+ logstash_format true")
2038
+ time = Time.parse Date.today.iso8601
2039
+ stub_opensearch
2040
+ stub_opensearch_info
2041
+ driver.run(default_tag: 'test') do
2042
+ driver.feed(time.to_i, sample_record.merge('@target_index' => 'LOCAL-OVERRIDE'))
2043
+ end
2044
+ # Allthough @target_index has upper-case characters,
2045
+ # it should be set as lower-case when sent to elasticsearch.
2046
+ assert_equal('local-override', index_cmds.first['index']['_index'])
2047
+ end
2048
+
2049
+ def test_writes_to_default_index_with_pipeline
2050
+ pipeline = "fluentd"
2051
+ driver.configure("pipeline #{pipeline}")
2052
+ stub_opensearch
2053
+ stub_opensearch_info
2054
+ driver.run(default_tag: 'test') do
2055
+ driver.feed(sample_record)
2056
+ end
2057
+ assert_equal(pipeline, index_cmds.first['index']['pipeline'])
2058
+ end
2059
+
2060
+ def stub_opensearch_affinity_target_index_search_with_body(url="http://localhost:9200/logstash-*/_search", ids, return_body_str)
2061
+ # Note: ids used in query is unique list of ids
2062
+ stub_request(:post, url)
2063
+ .with(
2064
+ body: "{\"query\":{\"ids\":{\"values\":#{ids.uniq.to_json}}},\"_source\":false,\"sort\":[{\"_index\":{\"order\":\"desc\"}}]}",
2065
+ )
2066
+ .to_return(lambda do |req|
2067
+ { :status => 200,
2068
+ :headers => { 'Content-Type' => 'json' },
2069
+ :body => return_body_str
2070
+ }
2071
+ end)
2072
+ end
2073
+
2074
+ def stub_opensearch_affinity_target_index_search(url="http://localhost:9200/logstash-*/_search", ids, indices)
2075
+ # Example ids and indices arrays.
2076
+ # [ "3408a2c8eecd4fbfb82e45012b54fa82", "2816fc6ef4524b3f8f7e869002005433"]
2077
+ # [ "logstash-2021.04.28", "logstash-2021.04.29"]
2078
+ body = %({
2079
+ "took" : 31,
2080
+ "timed_out" : false,
2081
+ "_shards" : {
2082
+ "total" : 52,
2083
+ "successful" : 52,
2084
+ "skipped" : 48,
2085
+ "failed" : 0
2086
+ },
2087
+ "hits" : {
2088
+ "total" : {
2089
+ "value" : 356,
2090
+ "relation" : "eq"
2091
+ },
2092
+ "max_score" : null,
2093
+ "hits" : [
2094
+ {
2095
+ "_index" : "#{indices[0]}",
2096
+ "_type" : "_doc",
2097
+ "_id" : "#{ids[0]}",
2098
+ "_score" : null,
2099
+ "sort" : [
2100
+ "#{indices[0]}"
2101
+ ]
2102
+ },
2103
+ {
2104
+ "_index" : "#{indices[1]}",
2105
+ "_type" : "_doc",
2106
+ "_id" : "#{ids[1]}",
2107
+ "_score" : null,
2108
+ "sort" : [
2109
+ "#{indices[1]}"
2110
+ ]
2111
+ }
2112
+ ]
2113
+ }
2114
+ })
2115
+ stub_opensearch_affinity_target_index_search_with_body(ids, body)
2116
+ end
2117
+
2118
+ def stub_opensearch_affinity_target_index_search_return_empty(url="http://localhost:9200/logstash-*/_search", ids)
2119
+ empty_body = %({
2120
+ "took" : 5,
2121
+ "timed_out" : false,
2122
+ "_shards" : {
2123
+ "total" : 54,
2124
+ "successful" : 54,
2125
+ "skipped" : 53,
2126
+ "failed" : 0
2127
+ },
2128
+ "hits" : {
2129
+ "total" : {
2130
+ "value" : 0,
2131
+ "relation" : "eq"
2132
+ },
2133
+ "max_score" : null,
2134
+ "hits" : [ ]
2135
+ }
2136
+ })
2137
+ stub_opensearch_affinity_target_index_search_with_body(ids, empty_body)
2138
+ end
2139
+
2140
+ def test_writes_to_affinity_target_index
2141
+ driver.configure("target_index_affinity true
2142
+ logstash_format true
2143
+ id_key my_id
2144
+ write_operation update")
2145
+
2146
+ my_id_value = "3408a2c8eecd4fbfb82e45012b54fa82"
2147
+ ids = [my_id_value]
2148
+ indices = ["logstash-2021.04.28"]
2149
+ stub_opensearch
2150
+ stub_opensearch_info
2151
+ stub_opensearch_affinity_target_index_search(ids, indices)
2152
+ driver.run(default_tag: 'test') do
2153
+ driver.feed(sample_record('my_id' => my_id_value))
2154
+ end
2155
+ assert_equal('logstash-2021.04.28', index_cmds.first['update']['_index'])
2156
+ end
2157
+
2158
+ def test_writes_to_affinity_target_index_write_operation_upsert
2159
+ driver.configure("target_index_affinity true
2160
+ logstash_format true
2161
+ id_key my_id
2162
+ write_operation upsert")
2163
+
2164
+ my_id_value = "3408a2c8eecd4fbfb82e45012b54fa82"
2165
+ ids = [my_id_value]
2166
+ indices = ["logstash-2021.04.28"]
2167
+ stub_opensearch
2168
+ stub_opensearch_info
2169
+ stub_opensearch_affinity_target_index_search(ids, indices)
2170
+ driver.run(default_tag: 'test') do
2171
+ driver.feed(sample_record('my_id' => my_id_value))
2172
+ end
2173
+ assert_equal('logstash-2021.04.28', index_cmds.first['update']['_index'])
2174
+ end
2175
+
2176
+ def test_writes_to_affinity_target_index_index_not_exists_yet
2177
+ driver.configure("target_index_affinity true
2178
+ logstash_format true
2179
+ id_key my_id
2180
+ write_operation update")
2181
+
2182
+ my_id_value = "3408a2c8eecd4fbfb82e45012b54fa82"
2183
+ ids = [my_id_value]
2184
+ stub_opensearch
2185
+ stub_opensearch_info
2186
+ stub_opensearch_affinity_target_index_search_return_empty(ids)
2187
+ time = Time.parse Date.today.iso8601
2188
+ driver.run(default_tag: 'test') do
2189
+ driver.feed(time.to_i, sample_record('my_id' => my_id_value))
2190
+ end
2191
+ assert_equal("logstash-#{time.utc.strftime("%Y.%m.%d")}", index_cmds.first['update']['_index'])
2192
+ end
2193
+
2194
+ def test_writes_to_affinity_target_index_multiple_indices
2195
+ driver.configure("target_index_affinity true
2196
+ logstash_format true
2197
+ id_key my_id
2198
+ write_operation update")
2199
+
2200
+ my_id_value = "2816fc6ef4524b3f8f7e869002005433"
2201
+ my_id_value2 = "3408a2c8eecd4fbfb82e45012b54fa82"
2202
+ ids = [my_id_value, my_id_value2]
2203
+ indices = ["logstash-2021.04.29", "logstash-2021.04.28"]
2204
+ stub_opensearch_info
2205
+ stub_opensearch_all_requests
2206
+ stub_opensearch_affinity_target_index_search(ids, indices)
2207
+ driver.run(default_tag: 'test') do
2208
+ driver.feed(sample_record('my_id' => my_id_value))
2209
+ driver.feed(sample_record('my_id' => my_id_value2))
2210
+ end
2211
+ assert_equal(2, index_cmds_all_requests.count)
2212
+ assert_equal('logstash-2021.04.29', index_cmds_all_requests[0].first['update']['_index'])
2213
+ assert_equal(my_id_value, index_cmds_all_requests[0].first['update']['_id'])
2214
+ assert_equal('logstash-2021.04.28', index_cmds_all_requests[1].first['update']['_index'])
2215
+ assert_equal(my_id_value2, index_cmds_all_requests[1].first['update']['_id'])
2216
+ end
2217
+
2218
+ def test_writes_to_affinity_target_index_same_id_dublicated_write_to_oldest_index
2219
+ driver.configure("target_index_affinity true
2220
+ logstash_format true
2221
+ id_key my_id
2222
+ write_operation update")
2223
+
2224
+ my_id_value = "2816fc6ef4524b3f8f7e869002005433"
2225
+ # It may happen than same id has inserted to two index while data inserted during rollover period
2226
+ ids = [my_id_value, my_id_value]
2227
+ # Simulate the used sorting here, as search sorts indices in DESC order to pick only oldest index per single _id
2228
+ indices = ["logstash-2021.04.29", "logstash-2021.04.28"]
2229
+
2230
+ stub_opensearch_info
2231
+ stub_opensearch_all_requests
2232
+ stub_opensearch_affinity_target_index_search(ids, indices)
2233
+ driver.run(default_tag: 'test') do
2234
+ driver.feed(sample_record('my_id' => my_id_value))
2235
+ driver.feed(sample_record('my_id' => my_id_value))
2236
+ end
2237
+ assert_equal('logstash-2021.04.28', index_cmds.first['update']['_index'])
2238
+
2239
+ assert_equal(1, index_cmds_all_requests.count)
2240
+ assert_equal('logstash-2021.04.28', index_cmds_all_requests[0].first['update']['_index'])
2241
+ assert_equal(my_id_value, index_cmds_all_requests[0].first['update']['_id'])
2242
+ end
2243
+
2244
+ class PipelinePlaceholdersTest < self
2245
+ def test_writes_to_default_index_with_pipeline_tag_placeholder
2246
+ pipeline = "fluentd-${tag}"
2247
+ driver.configure("pipeline #{pipeline}")
2248
+ stub_opensearch
2249
+ stub_opensearch_info
2250
+ driver.run(default_tag: 'test.builtin.placeholder') do
2251
+ driver.feed(sample_record)
2252
+ end
2253
+ assert_equal("fluentd-test.builtin.placeholder", index_cmds.first['index']['pipeline'])
2254
+ end
2255
+
2256
+ def test_writes_to_default_index_with_pipeline_time_placeholder
2257
+ driver.configure(Fluent::Config::Element.new(
2258
+ 'ROOT', '', {
2259
+ '@type' => 'elasticsearch',
2260
+ 'pipeline' => 'fluentd-%Y%m%d',
2261
+ }, [
2262
+ Fluent::Config::Element.new('buffer', 'tag,time', {
2263
+ 'chunk_keys' => ['tag', 'time'],
2264
+ 'timekey' => 3600,
2265
+ }, [])
2266
+ ]
2267
+ ))
2268
+ time = Time.parse Date.today.iso8601
2269
+ pipeline = "fluentd-#{time.getutc.strftime("%Y%m%d")}"
2270
+ stub_opensearch
2271
+ stub_opensearch_info
2272
+ driver.run(default_tag: 'test') do
2273
+ driver.feed(time.to_i, sample_record)
2274
+ end
2275
+ assert_equal(pipeline, index_cmds.first['index']['pipeline'])
2276
+ end
2277
+
2278
+ def test_writes_to_default_index_with_pipeline_custom_key_placeholder
2279
+ driver.configure(Fluent::Config::Element.new(
2280
+ 'ROOT', '', {
2281
+ '@type' => 'elasticsearch',
2282
+ 'pipeline' => 'fluentd-${pipeline_id}',
2283
+ }, [
2284
+ Fluent::Config::Element.new('buffer', 'tag,pipeline_id', {}, [])
2285
+ ]
2286
+ ))
2287
+ time = Time.parse Date.today.iso8601
2288
+ pipeline_id = "mypipeline"
2289
+ logstash_index = "fluentd-#{pipeline_id}"
2290
+ stub_opensearch
2291
+ stub_opensearch_info
2292
+ driver.run(default_tag: 'test') do
2293
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => pipeline_id}))
2294
+ end
2295
+ assert_equal(logstash_index, index_cmds.first['index']['pipeline'])
2296
+ end
2297
+ end
2298
+
2299
+ def test_writes_to_target_index_key_fallack
2300
+ driver.configure("target_index_key @target_index\n")
2301
+ stub_opensearch
2302
+ stub_opensearch_info
2303
+ driver.run(default_tag: 'test') do
2304
+ driver.feed(sample_record)
2305
+ end
2306
+ assert_equal('fluentd', index_cmds.first['index']['_index'])
2307
+ end
2308
+
2309
+ def test_writes_to_target_index_key_fallack_logstash
2310
+ driver.configure("target_index_key @target_index\n
2311
+ logstash_format true")
2312
+ time = Time.parse Date.today.iso8601
2313
+ logstash_index = "logstash-#{time.getutc.strftime("%Y.%m.%d")}"
2314
+ stub_opensearch
2315
+ stub_opensearch_info
2316
+ driver.run(default_tag: 'test') do
2317
+ driver.feed(time.to_i, sample_record)
2318
+ end
2319
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2320
+ end
2321
+
2322
+ data(
2323
+ "OpenSearch default"=> {"os_version" => 1, "_type" => "_doc"},
2324
+ )
2325
+ def test_writes_to_speficied_type(data)
2326
+ driver('', data["os_version"]).configure("")
2327
+ stub_opensearch
2328
+ stub_opensearch_info
2329
+ driver.run(default_tag: 'test') do
2330
+ driver.feed(sample_record)
2331
+ end
2332
+ assert_equal(data['_type'], index_cmds.first['index']['_type'])
2333
+ end
2334
+
2335
+ data(
2336
+ "OpenSearch 1"=> {"os_version" => 1, "_type" => "_doc"},
2337
+ )
2338
+ def test_writes_to_speficied_type_with_placeholders(data)
2339
+ driver('', data["os_version"]).configure("")
2340
+ stub_opensearch
2341
+ stub_opensearch_info
2342
+ driver.run(default_tag: 'test') do
2343
+ driver.feed(sample_record)
2344
+ end
2345
+ assert_equal(data['_type'], index_cmds.first['index']['_type'])
2346
+ end
2347
+
2348
+ def test_writes_to_speficied_host
2349
+ driver.configure("host 192.168.33.50\n")
2350
+ elastic_request = stub_opensearch("http://192.168.33.50:9200/_bulk")
2351
+ stub_opensearch_info("http://192.168.33.50:9200/")
2352
+ driver.run(default_tag: 'test') do
2353
+ driver.feed(sample_record)
2354
+ end
2355
+ assert_requested(elastic_request)
2356
+ end
2357
+
2358
+ def test_writes_to_speficied_port
2359
+ driver.configure("port 9201\n")
2360
+ elastic_request = stub_opensearch("http://localhost:9201/_bulk")
2361
+ stub_opensearch_info("http://localhost:9201")
2362
+ driver.run(default_tag: 'test') do
2363
+ driver.feed(sample_record)
2364
+ end
2365
+ assert_requested(elastic_request)
2366
+ end
2367
+
2368
+ def test_writes_to_multi_hosts
2369
+ hosts = [['192.168.33.50', 9201], ['192.168.33.51', 9201], ['192.168.33.52', 9201]]
2370
+ hosts_string = hosts.map {|x| "#{x[0]}:#{x[1]}"}.compact.join(',')
2371
+
2372
+ driver.configure("hosts #{hosts_string}")
2373
+
2374
+ hosts.each do |host_info|
2375
+ host, port = host_info
2376
+ stub_opensearch_with_store_index_command_counts("http://#{host}:#{port}/_bulk")
2377
+ stub_opensearch_info("http://#{host}:#{port}/")
2378
+ end
2379
+
2380
+ driver.run(default_tag: 'test') do
2381
+ 1000.times do
2382
+ driver.feed(sample_record.merge('age'=>rand(100)))
2383
+ end
2384
+ end
2385
+
2386
+ # @note: we cannot make multi chunks with options (flush_interval, buffer_chunk_limit)
2387
+ # it's Fluentd test driver's constraint
2388
+ # so @index_command_counts.size is always 1
2389
+
2390
+ assert(@index_command_counts.size > 0, "not working with hosts options")
2391
+
2392
+ total = 0
2393
+ @index_command_counts.each do |url, count|
2394
+ total += count
2395
+ end
2396
+ assert_equal(2000, total)
2397
+ end
2398
+
2399
+ def test_nested_record_with_flattening_on
2400
+ driver.configure("flatten_hashes true
2401
+ flatten_hashes_separator |")
2402
+
2403
+ original_hash = {"foo" => {"bar" => "baz"}, "people" => [
2404
+ {"age" => "25", "height" => "1ft"},
2405
+ {"age" => "30", "height" => "2ft"}
2406
+ ]}
2407
+
2408
+ expected_output = {"foo|bar"=>"baz", "people" => [
2409
+ {"age" => "25", "height" => "1ft"},
2410
+ {"age" => "30", "height" => "2ft"}
2411
+ ]}
2412
+
2413
+ stub_opensearch
2414
+ stub_opensearch_info
2415
+ driver.run(default_tag: 'test') do
2416
+ driver.feed(original_hash)
2417
+ end
2418
+ assert_equal expected_output, index_cmds[1]
2419
+ end
2420
+
2421
+ def test_nested_record_with_flattening_off
2422
+ # flattening off by default
2423
+
2424
+ original_hash = {"foo" => {"bar" => "baz"}}
2425
+ expected_output = {"foo" => {"bar" => "baz"}}
2426
+
2427
+ stub_opensearch
2428
+ stub_opensearch_info
2429
+ driver.run(default_tag: 'test') do
2430
+ driver.feed(original_hash)
2431
+ end
2432
+ assert_equal expected_output, index_cmds[1]
2433
+ end
2434
+
2435
+ def test_makes_bulk_request
2436
+ stub_opensearch
2437
+ stub_opensearch_info
2438
+ driver.run(default_tag: 'test') do
2439
+ driver.feed(sample_record)
2440
+ driver.feed(sample_record.merge('age' => 27))
2441
+ end
2442
+ assert_equal(4, index_cmds.count)
2443
+ end
2444
+
2445
+ def test_all_re
2446
+ stub_opensearch
2447
+ stub_opensearch_info
2448
+ driver.run(default_tag: 'test') do
2449
+ driver.feed(sample_record)
2450
+ driver.feed(sample_record.merge('age' => 27))
2451
+ end
2452
+ assert_equal(26, index_cmds[1]['age'])
2453
+ assert_equal(27, index_cmds[3]['age'])
2454
+ end
2455
+
2456
+ def test_writes_to_logstash_index
2457
+ driver.configure("logstash_format true\n")
2458
+ #
2459
+ # This is 1 second past midnight in BST, so the UTC index should be the day before
2460
+ dt = DateTime.new(2015, 6, 1, 0, 0, 1, "+01:00")
2461
+ logstash_index = "logstash-2015.05.31"
2462
+ stub_opensearch
2463
+ stub_opensearch_info
2464
+ driver.run(default_tag: 'test') do
2465
+ driver.feed(dt.to_time.to_i, sample_record)
2466
+ end
2467
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2468
+ end
2469
+
2470
+ def test_writes_to_logstash_non_utc_index
2471
+ driver.configure("logstash_format true
2472
+ utc_index false")
2473
+ # When using `utc_index false` the index time will be the local day of
2474
+ # ingestion time
2475
+ time = Date.today.to_time
2476
+ index = "logstash-#{time.strftime("%Y.%m.%d")}"
2477
+ stub_opensearch
2478
+ stub_opensearch_info
2479
+ driver.run(default_tag: 'test') do
2480
+ driver.feed(time.to_i, sample_record)
2481
+ end
2482
+ assert_equal(index, index_cmds.first['index']['_index'])
2483
+ end
2484
+
2485
+ def test_writes_to_logstash_index_with_specified_prefix
2486
+ driver.configure("logstash_format true
2487
+ logstash_prefix myprefix")
2488
+ time = Time.parse Date.today.iso8601
2489
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m.%d")}"
2490
+ stub_opensearch
2491
+ stub_opensearch_info
2492
+ driver.run(default_tag: 'test') do
2493
+ driver.feed(time.to_i, sample_record)
2494
+ end
2495
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2496
+ end
2497
+
2498
+ def test_writes_to_logstash_index_with_specified_prefix_and_separator
2499
+ separator = '_'
2500
+ driver.configure("logstash_format true
2501
+ logstash_prefix_separator #{separator}
2502
+ logstash_prefix myprefix")
2503
+ time = Time.parse Date.today.iso8601
2504
+ logstash_index = "myprefix#{separator}#{time.getutc.strftime("%Y.%m.%d")}"
2505
+ stub_opensearch
2506
+ stub_opensearch_info
2507
+ driver.run(default_tag: 'test') do
2508
+ driver.feed(time.to_i, sample_record)
2509
+ end
2510
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2511
+ end
2512
+
2513
+ class LogStashPrefixPlaceholdersTest < self
2514
+ def test_writes_to_logstash_index_with_specified_prefix_and_tag_placeholder
2515
+ driver.configure("logstash_format true
2516
+ logstash_prefix myprefix-${tag}")
2517
+ time = Time.parse Date.today.iso8601
2518
+ logstash_index = "myprefix-test-#{time.getutc.strftime("%Y.%m.%d")}"
2519
+ stub_opensearch
2520
+ stub_opensearch_info
2521
+ driver.run(default_tag: 'test') do
2522
+ driver.feed(time.to_i, sample_record)
2523
+ end
2524
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2525
+ end
2526
+
2527
+ def test_writes_to_logstash_index_with_specified_prefix_and_time_placeholder
2528
+ driver.configure(Fluent::Config::Element.new(
2529
+ 'ROOT', '', {
2530
+ '@type' => 'opensearch',
2531
+ 'logstash_format' => true,
2532
+ 'logstash_prefix' => 'myprefix-%H',
2533
+ }, [
2534
+ Fluent::Config::Element.new('buffer', 'tag,time', {
2535
+ 'chunk_keys' => ['tag', 'time'],
2536
+ 'timekey' => 3600,
2537
+ }, [])
2538
+ ]
2539
+ ))
2540
+ time = Time.parse Date.today.iso8601
2541
+ logstash_index = "myprefix-#{time.getutc.strftime("%H")}-#{time.getutc.strftime("%Y.%m.%d")}"
2542
+ stub_opensearch
2543
+ stub_opensearch_info
2544
+ driver.run(default_tag: 'test') do
2545
+ driver.feed(time.to_i, sample_record)
2546
+ end
2547
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2548
+ end
2549
+
2550
+ def test_writes_to_logstash_index_with_specified_prefix_and_custom_key_placeholder
2551
+ driver.configure(Fluent::Config::Element.new(
2552
+ 'ROOT', '', {
2553
+ '@type' => 'opensearch',
2554
+ 'logstash_format' => true,
2555
+ 'logstash_prefix' => 'myprefix-${pipeline_id}',
2556
+ }, [
2557
+ Fluent::Config::Element.new('buffer', 'tag,pipeline_id', {}, [])
2558
+ ]
2559
+ ))
2560
+ time = Time.parse Date.today.iso8601
2561
+ pipeline_id = "mypipeline"
2562
+ logstash_index = "myprefix-#{pipeline_id}-#{time.getutc.strftime("%Y.%m.%d")}"
2563
+ stub_opensearch
2564
+ stub_opensearch_info
2565
+ driver.run(default_tag: 'test') do
2566
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => pipeline_id}))
2567
+ end
2568
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2569
+ end
2570
+ end
2571
+
2572
+ class LogStashDateformatPlaceholdersTest < self
2573
+ def test_writes_to_logstash_index_with_specified_prefix_and_dateformat_placeholder_pattern_1
2574
+ driver.configure(Fluent::Config::Element.new(
2575
+ 'ROOT', '', {
2576
+ '@type' => 'opensearch',
2577
+ 'logstash_format' => true,
2578
+ 'logstash_dateformat' => '${indexformat}',
2579
+ 'logstash_prefix' => 'myprefix',
2580
+ }, [
2581
+ Fluent::Config::Element.new('buffer', 'tag,time,indexformat', {
2582
+ 'chunk_keys' => ['tag', 'time', 'indexformat'],
2583
+ 'timekey' => 3600,
2584
+ }, [])
2585
+ ]
2586
+ ))
2587
+ time = Time.parse Date.today.iso8601
2588
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m.%d")}"
2589
+ stub_opensearch
2590
+ stub_opensearch_info
2591
+ driver.run(default_tag: 'test') do
2592
+ driver.feed(time.to_i, sample_record.merge('indexformat' => '%Y.%m.%d'))
2593
+ end
2594
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2595
+ end
2596
+
2597
+ def test_writes_to_logstash_index_with_specified_prefix_and_dateformat_placeholder_pattern_2
2598
+ driver.configure(Fluent::Config::Element.new(
2599
+ 'ROOT', '', {
2600
+ '@type' => 'opensearch',
2601
+ 'logstash_format' => true,
2602
+ 'logstash_dateformat' => '${indexformat}',
2603
+ 'logstash_prefix' => 'myprefix',
2604
+ }, [
2605
+ Fluent::Config::Element.new('buffer', 'tag,time,indexformat', {
2606
+ 'chunk_keys' => ['tag', 'time', 'indexformat'],
2607
+ 'timekey' => 3600,
2608
+ }, [])
2609
+ ]
2610
+ ))
2611
+ time = Time.parse Date.today.iso8601
2612
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m")}"
2613
+ stub_opensearch
2614
+ stub_opensearch_info
2615
+ driver.run(default_tag: 'test') do
2616
+ driver.feed(time.to_i, sample_record.merge('indexformat' => '%Y.%m'))
2617
+ end
2618
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2619
+ end
2620
+ end
2621
+
2622
+ class HostnamePlaceholders < self
2623
+ def test_writes_to_extracted_host
2624
+ driver.configure("host ${tag}\n")
2625
+ time = Time.parse Date.today.iso8601
2626
+ elastic_request = stub_opensearch("http://extracted-host:9200/_bulk")
2627
+ stub_opensearch_info("http://extracted-host:9200/")
2628
+ driver.run(default_tag: 'extracted-host') do
2629
+ driver.feed(time.to_i, sample_record)
2630
+ end
2631
+ assert_requested(elastic_request)
2632
+ end
2633
+
2634
+ def test_writes_to_multi_hosts_with_placeholders
2635
+ hosts = [['${tag}', 9201], ['192.168.33.51', 9201], ['192.168.33.52', 9201]]
2636
+ hosts_string = hosts.map {|x| "#{x[0]}:#{x[1]}"}.compact.join(',')
2637
+
2638
+ driver.configure("hosts #{hosts_string}")
2639
+
2640
+ hosts.each do |host_info|
2641
+ host, port = host_info
2642
+ host = "extracted-host" if host == '${tag}'
2643
+ stub_opensearch_with_store_index_command_counts("http://#{host}:#{port}/_bulk")
2644
+ stub_opensearch_info("http://#{host}:#{port}")
2645
+ end
2646
+
2647
+ driver.run(default_tag: 'extracted-host') do
2648
+ 1000.times do
2649
+ driver.feed(sample_record.merge('age'=>rand(100)))
2650
+ end
2651
+ end
2652
+
2653
+ # @note: we cannot make multi chunks with options (flush_interval, buffer_chunk_limit)
2654
+ # it's Fluentd test driver's constraint
2655
+ # so @index_command_counts.size is always 1
2656
+
2657
+ assert(@index_command_counts.size > 0, "not working with hosts options")
2658
+
2659
+ total = 0
2660
+ @index_command_counts.each do |url, count|
2661
+ total += count
2662
+ end
2663
+ assert_equal(2000, total)
2664
+ end
2665
+
2666
+ def test_writes_to_extracted_host_with_time_placeholder
2667
+ driver.configure(Fluent::Config::Element.new(
2668
+ 'ROOT', '', {
2669
+ '@type' => 'elasticsearch',
2670
+ 'host' => 'host-%Y%m%d',
2671
+ }, [
2672
+ Fluent::Config::Element.new('buffer', 'tag,time', {
2673
+ 'chunk_keys' => ['tag', 'time'],
2674
+ 'timekey' => 3600,
2675
+ }, [])
2676
+ ]
2677
+ ))
2678
+ stub_opensearch
2679
+ stub_opensearch_info
2680
+ time = Time.parse Date.today.iso8601
2681
+ elastic_request = stub_opensearch("http://host-#{time.utc.strftime('%Y%m%d')}:9200/_bulk")
2682
+ stub_opensearch_info("http://host-#{time.utc.strftime('%Y%m%d')}:9200/")
2683
+ driver.run(default_tag: 'test') do
2684
+ driver.feed(time.to_i, sample_record)
2685
+ end
2686
+ assert_requested(elastic_request)
2687
+ end
2688
+
2689
+ def test_writes_to_extracted_host_with_custom_key_placeholder
2690
+ driver.configure(Fluent::Config::Element.new(
2691
+ 'ROOT', '', {
2692
+ '@type' => 'opensearch',
2693
+ 'host' => 'myhost-${pipeline_id}',
2694
+ }, [
2695
+ Fluent::Config::Element.new('buffer', 'tag,pipeline_id', {}, [])
2696
+ ]
2697
+ ))
2698
+ time = Time.parse Date.today.iso8601
2699
+ first_pipeline_id = "1"
2700
+ second_pipeline_id = "2"
2701
+ first_request = stub_opensearch("http://myhost-1:9200/_bulk")
2702
+ second_request = stub_opensearch("http://myhost-2:9200/_bulk")
2703
+ stub_opensearch_info("http://myhost-1:9200/")
2704
+ stub_opensearch_info("http://myhost-2:9200/")
2705
+ driver.run(default_tag: 'test') do
2706
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => first_pipeline_id}))
2707
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => second_pipeline_id}))
2708
+ end
2709
+ assert_requested(first_request)
2710
+ assert_requested(second_request)
2711
+ end
2712
+
2713
+ def test_writes_to_extracted_host_with_placeholder_replaced_in_exception_message
2714
+ driver.configure(Fluent::Config::Element.new(
2715
+ 'ROOT', '', {
2716
+ '@type' => 'opensearch',
2717
+ 'host' => 'myhost-${pipeline_id}',
2718
+ }, [
2719
+ Fluent::Config::Element.new('buffer', 'tag,pipeline_id', {}, [])
2720
+ ]
2721
+ ))
2722
+ time = Time.parse Date.today.iso8601
2723
+ pipeline_id = "1"
2724
+ request = stub_opensearch_unavailable("http://myhost-1:9200/_bulk")
2725
+ stub_opensearch_info("http://myhost-1:9200/")
2726
+ exception = assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
2727
+ driver.run(default_tag: 'test') do
2728
+ driver.feed(time.to_i, sample_record.merge({"pipeline_id" => pipeline_id}))
2729
+ end
2730
+ }
2731
+ assert_equal("could not push logs to OpenSearch cluster ({:host=>\"myhost-1\", :port=>9200, :scheme=>\"http\"}): [503] ", exception.message)
2732
+ end
2733
+ end
2734
+
2735
+ def test_writes_to_logstash_index_with_specified_prefix_uppercase
2736
+ driver.configure("logstash_format true
2737
+ logstash_prefix MyPrefix")
2738
+ time = Time.parse Date.today.iso8601
2739
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m.%d")}"
2740
+ stub_opensearch
2741
+ stub_opensearch_info
2742
+ driver.run(default_tag: 'test') do
2743
+ driver.feed(time.to_i, sample_record)
2744
+ end
2745
+ # Allthough logstash_prefix has upper-case characters,
2746
+ # it should be set as lower-case when sent to elasticsearch.
2747
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2748
+ end
2749
+
2750
+ def test_writes_to_logstash_index_with_specified_dateformat
2751
+ driver.configure("logstash_format true
2752
+ logstash_dateformat %Y.%m")
2753
+ time = Time.parse Date.today.iso8601
2754
+ logstash_index = "logstash-#{time.getutc.strftime("%Y.%m")}"
2755
+ stub_opensearch
2756
+ stub_opensearch_info
2757
+ driver.run(default_tag: 'test') do
2758
+ driver.feed(time.to_i, sample_record)
2759
+ end
2760
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2761
+ end
2762
+
2763
+ def test_writes_to_logstash_index_with_specified_prefix_and_dateformat
2764
+ driver.configure("logstash_format true
2765
+ logstash_prefix myprefix
2766
+ logstash_dateformat %Y.%m")
2767
+ time = Time.parse Date.today.iso8601
2768
+ logstash_index = "myprefix-#{time.getutc.strftime("%Y.%m")}"
2769
+ stub_opensearch
2770
+ stub_opensearch_info
2771
+ driver.run(default_tag: 'test') do
2772
+ driver.feed(time.to_i, sample_record)
2773
+ end
2774
+ assert_equal(logstash_index, index_cmds.first['index']['_index'])
2775
+ end
2776
+
2777
+ def test_error_if_tag_not_in_chunk_keys
2778
+ assert_raise(Fluent::ConfigError) {
2779
+ config = %{
2780
+ <buffer foo>
2781
+ </buffer>
2782
+ }
2783
+ driver.configure(config)
2784
+ }
2785
+ end
2786
+
2787
+ def test_can_use_custom_chunk_along_with_tag
2788
+ config = %{
2789
+ <buffer tag, foo>
2790
+ </buffer>
2791
+ }
2792
+ driver.configure(config)
2793
+ end
2794
+
2795
+ def test_doesnt_add_logstash_timestamp_by_default
2796
+ stub_opensearch
2797
+ stub_opensearch_info
2798
+ driver.run(default_tag: 'test') do
2799
+ driver.feed(sample_record)
2800
+ end
2801
+ assert_nil(index_cmds[1]['@timestamp'])
2802
+ end
2803
+
2804
+ def test_adds_timestamp_when_logstash
2805
+ driver.configure("logstash_format true\n")
2806
+ stub_opensearch
2807
+ stub_opensearch_info
2808
+ ts = DateTime.now
2809
+ time = Fluent::EventTime.from_time(ts.to_time)
2810
+ driver.run(default_tag: 'test') do
2811
+ driver.feed(time, sample_record)
2812
+ end
2813
+ assert(index_cmds[1].has_key? '@timestamp')
2814
+ assert_equal(ts.iso8601(9), index_cmds[1]['@timestamp'])
2815
+ end
2816
+
2817
+ def test_adds_timestamp_when_include_timestamp
2818
+ driver.configure("include_timestamp true\n")
2819
+ stub_opensearch
2820
+ stub_opensearch_info
2821
+ ts = DateTime.now
2822
+ time = Fluent::EventTime.from_time(ts.to_time)
2823
+ driver.run(default_tag: 'test') do
2824
+ driver.feed(time, sample_record)
2825
+ end
2826
+ assert(index_cmds[1].has_key? '@timestamp')
2827
+ assert_equal(ts.iso8601(9), index_cmds[1]['@timestamp'])
2828
+ end
2829
+
2830
+ def test_uses_custom_timestamp_when_included_in_record
2831
+ driver.configure("logstash_format true\n")
2832
+ stub_opensearch
2833
+ stub_opensearch_info
2834
+ ts = DateTime.new(2001,2,3).iso8601
2835
+ driver.run(default_tag: 'test') do
2836
+ driver.feed(sample_record.merge!('@timestamp' => ts))
2837
+ end
2838
+ assert(index_cmds[1].has_key? '@timestamp')
2839
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2840
+ end
2841
+
2842
+ def test_uses_custom_timestamp_when_included_in_record_without_logstash
2843
+ driver.configure("include_timestamp true\n")
2844
+ stub_opensearch
2845
+ stub_opensearch_info
2846
+ ts = DateTime.new(2001,2,3).iso8601
2847
+ driver.run(default_tag: 'test') do
2848
+ driver.feed(sample_record.merge!('@timestamp' => ts))
2849
+ end
2850
+ assert(index_cmds[1].has_key? '@timestamp')
2851
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2852
+ end
2853
+
2854
+ def test_uses_custom_time_key
2855
+ driver.configure("logstash_format true
2856
+ time_key vtm\n")
2857
+ stub_opensearch
2858
+ stub_opensearch_info
2859
+ ts = DateTime.new(2001,2,3).iso8601(9)
2860
+ driver.run(default_tag: 'test') do
2861
+ driver.feed(sample_record.merge!('vtm' => ts))
2862
+ end
2863
+ assert(index_cmds[1].has_key? '@timestamp')
2864
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2865
+ end
2866
+
2867
+ def test_uses_custom_time_key_with_float_record
2868
+ driver.configure("logstash_format true
2869
+ time_precision 3
2870
+ time_key vtm\n")
2871
+ stub_opensearch
2872
+ stub_opensearch_info
2873
+ time = Time.now
2874
+ float_time = time.to_f
2875
+ driver.run(default_tag: 'test') do
2876
+ driver.feed(sample_record.merge!('vtm' => float_time))
2877
+ end
2878
+ assert(index_cmds[1].has_key? '@timestamp')
2879
+ assert_equal(time.to_datetime.iso8601(3), index_cmds[1]['@timestamp'])
2880
+ end
2881
+
2882
+ def test_uses_custom_time_key_with_format
2883
+ driver.configure("logstash_format true
2884
+ time_key_format %Y-%m-%d %H:%M:%S.%N%z
2885
+ time_key vtm\n")
2886
+ stub_opensearch
2887
+ stub_opensearch_info
2888
+ ts = "2001-02-03 13:14:01.673+02:00"
2889
+ driver.run(default_tag: 'test') do
2890
+ driver.feed(sample_record.merge!('vtm' => ts))
2891
+ end
2892
+ assert(index_cmds[1].has_key? '@timestamp')
2893
+ assert_equal(DateTime.parse(ts).iso8601(9), index_cmds[1]['@timestamp'])
2894
+ assert_equal("logstash-2001.02.03", index_cmds[0]['index']['_index'])
2895
+ end
2896
+
2897
+ def test_uses_custom_time_key_with_float_record_and_format
2898
+ driver.configure("logstash_format true
2899
+ time_key_format %Y-%m-%d %H:%M:%S.%N%z
2900
+ time_key vtm\n")
2901
+ stub_opensearch
2902
+ stub_opensearch_info
2903
+ ts = "2001-02-03 13:14:01.673+02:00"
2904
+ time = Time.parse(ts)
2905
+ current_zone_offset = Time.new(2001, 02, 03).to_datetime.offset
2906
+ float_time = time.to_f
2907
+ driver.run(default_tag: 'test') do
2908
+ driver.feed(sample_record.merge!('vtm' => float_time))
2909
+ end
2910
+ assert(index_cmds[1].has_key? '@timestamp')
2911
+ assert_equal(DateTime.parse(ts).new_offset(current_zone_offset).iso8601(9), index_cmds[1]['@timestamp'])
2912
+ end
2913
+
2914
+ def test_uses_custom_time_key_with_format_without_logstash
2915
+ driver.configure("include_timestamp true
2916
+ index_name test
2917
+ time_key_format %Y-%m-%d %H:%M:%S.%N%z
2918
+ time_key vtm\n")
2919
+ stub_opensearch
2920
+ stub_opensearch_info
2921
+ ts = "2001-02-03 13:14:01.673+02:00"
2922
+ driver.run(default_tag: 'test') do
2923
+ driver.feed(sample_record.merge!('vtm' => ts))
2924
+ end
2925
+ assert(index_cmds[1].has_key? '@timestamp')
2926
+ assert_equal(DateTime.parse(ts).iso8601(9), index_cmds[1]['@timestamp'])
2927
+ assert_equal("test", index_cmds[0]['index']['_index'])
2928
+ end
2929
+
2930
+ def test_uses_custom_time_key_exclude_timekey
2931
+ driver.configure("logstash_format true
2932
+ time_key vtm
2933
+ time_key_exclude_timestamp true\n")
2934
+ stub_opensearch
2935
+ stub_opensearch_info
2936
+ ts = DateTime.new(2001,2,3).iso8601
2937
+ driver.run(default_tag: 'test') do
2938
+ driver.feed(sample_record.merge!('vtm' => ts))
2939
+ end
2940
+ assert(!index_cmds[1].key?('@timestamp'), '@timestamp should be messing')
2941
+ end
2942
+
2943
+ def test_uses_custom_time_key_format
2944
+ driver.configure("logstash_format true
2945
+ time_key_format %Y-%m-%dT%H:%M:%S.%N%z\n")
2946
+ stub_opensearch
2947
+ stub_opensearch_info
2948
+ ts = "2001-02-03T13:14:01.673+02:00"
2949
+ driver.run(default_tag: 'test') do
2950
+ driver.feed(sample_record.merge!('@timestamp' => ts))
2951
+ end
2952
+ assert_equal("logstash-2001.02.03", index_cmds[0]['index']['_index'])
2953
+ assert(index_cmds[1].has_key? '@timestamp')
2954
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2955
+ end
2956
+
2957
+ def test_uses_custom_time_key_format_without_logstash
2958
+ driver.configure("include_timestamp true
2959
+ index_name test
2960
+ time_key_format %Y-%m-%dT%H:%M:%S.%N%z\n")
2961
+ stub_opensearch
2962
+ stub_opensearch_info
2963
+ ts = "2001-02-03T13:14:01.673+02:00"
2964
+ driver.run(default_tag: 'test') do
2965
+ driver.feed(sample_record.merge!('@timestamp' => ts))
2966
+ end
2967
+ assert_equal("test", index_cmds[0]['index']['_index'])
2968
+ assert(index_cmds[1].has_key? '@timestamp')
2969
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2970
+ end
2971
+
2972
+ data(:default => nil,
2973
+ :custom_tag => 'es_plugin.output.time.error')
2974
+ def test_uses_custom_time_key_format_logs_an_error(tag_for_error)
2975
+ tag_config = tag_for_error ? "time_parse_error_tag #{tag_for_error}" : ''
2976
+ tag_for_error = 'opensearch_plugin.output.time.error' if tag_for_error.nil?
2977
+ driver.configure("logstash_format true
2978
+ time_key_format %Y-%m-%dT%H:%M:%S.%N%z\n#{tag_config}\n")
2979
+ stub_opensearch
2980
+ stub_opensearch_info
2981
+
2982
+ ts = "2001/02/03 13:14:01,673+02:00"
2983
+ index = "logstash-#{Time.now.getutc.strftime("%Y.%m.%d")}"
2984
+
2985
+ flexmock(driver.instance.router).should_receive(:emit_error_event)
2986
+ .with(tag_for_error, Fluent::EventTime, Hash, ArgumentError).once
2987
+ driver.run(default_tag: 'test') do
2988
+ driver.feed(sample_record.merge!('@timestamp' => ts))
2989
+ end
2990
+
2991
+ assert_equal(index, index_cmds[0]['index']['_index'])
2992
+ assert(index_cmds[1].has_key? '@timestamp')
2993
+ assert_equal(ts, index_cmds[1]['@timestamp'])
2994
+ end
2995
+
2996
+
2997
+ def test_uses_custom_time_key_format_obscure_format
2998
+ driver.configure("logstash_format true
2999
+ time_key_format %a %b %d %H:%M:%S %Z %Y\n")
3000
+ stub_opensearch
3001
+ stub_opensearch_info
3002
+ ts = "Thu Nov 29 14:33:20 GMT 2001"
3003
+ driver.run(default_tag: 'test') do
3004
+ driver.feed(sample_record.merge!('@timestamp' => ts))
3005
+ end
3006
+ assert_equal("logstash-2001.11.29", index_cmds[0]['index']['_index'])
3007
+ assert(index_cmds[1].has_key? '@timestamp')
3008
+ assert_equal(ts, index_cmds[1]['@timestamp'])
3009
+ end
3010
+
3011
+ def test_uses_nanosecond_precision_by_default
3012
+ driver.configure("logstash_format true\n")
3013
+ stub_opensearch
3014
+ stub_opensearch_info
3015
+ time = Fluent::EventTime.new(Time.now.to_i, 123456789)
3016
+ driver.run(default_tag: 'test') do
3017
+ driver.feed(time, sample_record)
3018
+ end
3019
+ assert(index_cmds[1].has_key? '@timestamp')
3020
+ assert_equal(Time.at(time).iso8601(9), index_cmds[1]['@timestamp'])
3021
+ end
3022
+
3023
+ def test_uses_subsecond_precision_when_configured
3024
+ driver.configure("logstash_format true
3025
+ time_precision 3\n")
3026
+ stub_opensearch
3027
+ stub_opensearch_info
3028
+ time = Fluent::EventTime.new(Time.now.to_i, 123456789)
3029
+ driver.run(default_tag: 'test') do
3030
+ driver.feed(time, sample_record)
3031
+ end
3032
+ assert(index_cmds[1].has_key? '@timestamp')
3033
+ assert_equal(Time.at(time).iso8601(3), index_cmds[1]['@timestamp'])
3034
+ end
3035
+
3036
+ def test_doesnt_add_tag_key_by_default
3037
+ stub_opensearch
3038
+ stub_opensearch_info
3039
+ driver.run(default_tag: 'test') do
3040
+ driver.feed(sample_record)
3041
+ end
3042
+ assert_nil(index_cmds[1]['tag'])
3043
+ end
3044
+
3045
+ def test_adds_tag_key_when_configured
3046
+ driver.configure("include_tag_key true\n")
3047
+ stub_opensearch
3048
+ stub_opensearch_info
3049
+ driver.run(default_tag: 'mytag') do
3050
+ driver.feed(sample_record)
3051
+ end
3052
+ assert(index_cmds[1].has_key?('tag'))
3053
+ assert_equal('mytag', index_cmds[1]['tag'])
3054
+ end
3055
+
3056
+ def test_adds_id_key_when_configured
3057
+ driver.configure("id_key request_id\n")
3058
+ stub_opensearch
3059
+ stub_opensearch_info
3060
+ driver.run(default_tag: 'test') do
3061
+ driver.feed(sample_record)
3062
+ end
3063
+ assert_equal('42', index_cmds[0]['index']['_id'])
3064
+ end
3065
+
3066
+ class NestedIdKeyTest < self
3067
+ def test_adds_nested_id_key_with_dot
3068
+ driver.configure("id_key nested.request_id\n")
3069
+ stub_opensearch
3070
+ stub_opensearch_info
3071
+ driver.run(default_tag: 'test') do
3072
+ driver.feed(nested_sample_record)
3073
+ end
3074
+ assert_equal('42', index_cmds[0]['index']['_id'])
3075
+ end
3076
+
3077
+ def test_adds_nested_id_key_with_dollar_dot
3078
+ driver.configure("id_key $.nested.request_id\n")
3079
+ stub_opensearch
3080
+ stub_opensearch_info
3081
+ driver.run(default_tag: 'test') do
3082
+ driver.feed(nested_sample_record)
3083
+ end
3084
+ assert_equal('42', index_cmds[0]['index']['_id'])
3085
+ end
3086
+
3087
+ def test_adds_nested_id_key_with_bracket
3088
+ driver.configure("id_key $['nested']['request_id']\n")
3089
+ stub_opensearch
3090
+ stub_opensearch_info
3091
+ driver.run(default_tag: 'test') do
3092
+ driver.feed(nested_sample_record)
3093
+ end
3094
+ assert_equal('42', index_cmds[0]['index']['_id'])
3095
+ end
3096
+ end
3097
+
3098
+ def test_doesnt_add_id_key_if_missing_when_configured
3099
+ driver.configure("id_key another_request_id\n")
3100
+ stub_opensearch
3101
+ stub_opensearch_info
3102
+ driver.run(default_tag: 'test') do
3103
+ driver.feed(sample_record)
3104
+ end
3105
+ assert(!index_cmds[0]['index'].has_key?('_id'))
3106
+ end
3107
+
3108
+ def test_adds_id_key_when_not_configured
3109
+ stub_opensearch
3110
+ stub_opensearch_info
3111
+ driver.run(default_tag: 'test') do
3112
+ driver.feed(sample_record)
3113
+ end
3114
+ assert(!index_cmds[0]['index'].has_key?('_id'))
3115
+ end
3116
+
3117
+ def test_adds_parent_key_when_configured
3118
+ driver.configure("parent_key parent_id\n")
3119
+ stub_opensearch
3120
+ stub_opensearch_info
3121
+ driver.run(default_tag: 'test') do
3122
+ driver.feed(sample_record)
3123
+ end
3124
+ assert_equal('parent', index_cmds[0]['index']['_parent'])
3125
+ end
3126
+
3127
+ class NestedParentKeyTest < self
3128
+ def test_adds_nested_parent_key_with_dot
3129
+ driver.configure("parent_key nested.parent_id\n")
3130
+ stub_opensearch
3131
+ stub_opensearch_info
3132
+ driver.run(default_tag: 'test') do
3133
+ driver.feed(nested_sample_record)
3134
+ end
3135
+ assert_equal('parent', index_cmds[0]['index']['_parent'])
3136
+ end
3137
+
3138
+ def test_adds_nested_parent_key_with_dollar_dot
3139
+ driver.configure("parent_key $.nested.parent_id\n")
3140
+ stub_opensearch
3141
+ stub_opensearch_info
3142
+ driver.run(default_tag: 'test') do
3143
+ driver.feed(nested_sample_record)
3144
+ end
3145
+ assert_equal('parent', index_cmds[0]['index']['_parent'])
3146
+ end
3147
+
3148
+ def test_adds_nested_parent_key_with_bracket
3149
+ driver.configure("parent_key $['nested']['parent_id']\n")
3150
+ stub_opensearch
3151
+ stub_opensearch_info
3152
+ driver.run(default_tag: 'test') do
3153
+ driver.feed(nested_sample_record)
3154
+ end
3155
+ assert_equal('parent', index_cmds[0]['index']['_parent'])
3156
+ end
3157
+ end
3158
+
3159
+ def test_doesnt_add_parent_key_if_missing_when_configured
3160
+ driver.configure("parent_key another_parent_id\n")
3161
+ stub_opensearch
3162
+ stub_opensearch_info
3163
+ driver.run(default_tag: 'test') do
3164
+ driver.feed(sample_record)
3165
+ end
3166
+ assert(!index_cmds[0]['index'].has_key?('_parent'))
3167
+ end
3168
+
3169
+ def test_adds_parent_key_when_not_configured
3170
+ stub_opensearch
3171
+ stub_opensearch_info
3172
+ driver.run(default_tag: 'test') do
3173
+ driver.feed(sample_record)
3174
+ end
3175
+ assert(!index_cmds[0]['index'].has_key?('_parent'))
3176
+ end
3177
+
3178
+ class AddsRoutingKeyWhenConfiguredTest < self
3179
+ def test_os1
3180
+ driver("routing_key routing_id\n", 1)
3181
+ stub_opensearch
3182
+ stub_opensearch_info
3183
+ driver.run(default_tag: 'test') do
3184
+ driver.feed(sample_record)
3185
+ end
3186
+ assert_equal('routing', index_cmds[0]['index']['routing'])
3187
+ end
3188
+ end
3189
+
3190
+ class NestedRoutingKeyTest < self
3191
+ def test_adds_nested_routing_key_with_dot
3192
+ driver.configure("routing_key nested.routing_id\n")
3193
+ stub_opensearch
3194
+ stub_opensearch_info
3195
+ driver.run(default_tag: 'test') do
3196
+ driver.feed(nested_sample_record)
3197
+ end
3198
+ assert_equal('routing', index_cmds[0]['index']['routing'])
3199
+ end
3200
+
3201
+ def test_adds_nested_routing_key_with_dollar_dot
3202
+ driver.configure("routing_key $.nested.routing_id\n")
3203
+ stub_opensearch
3204
+ stub_opensearch_info
3205
+ driver.run(default_tag: 'test') do
3206
+ driver.feed(nested_sample_record)
3207
+ end
3208
+ assert_equal('routing', index_cmds[0]['index']['routing'])
3209
+ end
3210
+
3211
+ def test_adds_nested_routing_key_with_bracket
3212
+ driver.configure("routing_key $['nested']['routing_id']\n")
3213
+ stub_opensearch
3214
+ stub_opensearch_info
3215
+ driver.run(default_tag: 'test') do
3216
+ driver.feed(nested_sample_record)
3217
+ end
3218
+ assert_equal('routing', index_cmds[0]['index']['routing'])
3219
+ end
3220
+ end
3221
+
3222
+ def test_doesnt_add_routing_key_if_missing_when_configured
3223
+ driver.configure("routing_key another_routing_id\n")
3224
+ stub_opensearch
3225
+ stub_opensearch_info
3226
+ driver.run(default_tag: 'test') do
3227
+ driver.feed(sample_record)
3228
+ end
3229
+ assert(!index_cmds[0]['index'].has_key?('_routing'))
3230
+ end
3231
+
3232
+ def test_adds_routing_key_when_not_configured
3233
+ stub_opensearch
3234
+ stub_opensearch_info
3235
+ driver.run(default_tag: 'test') do
3236
+ driver.feed(sample_record)
3237
+ end
3238
+ assert(!index_cmds[0]['index'].has_key?('_routing'))
3239
+ end
3240
+
3241
+ def test_remove_one_key
3242
+ driver.configure("remove_keys key1\n")
3243
+ stub_opensearch
3244
+ stub_opensearch_info
3245
+ driver.run(default_tag: 'test') do
3246
+ driver.feed(sample_record.merge('key1' => 'v1', 'key2' => 'v2'))
3247
+ end
3248
+ assert(!index_cmds[1].has_key?('key1'))
3249
+ assert(index_cmds[1].has_key?('key2'))
3250
+ end
3251
+
3252
+ def test_remove_multi_keys
3253
+ driver.configure("remove_keys key1, key2\n")
3254
+ stub_opensearch
3255
+ stub_opensearch_info
3256
+ driver.run(default_tag: 'test') do
3257
+ driver.feed(sample_record.merge('key1' => 'v1', 'key2' => 'v2'))
3258
+ end
3259
+ assert(!index_cmds[1].has_key?('key1'))
3260
+ assert(!index_cmds[1].has_key?('key2'))
3261
+ end
3262
+
3263
+ def test_request_error
3264
+ stub_opensearch_info
3265
+ stub_opensearch_unavailable
3266
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
3267
+ driver.run(default_tag: 'test', shutdown: false) do
3268
+ driver.feed(sample_record)
3269
+ end
3270
+ }
3271
+ end
3272
+
3273
+ def test_request_forever
3274
+ omit("retry_forever test is unstable.") if ENV["CI"]
3275
+ stub_opensearch
3276
+ stub_opensearch_info
3277
+ driver.configure(Fluent::Config::Element.new(
3278
+ 'ROOT', '', {
3279
+ '@type' => 'opensearch',
3280
+ }, [
3281
+ Fluent::Config::Element.new('buffer', '', {
3282
+ 'retry_forever' => true
3283
+ }, [])
3284
+ ]
3285
+ ))
3286
+ stub_opensearch_timeout
3287
+ assert_raise(Timeout::Error) {
3288
+ driver.run(default_tag: 'test', timeout: 10, force_flush_retry: true) do
3289
+ driver.feed(sample_record)
3290
+ end
3291
+ }
3292
+ end
3293
+
3294
+ def test_connection_failed
3295
+ connection_resets = 0
3296
+
3297
+ stub_request(:post, "http://localhost:9200/_bulk").with do |req|
3298
+ connection_resets += 1
3299
+ raise Faraday::ConnectionFailed, "Test message"
3300
+ end
3301
+ stub_opensearch_info
3302
+
3303
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
3304
+ driver.run(default_tag: 'test', shutdown: false) do
3305
+ driver.feed(sample_record)
3306
+ end
3307
+ }
3308
+ assert_equal(1, connection_resets)
3309
+ end
3310
+
3311
+ def test_reconnect_on_error_enabled
3312
+ connection_resets = 0
3313
+
3314
+ stub_request(:post, "http://localhost:9200/_bulk").with do |req|
3315
+ connection_resets += 1
3316
+ raise ZeroDivisionError, "any not host_unreachable_exceptions exception"
3317
+ end
3318
+ stub_opensearch_info
3319
+
3320
+ driver.configure("reconnect_on_error true\n")
3321
+
3322
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
3323
+ driver.run(default_tag: 'test', shutdown: false) do
3324
+ driver.feed(sample_record)
3325
+ end
3326
+ }
3327
+
3328
+ assert_raise(Timeout::Error) {
3329
+ driver.run(default_tag: 'test', shutdown: false) do
3330
+ driver.feed(sample_record)
3331
+ end
3332
+ }
3333
+ # FIXME: Consider keywords arguments in #run and how to test this later.
3334
+ # Because v0.14 test driver does not have 1 to 1 correspondence between #run and #flush in tests.
3335
+ assert_equal(1, connection_resets)
3336
+ end
3337
+
3338
+ def test_reconnect_on_error_disabled
3339
+ connection_resets = 0
3340
+
3341
+ stub_request(:post, "http://localhost:9200/_bulk").with do |req|
3342
+ connection_resets += 1
3343
+ raise ZeroDivisionError, "any not host_unreachable_exceptions exception"
3344
+ end
3345
+ stub_opensearch_info
3346
+
3347
+ driver.configure("reconnect_on_error false\n")
3348
+
3349
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
3350
+ driver.run(default_tag: 'test', shutdown: false) do
3351
+ driver.feed(sample_record)
3352
+ end
3353
+ }
3354
+
3355
+ assert_raise(Timeout::Error) {
3356
+ driver.run(default_tag: 'test', shutdown: false) do
3357
+ driver.feed(sample_record)
3358
+ end
3359
+ }
3360
+ assert_equal(1, connection_resets)
3361
+ end
3362
+
3363
+ def test_bulk_error_retags_when_configured
3364
+ driver.configure("retry_tag retry\n")
3365
+ stub_request(:post, 'http://localhost:9200/_bulk')
3366
+ .to_return(lambda do |req|
3367
+ { :status => 200,
3368
+ :headers => { 'Content-Type' => 'json' },
3369
+ :body => %({
3370
+ "took" : 1,
3371
+ "errors" : true,
3372
+ "items" : [
3373
+ {
3374
+ "create" : {
3375
+ "_index" : "foo",
3376
+ "_type" : "bar",
3377
+ "_id" : "abc",
3378
+ "status" : 500,
3379
+ "error" : {
3380
+ "type" : "some unrecognized type",
3381
+ "reason":"some error to cause version mismatch"
3382
+ }
3383
+ }
3384
+ }
3385
+ ]
3386
+ })
3387
+ }
3388
+ end)
3389
+ stub_opensearch_info
3390
+
3391
+ driver.run(default_tag: 'test') do
3392
+ driver.feed(1, sample_record)
3393
+ end
3394
+
3395
+ assert_equal [['retry', 1, sample_record]], driver.events
3396
+ end
3397
+
3398
+ class FulfilledBufferRetryStreamTest < self
3399
+ def test_bulk_error_retags_with_error_when_configured_and_fullfilled_buffer
3400
+ def create_driver(conf='', os_version=1, client_version="\"1.0\"")
3401
+ @client_version ||= client_version
3402
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
3403
+ def retry_stream_retryable?
3404
+ false
3405
+ end
3406
+ CODE
3407
+ # For request stub to detect compatibility.
3408
+ @os_version ||= os_version
3409
+ @client_version ||= client_version
3410
+ if @os_version
3411
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
3412
+ def detect_os_major_version
3413
+ #{@os_version}
3414
+ end
3415
+ CODE
3416
+ end
3417
+ Fluent::Plugin::OpenSearchOutput.module_eval(<<-CODE)
3418
+ def client_library_version
3419
+ #{@client_version}
3420
+ end
3421
+ CODE
3422
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::OpenSearchOutput).configure(conf)
3423
+ end
3424
+ driver = create_driver("retry_tag retry\n")
3425
+ stub_request(:post, 'http://localhost:9200/_bulk')
3426
+ .to_return(lambda do |req|
3427
+ { :status => 200,
3428
+ :headers => { 'Content-Type' => 'json' },
3429
+ :body => %({
3430
+ "took" : 1,
3431
+ "errors" : true,
3432
+ "items" : [
3433
+ {
3434
+ "create" : {
3435
+ "_index" : "foo",
3436
+ "_type" : "bar",
3437
+ "_id" : "abc1",
3438
+ "status" : 403,
3439
+ "error" : {
3440
+ "type" : "cluster_block_exception",
3441
+ "reason":"index [foo] blocked by: [FORBIDDEN/8/index write (api)]"
3442
+ }
3443
+ }
3444
+ },
3445
+ {
3446
+ "create" : {
3447
+ "_index" : "foo",
3448
+ "_type" : "bar",
3449
+ "_id" : "abc2",
3450
+ "status" : 403,
3451
+ "error" : {
3452
+ "type" : "cluster_block_exception",
3453
+ "reason":"index [foo] blocked by: [FORBIDDEN/8/index write (api)]"
3454
+ }
3455
+ }
3456
+ }
3457
+ ]
3458
+ })
3459
+ }
3460
+ end)
3461
+ stub_opensearch_info
3462
+
3463
+ # Check buffer fulfillment condition
3464
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RetryStreamEmitFailure) do
3465
+ driver.run(default_tag: 'test') do
3466
+ driver.feed(1, sample_record)
3467
+ driver.feed(1, sample_record)
3468
+ end
3469
+ end
3470
+
3471
+ assert_equal [], driver.events
3472
+ end
3473
+ end
3474
+
3475
+ def test_create_should_write_records_with_ids_and_skip_those_without
3476
+ driver.configure("write_operation create\nid_key my_id\n@log_level debug")
3477
+ stub_request(:post, 'http://localhost:9200/_bulk')
3478
+ .to_return(lambda do |req|
3479
+ { :status => 200,
3480
+ :headers => { 'Content-Type' => 'json' },
3481
+ :body => %({
3482
+ "took" : 1,
3483
+ "errors" : true,
3484
+ "items" : [
3485
+ {
3486
+ "create" : {
3487
+ "_index" : "foo",
3488
+ "_type" : "bar",
3489
+ "_id" : "abc"
3490
+ }
3491
+ },
3492
+ {
3493
+ "create" : {
3494
+ "_index" : "foo",
3495
+ "_type" : "bar",
3496
+ "_id" : "xyz",
3497
+ "status" : 500,
3498
+ "error" : {
3499
+ "type" : "some unrecognized type",
3500
+ "reason":"some error to cause version mismatch"
3501
+ }
3502
+ }
3503
+ }
3504
+ ]
3505
+ })
3506
+ }
3507
+ end)
3508
+ stub_opensearch_info
3509
+
3510
+ sample_record1 = sample_record('my_id' => 'abc')
3511
+ sample_record4 = sample_record('my_id' => 'xyz')
3512
+
3513
+ driver.run(default_tag: 'test') do
3514
+ driver.feed(1, sample_record1)
3515
+ driver.feed(2, sample_record)
3516
+ driver.feed(3, sample_record)
3517
+ driver.feed(4, sample_record4)
3518
+ end
3519
+
3520
+ logs = driver.logs
3521
+ # one record succeeded while the other should be 'retried'
3522
+ assert_equal [['test', 4, sample_record4]], driver.events
3523
+ assert_logs_include(logs, /(Dropping record)/, 2)
3524
+ end
3525
+
3526
+ def test_create_should_write_records_with_ids_and_emit_those_without
3527
+ driver.configure("write_operation create\nid_key my_id\nemit_error_for_missing_id true\n@log_level debug")
3528
+ stub_request(:post, 'http://localhost:9200/_bulk')
3529
+ .to_return(lambda do |req|
3530
+ { :status => 200,
3531
+ :headers => { 'Content-Type' => 'json' },
3532
+ :body => %({
3533
+ "took" : 1,
3534
+ "errors" : true,
3535
+ "items" : [
3536
+ {
3537
+ "create" : {
3538
+ "_index" : "foo",
3539
+ "_type" : "bar",
3540
+ "_id" : "abc"
3541
+ }
3542
+ },
3543
+ {
3544
+ "create" : {
3545
+ "_index" : "foo",
3546
+ "_type" : "bar",
3547
+ "_id" : "xyz",
3548
+ "status" : 500,
3549
+ "error" : {
3550
+ "type" : "some unrecognized type",
3551
+ "reason":"some error to cause version mismatch"
3552
+ }
3553
+ }
3554
+ }
3555
+ ]
3556
+ })
3557
+ }
3558
+ end)
3559
+ stub_opensearch_info
3560
+
3561
+ sample_record1 = sample_record('my_id' => 'abc')
3562
+ sample_record4 = sample_record('my_id' => 'xyz')
3563
+
3564
+ driver.run(default_tag: 'test') do
3565
+ driver.feed(1, sample_record1)
3566
+ driver.feed(2, sample_record)
3567
+ driver.feed(3, sample_record)
3568
+ driver.feed(4, sample_record4)
3569
+ end
3570
+
3571
+ error_log = driver.error_events.map {|e| e.last.message }
3572
+ # one record succeeded while the other should be 'retried'
3573
+ assert_equal [['test', 4, sample_record4]], driver.events
3574
+ assert_logs_include(error_log, /(Missing '_id' field)/, 2)
3575
+ end
3576
+
3577
+ def test_bulk_error
3578
+ stub_request(:post, 'http://localhost:9200/_bulk')
3579
+ .to_return(lambda do |req|
3580
+ { :status => 200,
3581
+ :headers => { 'Content-Type' => 'json' },
3582
+ :body => %({
3583
+ "took" : 1,
3584
+ "errors" : true,
3585
+ "items" : [
3586
+ {
3587
+ "create" : {
3588
+ "_index" : "foo",
3589
+ "_type" : "bar",
3590
+ "_id" : "abc",
3591
+ "status" : 500,
3592
+ "error" : {
3593
+ "type" : "some unrecognized type",
3594
+ "reason":"some error to cause version mismatch"
3595
+ }
3596
+ }
3597
+ },
3598
+ {
3599
+ "create" : {
3600
+ "_index" : "foo",
3601
+ "_type" : "bar",
3602
+ "_id" : "abc",
3603
+ "status" : 201
3604
+ }
3605
+ },
3606
+ {
3607
+ "create" : {
3608
+ "_index" : "foo",
3609
+ "_type" : "bar",
3610
+ "_id" : "abc",
3611
+ "status" : 500,
3612
+ "error" : {
3613
+ "type" : "some unrecognized type",
3614
+ "reason":"some error to cause version mismatch"
3615
+ }
3616
+ }
3617
+ },
3618
+ {
3619
+ "create" : {
3620
+ "_index" : "foo",
3621
+ "_type" : "bar",
3622
+ "_id" : "abc",
3623
+ "_id" : "abc",
3624
+ "status" : 409
3625
+ }
3626
+ }
3627
+ ]
3628
+ })
3629
+ }
3630
+ end)
3631
+ stub_opensearch_info
3632
+
3633
+ driver.run(default_tag: 'test') do
3634
+ driver.feed(1, sample_record)
3635
+ driver.feed(2, sample_record)
3636
+ driver.feed(3, sample_record)
3637
+ driver.feed(4, sample_record)
3638
+ end
3639
+
3640
+ expect = [['test', 1, sample_record],
3641
+ ['test', 3, sample_record]]
3642
+ assert_equal expect, driver.events
3643
+ end
3644
+
3645
+ def test_update_should_not_write_if_theres_no_id
3646
+ driver.configure("write_operation update\n")
3647
+ stub_opensearch
3648
+ stub_opensearch_info
3649
+ driver.run(default_tag: 'test') do
3650
+ driver.feed(sample_record)
3651
+ end
3652
+ assert_nil(index_cmds)
3653
+ end
3654
+
3655
+ def test_upsert_should_not_write_if_theres_no_id
3656
+ driver.configure("write_operation upsert\n")
3657
+ stub_opensearch
3658
+ stub_opensearch_info
3659
+ driver.run(default_tag: 'test') do
3660
+ driver.feed(sample_record)
3661
+ end
3662
+ assert_nil(index_cmds)
3663
+ end
3664
+
3665
+ def test_create_should_not_write_if_theres_no_id
3666
+ driver.configure("write_operation create\n")
3667
+ stub_opensearch
3668
+ stub_opensearch_info
3669
+ driver.run(default_tag: 'test') do
3670
+ driver.feed(sample_record)
3671
+ end
3672
+ assert_nil(index_cmds)
3673
+ end
3674
+
3675
+ def test_update_should_write_update_op_and_doc_as_upsert_is_false
3676
+ driver.configure("write_operation update
3677
+ id_key request_id")
3678
+ stub_opensearch
3679
+ stub_opensearch_info
3680
+ driver.run(default_tag: 'test') do
3681
+ driver.feed(sample_record)
3682
+ end
3683
+ assert(index_cmds[0].has_key?("update"))
3684
+ assert(!index_cmds[1]["doc_as_upsert"])
3685
+ assert(!index_cmds[1]["upsert"])
3686
+ end
3687
+
3688
+ def test_update_should_remove_keys_from_doc_when_keys_are_skipped
3689
+ driver.configure("write_operation update
3690
+ id_key request_id
3691
+ remove_keys_on_update parent_id")
3692
+ stub_opensearch
3693
+ stub_opensearch_info
3694
+ driver.run(default_tag: 'test') do
3695
+ driver.feed(sample_record)
3696
+ end
3697
+ assert(index_cmds[1]["doc"])
3698
+ assert(!index_cmds[1]["doc"]["parent_id"])
3699
+ end
3700
+
3701
+ def test_upsert_should_write_update_op_and_doc_as_upsert_is_true
3702
+ driver.configure("write_operation upsert
3703
+ id_key request_id")
3704
+ stub_opensearch
3705
+ stub_opensearch_info
3706
+ driver.run(default_tag: 'test') do
3707
+ driver.feed(sample_record)
3708
+ end
3709
+ assert(index_cmds[0].has_key?("update"))
3710
+ assert(index_cmds[1]["doc_as_upsert"])
3711
+ assert(!index_cmds[1]["upsert"])
3712
+ end
3713
+
3714
+ def test_upsert_should_write_update_op_upsert_and_doc_when_keys_are_skipped
3715
+ driver.configure("write_operation upsert
3716
+ id_key request_id
3717
+ remove_keys_on_update parent_id")
3718
+ stub_opensearch
3719
+ stub_opensearch_info
3720
+ driver.run(default_tag: 'test') do
3721
+ driver.feed(sample_record)
3722
+ end
3723
+ assert(index_cmds[0].has_key?("update"))
3724
+ assert(!index_cmds[1]["doc_as_upsert"])
3725
+ assert(index_cmds[1]["upsert"])
3726
+ assert(index_cmds[1]["doc"])
3727
+ end
3728
+
3729
+ def test_upsert_should_remove_keys_from_doc_when_keys_are_skipped
3730
+ driver.configure("write_operation upsert
3731
+ id_key request_id
3732
+ remove_keys_on_update parent_id")
3733
+ stub_opensearch
3734
+ stub_opensearch_info
3735
+ driver.run(default_tag: 'test') do
3736
+ driver.feed(sample_record)
3737
+ end
3738
+ assert(index_cmds[1]["upsert"] != index_cmds[1]["doc"])
3739
+ assert(!index_cmds[1]["doc"]["parent_id"])
3740
+ assert(index_cmds[1]["upsert"]["parent_id"])
3741
+ end
3742
+
3743
+ def test_upsert_should_remove_multiple_keys_when_keys_are_skipped
3744
+ driver.configure("write_operation upsert
3745
+ id_key id
3746
+ remove_keys_on_update foo,baz")
3747
+ stub_opensearch
3748
+ stub_opensearch_info
3749
+ driver.run(default_tag: 'test') do
3750
+ driver.feed("id" => 1, "foo" => "bar", "baz" => "quix", "zip" => "zam")
3751
+ end
3752
+ assert(
3753
+ index_cmds[1]["doc"] == {
3754
+ "id" => 1,
3755
+ "zip" => "zam",
3756
+ }
3757
+ )
3758
+ assert(
3759
+ index_cmds[1]["upsert"] == {
3760
+ "id" => 1,
3761
+ "foo" => "bar",
3762
+ "baz" => "quix",
3763
+ "zip" => "zam",
3764
+ }
3765
+ )
3766
+ end
3767
+
3768
+ def test_upsert_should_remove_keys_from_when_the_keys_are_in_the_record
3769
+ driver.configure("write_operation upsert
3770
+ id_key id
3771
+ remove_keys_on_update_key keys_to_skip")
3772
+ stub_opensearch
3773
+ stub_opensearch_info
3774
+ driver.run(default_tag: 'test') do
3775
+ driver.feed("id" => 1, "foo" => "bar", "baz" => "quix", "keys_to_skip" => ["baz"])
3776
+ end
3777
+ assert(
3778
+ index_cmds[1]["doc"] == {
3779
+ "id" => 1,
3780
+ "foo" => "bar",
3781
+ }
3782
+ )
3783
+ assert(
3784
+ index_cmds[1]["upsert"] == {
3785
+ "id" => 1,
3786
+ "foo" => "bar",
3787
+ "baz" => "quix",
3788
+ }
3789
+ )
3790
+ end
3791
+
3792
+ def test_upsert_should_remove_keys_from_key_on_record_has_higher_presedence_than_config
3793
+ driver.configure("write_operation upsert
3794
+ id_key id
3795
+ remove_keys_on_update foo,bar
3796
+ remove_keys_on_update_key keys_to_skip")
3797
+ stub_opensearch
3798
+ stub_opensearch_info
3799
+ driver.run(default_tag: 'test') do
3800
+ driver.feed("id" => 1, "foo" => "bar", "baz" => "quix", "keys_to_skip" => ["baz"])
3801
+ end
3802
+ assert(
3803
+ index_cmds[1]["doc"] == {
3804
+ "id" => 1,
3805
+ # we only expect baz to be stripped here, if the config was more important
3806
+ # foo would be stripped too.
3807
+ "foo" => "bar",
3808
+ }
3809
+ )
3810
+ assert(
3811
+ index_cmds[1]["upsert"] == {
3812
+ "id" => 1,
3813
+ "foo" => "bar",
3814
+ "baz" => "quix",
3815
+ }
3816
+ )
3817
+ end
3818
+
3819
+ def test_create_should_write_create_op
3820
+ driver.configure("write_operation create
3821
+ id_key request_id")
3822
+ stub_opensearch
3823
+ stub_opensearch_info
3824
+ driver.run(default_tag: 'test') do
3825
+ driver.feed(sample_record)
3826
+ end
3827
+ assert(index_cmds[0].has_key?("create"))
3828
+ end
3829
+
3830
+ def test_include_index_in_url
3831
+ stub_opensearch('http://localhost:9200/logstash-2018.01.01/_bulk')
3832
+ stub_opensearch_info('http://localhost:9200/')
3833
+
3834
+ driver.configure("index_name logstash-2018.01.01
3835
+ include_index_in_url true")
3836
+ driver.run(default_tag: 'test') do
3837
+ driver.feed(sample_record)
3838
+ end
3839
+
3840
+ assert_equal(2, index_cmds.length)
3841
+ assert_equal(nil, index_cmds.first['index']['_index'])
3842
+ end
3843
+
3844
+ def test_use_simple_sniffer
3845
+ require 'fluent/plugin/opensearch_simple_sniffer'
3846
+ stub_opensearch
3847
+ stub_opensearch_info
3848
+ config = %[
3849
+ sniffer_class_name Fluent::Plugin::OpenSearchSimpleSniffer
3850
+ log_level debug
3851
+ with_transporter_log true
3852
+ reload_connections true
3853
+ reload_after 1
3854
+ ]
3855
+ driver(config, nil)
3856
+ driver.run(default_tag: 'test') do
3857
+ driver.feed(sample_record)
3858
+ end
3859
+ log = driver.logs
3860
+ # 2 or 3 - one for the ping, one for the _bulk, (and client.info)
3861
+ assert_logs_include_compare_size(3, ">", log, /In Fluent::Plugin::OpenSearchSimpleSniffer hosts/)
3862
+ assert_logs_include_compare_size(1, "<=", log, /In Fluent::Plugin::OpenSearchSimpleSniffer hosts/)
3863
+ end
3864
+
3865
+ def test_suppress_doc_wrap
3866
+ driver.configure('write_operation update
3867
+ id_key id
3868
+ remove_keys id
3869
+ suppress_doc_wrap true')
3870
+ stub_opensearch
3871
+ stub_opensearch_info
3872
+ doc_body = {'field' => 'value'}
3873
+ script_body = {'source' => 'ctx._source.counter += params.param1',
3874
+ 'lang' => 'painless',
3875
+ 'params' => {'param1' => 1}}
3876
+ upsert_body = {'counter' => 1}
3877
+ driver.run(default_tag: 'test') do
3878
+ driver.feed('id' => 1, 'doc' => doc_body)
3879
+ driver.feed('id' => 2, 'script' => script_body, 'upsert' => upsert_body)
3880
+ end
3881
+ assert(
3882
+ index_cmds[1] == {'doc' => doc_body}
3883
+ )
3884
+ assert(
3885
+ index_cmds[3] == {
3886
+ 'script' => script_body,
3887
+ 'upsert' => upsert_body
3888
+ }
3889
+ )
3890
+ end
3891
+
3892
+ def test_suppress_doc_wrap_should_handle_record_as_is_at_upsert
3893
+ driver.configure('write_operation upsert
3894
+ id_key id
3895
+ remove_keys id
3896
+ suppress_doc_wrap true')
3897
+ stub_opensearch
3898
+ stub_opensearch_info
3899
+ doc_body = {'field' => 'value'}
3900
+ script_body = {'source' => 'ctx._source.counter += params.param1',
3901
+ 'lang' => 'painless',
3902
+ 'params' => {'param1' => 1}}
3903
+ upsert_body = {'counter' => 1}
3904
+ driver.run(default_tag: 'test') do
3905
+ driver.feed('id' => 1, 'doc' => doc_body, 'doc_as_upsert' => true)
3906
+ driver.feed('id' => 2, 'script' => script_body, 'upsert' => upsert_body)
3907
+ end
3908
+ assert(
3909
+ index_cmds[1] == {
3910
+ 'doc' => doc_body,
3911
+ 'doc_as_upsert' => true
3912
+ }
3913
+ )
3914
+ assert(
3915
+ index_cmds[3] == {
3916
+ 'script' => script_body,
3917
+ 'upsert' => upsert_body
3918
+ }
3919
+ )
3920
+ end
3921
+
3922
+ def test_ignore_exception
3923
+ driver.configure('ignore_exceptions ["OpenSearch::Transport::Transport::Errors::ServiceUnavailable"]')
3924
+ stub_opensearch_unavailable
3925
+ stub_opensearch_info
3926
+
3927
+ driver.run(default_tag: 'test') do
3928
+ driver.feed(sample_record)
3929
+ end
3930
+ end
3931
+
3932
+ def test_ignore_exception_with_superclass
3933
+ driver.configure('ignore_exceptions ["OpenSearch::Transport::Transport::ServerError"]')
3934
+ stub_opensearch_unavailable
3935
+ stub_opensearch_info
3936
+
3937
+ driver.run(default_tag: 'test') do
3938
+ driver.feed(sample_record)
3939
+ end
3940
+ end
3941
+
3942
+ def test_ignore_excetion_handles_appropriate_ones
3943
+ driver.configure('ignore_exceptions ["Faraday::ConnectionFailed"]')
3944
+ stub_opensearch_unavailable
3945
+ stub_opensearch_info
3946
+
3947
+ assert_raise(Fluent::Plugin::OpenSearchOutput::RecoverableRequestFailure) {
3948
+ driver.run(default_tag: 'test', shutdown: false) do
3949
+ driver.feed(sample_record)
3950
+ end
3951
+ }
3952
+ end
3953
+ end