fluent-plugin-opensearch 1.0.0

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