logstash-output-elasticsearch 11.8.0-java → 11.9.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28dfa1d2a757a4cb14aa7972f13c13f78975a52db0cac8d39425fa6bb34a462c
4
- data.tar.gz: 7a9b9e05f59e2d9024f8faf7d6d5faa58aeb123cb77fd4f89b88826787d5d4f5
3
+ metadata.gz: ad30f3f3af7faaebf3409ac23375e99531def7d30940330f0f617e007f341b1d
4
+ data.tar.gz: d06a9954211995b266d08cfbc6586164275d5d269ef5716ba18d4f7e3ca4cf5c
5
5
  SHA512:
6
- metadata.gz: 8ee227e4dcb1f1af9cb7d1baf11cabb1e4f1a8ab87781494ca71f2d3a9ad57e6635819827c00090f597f79d33d8724f1a2e5e4047f506a0fde144c0dc5d4d71a
7
- data.tar.gz: 6ad8e717ade5b09f46cf4bab5d9749c782cf654dd3e69f79010a230237cd251b7fd034b18f42ed09ffb46aa729c8596eae62a3cb41a8b33c36a7c23e7084adcf
6
+ metadata.gz: ad84072b2dcc3d3a567b18cc89bbd3390e93303e952188053a8f61cbefbddb0a8bf46453fb8df0b94cecaf29304452b85fbe7a822bb9ece0763c4278b514ff91
7
+ data.tar.gz: 348d53c674b7b2730ce918403e705ddbda5f9745ca2564e922d3762bff9de7a455ff871c269669dc1e74d5e0c5f89c7997221eca4c851a073b1540b2a8bbdc15
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 11.9.0
2
+ - Feature: force unresolved dynamic index names to be sent into DLQ. This feature could be explicitly disabled using `dlq_on_failed_indexname_interpolation` setting [#1084](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/1084)
3
+
1
4
  ## 11.8.0
2
5
  - Feature: Adds a new `dlq_custom_codes` option to customize DLQ codes [#1067](https://github.com/logstash-plugins/logstash-output-elasticsearch/pull/1067)
3
6
 
data/docs/index.asciidoc CHANGED
@@ -320,6 +320,7 @@ This plugin supports the following configuration options plus the
320
320
  | <<plugins-{type}s-{plugin}-data_stream_sync_fields>> |<<boolean,boolean>>|No
321
321
  | <<plugins-{type}s-{plugin}-data_stream_type>> |<<string,string>>|No
322
322
  | <<plugins-{type}s-{plugin}-dlq_custom_codes>> |<<number,number>>|No
323
+ | <<plugins-{type}s-{plugin}-dlq_on_failed_indexname_interpolation>> |<<boolean,boolean>>|No
323
324
  | <<plugins-{type}s-{plugin}-doc_as_upsert>> |<<boolean,boolean>>|No
324
325
  | <<plugins-{type}s-{plugin}-document_id>> |<<string,string>>|No
325
326
  | <<plugins-{type}s-{plugin}-document_type>> |<<string,string>>|No
@@ -533,6 +534,14 @@ This list is an addition to the ordinary error codes considered for this feature
533
534
  It's considered a configuration error to re-use the same predefined codes for success, DLQ or conflict.
534
535
  The option accepts a list of natural numbers corresponding to HTTP errors codes.
535
536
 
537
+ [id="plugins-{type}s-{plugin}-dlq_on_failed_indexname_interpolation"]
538
+ ===== `dlq_on_failed_indexname_interpolation`
539
+
540
+ * Value type is <<boolean,boolean>>
541
+ * Default value is `true`.
542
+
543
+ If enabled, failed index name interpolation events go into dead letter queue.
544
+
536
545
  [id="plugins-{type}s-{plugin}-doc_as_upsert"]
537
546
  ===== `doc_as_upsert`
538
547
 
@@ -261,6 +261,9 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
261
261
  # The option accepts a list of natural numbers corresponding to HTTP errors codes.
262
262
  config :dlq_custom_codes, :validate => :number, :list => true, :default => []
263
263
 
264
+ # if enabled, failed index name interpolation events go into dead letter queue.
265
+ config :dlq_on_failed_indexname_interpolation, :validate => :boolean, :default => true
266
+
264
267
  attr_reader :client
265
268
  attr_reader :default_index
266
269
  attr_reader :default_ilm_rollover_alias
@@ -362,11 +365,43 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
362
365
  # Receive an array of events and immediately attempt to index them (no buffering)
363
366
  def multi_receive(events)
364
367
  wait_for_successful_connection if @after_successful_connection_done
365
- retrying_submit map_events(events)
368
+ events_mapped = safe_interpolation_map_events(events)
369
+ retrying_submit(events_mapped.successful_events)
370
+ unless events_mapped.failed_events.empty?
371
+ @logger.error("Can't map some events, needs to be handled by DLQ #{events_mapped.failed_events}")
372
+ send_failed_resolutions_to_dlq(events_mapped.failed_events)
373
+ end
374
+ end
375
+
376
+ # @param: Arrays of EventActionTuple
377
+ private
378
+ def send_failed_resolutions_to_dlq(failed_action_tuples)
379
+ failed_action_tuples.each do |action|
380
+ handle_dlq_status(action, "warn", "Could not resolve dynamic index")
381
+ end
382
+ end
383
+
384
+ MapEventsResult = Struct.new(:successful_events, :failed_events)
385
+
386
+ private
387
+ def safe_interpolation_map_events(events)
388
+ successful_events = [] # list of LogStash::Outputs::ElasticSearch::EventActionTuple
389
+ failed_events = [] # list of LogStash::Event
390
+ events.each do |event|
391
+ begin
392
+ successful_events << @event_mapper.call(event)
393
+ rescue IndexInterpolationError, e
394
+ action = event.sprintf(@action || 'index')
395
+ event_action_tuple = EventActionTuple.new(action, [], event)
396
+ failed_events << event_action_tuple
397
+ end
398
+ end
399
+ MapEventsResult.new(successful_events, failed_events)
366
400
  end
367
401
 
402
+ public
368
403
  def map_events(events)
369
- events.map(&@event_mapper)
404
+ safe_interpolation_map_events(events).successful_events
370
405
  end
371
406
 
372
407
  def wait_for_successful_connection
@@ -441,12 +476,24 @@ class LogStash::Outputs::ElasticSearch < LogStash::Outputs::Base
441
476
 
442
477
  end
443
478
 
479
+ class IndexInterpolationError < ArgumentError
480
+ attr_reader :bad_formatted_index
481
+
482
+ def initialize(bad_formatted_index)
483
+ super("Badly formatted index, after interpolation still contains placeholder: [#{bad_formatted_index}]")
484
+
485
+ @bad_formatted_index = bad_formatted_index
486
+ end
487
+ end
488
+
444
489
  # @return Hash (initial) parameters for given event
445
490
  # @private shared event params factory between index and data_stream mode
446
491
  def common_event_params(event)
492
+ sprintf_index = @event_target.call(event)
493
+ raise IndexInterpolationError, sprintf_index if sprintf_index.match(/%{.*?}/) && dlq_on_failed_indexname_interpolation
447
494
  params = {
448
495
  :_id => @document_id ? event.sprintf(@document_id) : nil,
449
- :_index => @event_target.call(event),
496
+ :_index => sprintf_index,
450
497
  routing_field_name => @routing ? event.sprintf(@routing) : nil
451
498
  }
452
499
 
@@ -206,19 +206,23 @@ module LogStash; module PluginMixins; module ElasticSearch
206
206
  doubled > @retry_max_interval ? @retry_max_interval : doubled
207
207
  end
208
208
 
209
- def handle_dlq_status(message, action, status, response)
209
+ def handle_dlq_response(message, action, status, response)
210
+ _, action_params = action.event, [action[0], action[1], action[2]]
211
+
212
+ # TODO: Change this to send a map with { :status => status, :action => action } in the future
213
+ detailed_message = "#{message} status: #{status}, action: #{action_params}, response: #{response}"
214
+
215
+ log_level = dig_value(response, 'index', 'error', 'type') == 'invalid_index_name_exception' ? :error : :warn
216
+
217
+ handle_dlq_status(action, log_level, detailed_message)
218
+ end
219
+
220
+ def handle_dlq_status(action, log_level, message)
210
221
  # To support bwc, we check if DLQ exists. otherwise we log and drop event (previous behavior)
211
222
  if @dlq_writer
212
- event, action = action.event, [action[0], action[1], action[2]]
213
- # TODO: Change this to send a map with { :status => status, :action => action } in the future
214
- @dlq_writer.write(event, "#{message} status: #{status}, action: #{action}, response: #{response}")
223
+ @dlq_writer.write(action.event, "#{message}")
215
224
  else
216
- if dig_value(response, 'index', 'error', 'type') == 'invalid_index_name_exception'
217
- level = :error
218
- else
219
- level = :warn
220
- end
221
- @logger.send level, message, status: status, action: action, response: response
225
+ @logger.send log_level, message
222
226
  end
223
227
  end
224
228
 
@@ -269,7 +273,7 @@ module LogStash; module PluginMixins; module ElasticSearch
269
273
  @logger.warn "Failed action", status: status, action: action, response: response if log_failure_type?(error)
270
274
  next
271
275
  elsif @dlq_codes.include?(status)
272
- handle_dlq_status("Could not index event to Elasticsearch.", action, status, response)
276
+ handle_dlq_response("Could not index event to Elasticsearch.", action, status, response)
273
277
  @document_level_metrics.increment(:non_retryable_failures)
274
278
  next
275
279
  else
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-output-elasticsearch'
3
- s.version = '11.8.0'
3
+ s.version = '11.9.0'
4
4
  s.licenses = ['apache-2.0']
5
5
  s.summary = "Stores logs in Elasticsearch"
6
6
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -1,5 +1,6 @@
1
1
  require_relative "../../../spec/es_spec_helper"
2
2
  require "logstash/outputs/elasticsearch"
3
+ require 'cgi'
3
4
 
4
5
  describe "TARGET_BULK_BYTES", :integration => true do
5
6
  let(:target_bulk_bytes) { LogStash::Outputs::ElasticSearch::TARGET_BULK_BYTES }
@@ -45,16 +46,57 @@ describe "TARGET_BULK_BYTES", :integration => true do
45
46
  end
46
47
  end
47
48
 
48
- describe "indexing" do
49
+ def curl_and_get_json_response(url, method: :get, retrieve_err_payload: false); require 'open3'
50
+ cmd = "curl -s -v --show-error #{curl_opts} -X #{method.to_s.upcase} -k #{url}"
51
+ begin
52
+ out, err, status = Open3.capture3(cmd)
53
+ rescue Errno::ENOENT
54
+ fail "curl not available, make sure curl binary is installed and available on $PATH"
55
+ end
56
+
57
+ if status.success?
58
+ http_status = err.match(/< HTTP\/1.1 (\d+)/)[1] || '0' # < HTTP/1.1 200 OK\r\n
59
+
60
+ if http_status.strip[0].to_i > 2
61
+ error = (LogStash::Json.load(out)['error']) rescue nil
62
+ if error
63
+ if retrieve_err_payload
64
+ return error
65
+ else
66
+ fail "#{cmd.inspect} received an error: #{http_status}\n\n#{error.inspect}"
67
+ end
68
+ else
69
+ warn out
70
+ fail "#{cmd.inspect} unexpected response: #{http_status}\n\n#{err}"
71
+ end
72
+ end
73
+
74
+ LogStash::Json.load(out)
75
+ else
76
+ warn out
77
+ fail "#{cmd.inspect} process failed: #{status}\n\n#{err}"
78
+ end
79
+ end
80
+
81
+ describe "indexing with sprintf resolution", :integration => true do
49
82
  let(:message) { "Hello from #{__FILE__}" }
50
83
  let(:event) { LogStash::Event.new("message" => message, "type" => type) }
51
- let(:index) { 10.times.collect { rand(10).to_s }.join("") }
84
+ let (:index) { "%{[index_name]}_dynamic" }
52
85
  let(:type) { ESHelper.es_version_satisfies?("< 7") ? "doc" : "_doc" }
53
- let(:event_count) { 1 + rand(2) }
54
- let(:config) { "not implemented" }
86
+ let(:event_count) { 1 }
87
+ let(:user) { "simpleuser" }
88
+ let(:password) { "abc123" }
89
+ let(:config) do
90
+ {
91
+ "hosts" => [ get_host_port ],
92
+ "user" => user,
93
+ "password" => password,
94
+ "index" => index
95
+ }
96
+ end
55
97
  let(:events) { event_count.times.map { event }.to_a }
56
98
  subject { LogStash::Outputs::ElasticSearch.new(config) }
57
-
99
+
58
100
  let(:es_url) { "http://#{get_host_port}" }
59
101
  let(:index_url) { "#{es_url}/#{index}" }
60
102
 
@@ -63,33 +105,65 @@ describe "indexing" do
63
105
  let(:es_admin) { 'admin' } # default user added in ES -> 8.x requires auth credentials for /_refresh etc
64
106
  let(:es_admin_pass) { 'elastic' }
65
107
 
66
- def curl_and_get_json_response(url, method: :get); require 'open3'
67
- cmd = "curl -s -v --show-error #{curl_opts} -X #{method.to_s.upcase} -k #{url}"
68
- begin
69
- out, err, status = Open3.capture3(cmd)
70
- rescue Errno::ENOENT
71
- fail "curl not available, make sure curl binary is installed and available on $PATH"
72
- end
108
+ let(:initial_events) { [] }
73
109
 
74
- if status.success?
75
- http_status = err.match(/< HTTP\/1.1 (\d+)/)[1] || '0' # < HTTP/1.1 200 OK\r\n
110
+ let(:do_register) { true }
76
111
 
77
- if http_status.strip[0].to_i > 2
78
- error = (LogStash::Json.load(out)['error']) rescue nil
79
- if error
80
- fail "#{cmd.inspect} received an error: #{http_status}\n\n#{error.inspect}"
81
- else
82
- warn out
83
- fail "#{cmd.inspect} unexpected response: #{http_status}\n\n#{err}"
84
- end
85
- end
112
+ before do
113
+ subject.register if do_register
114
+ subject.multi_receive(initial_events) if initial_events
115
+ end
86
116
 
87
- LogStash::Json.load(out)
88
- else
89
- warn out
90
- fail "#{cmd.inspect} process failed: #{status}\n\n#{err}"
117
+ after do
118
+ subject.do_close
119
+ end
120
+
121
+ let(:event) { LogStash::Event.new("message" => message, "type" => type, "index_name" => "test") }
122
+
123
+ it "should index successfully when field is resolved" do
124
+ expected_index_name = "test_dynamic"
125
+ subject.multi_receive(events)
126
+
127
+ # curl_and_get_json_response "#{es_url}/_refresh", method: :post
128
+
129
+ result = curl_and_get_json_response "#{es_url}/#{expected_index_name}"
130
+
131
+ expect(result[expected_index_name]).not_to be(nil)
132
+ end
133
+
134
+ context "when dynamic field doesn't resolve the index_name" do
135
+ let(:event) { LogStash::Event.new("message" => message, "type" => type) }
136
+ let(:dlq_writer) { double('DLQ writer') }
137
+ before { subject.instance_variable_set('@dlq_writer', dlq_writer) }
138
+
139
+ it "should doesn't create an index name with unresolved placeholders" do
140
+ expect(dlq_writer).to receive(:write).once.with(event, /Could not resolve dynamic index/)
141
+ subject.multi_receive(events)
142
+
143
+ escaped_index_name = CGI.escape("%{[index_name]}_dynamic")
144
+ result = curl_and_get_json_response "#{es_url}/#{escaped_index_name}", retrieve_err_payload: true
145
+ expect(result["root_cause"].first()["type"]).to eq("index_not_found_exception")
91
146
  end
92
147
  end
148
+ end
149
+
150
+ describe "indexing" do
151
+ let(:message) { "Hello from #{__FILE__}" }
152
+ let(:event) { LogStash::Event.new("message" => message, "type" => type) }
153
+ let(:index) { 10.times.collect { rand(10).to_s }.join("") }
154
+ let(:type) { ESHelper.es_version_satisfies?("< 7") ? "doc" : "_doc" }
155
+ let(:event_count) { 1 + rand(2) }
156
+ let(:config) { "not implemented" }
157
+ let(:events) { event_count.times.map { event }.to_a }
158
+ subject { LogStash::Outputs::ElasticSearch.new(config) }
159
+
160
+ let(:es_url) { "http://#{get_host_port}" }
161
+ let(:index_url) { "#{es_url}/#{index}" }
162
+
163
+ let(:curl_opts) { nil }
164
+
165
+ let(:es_admin) { 'admin' } # default user added in ES -> 8.x requires auth credentials for /_refresh etc
166
+ let(:es_admin_pass) { 'elastic' }
93
167
 
94
168
  let(:initial_events) { [] }
95
169
 
@@ -768,6 +768,7 @@ describe LogStash::Outputs::ElasticSearch do
768
768
 
769
769
  context 'handling elasticsearch document-level status meant for the DLQ' do
770
770
  let(:options) { { "manage_template" => false } }
771
+ let(:action) { LogStash::Outputs::ElasticSearch::EventActionTuple.new(:action, :params, LogStash::Event.new("foo" => "bar")) }
771
772
 
772
773
  context 'when @dlq_writer is nil' do
773
774
  before { subject.instance_variable_set '@dlq_writer', nil }
@@ -777,8 +778,7 @@ describe LogStash::Outputs::ElasticSearch do
777
778
  it 'should log at ERROR level' do
778
779
  subject.instance_variable_set(:@logger, double("logger").as_null_object)
779
780
  mock_response = { 'index' => { 'error' => { 'type' => 'invalid_index_name_exception' } } }
780
- subject.handle_dlq_status("Could not index event to Elasticsearch.",
781
- [:action, :params, :event], :some_status, mock_response)
781
+ subject.handle_dlq_response("Could not index event to Elasticsearch.", action, :some_status, mock_response)
782
782
  end
783
783
  end
784
784
 
@@ -786,10 +786,9 @@ describe LogStash::Outputs::ElasticSearch do
786
786
  it 'should log at WARN level' do
787
787
  logger = double("logger").as_null_object
788
788
  subject.instance_variable_set(:@logger, logger)
789
- expect(logger).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
789
+ expect(logger).to receive(:warn).with(a_string_including "Could not index event to Elasticsearch. status: some_status, action: [:action, :params, {")
790
790
  mock_response = { 'index' => { 'error' => { 'type' => 'illegal_argument_exception' } } }
791
- subject.handle_dlq_status("Could not index event to Elasticsearch.",
792
- [:action, :params, :event], :some_status, mock_response)
791
+ subject.handle_dlq_response("Could not index event to Elasticsearch.", action, :some_status, mock_response)
793
792
  end
794
793
  end
795
794
 
@@ -797,11 +796,10 @@ describe LogStash::Outputs::ElasticSearch do
797
796
  it 'should not fail, but just log a warning' do
798
797
  logger = double("logger").as_null_object
799
798
  subject.instance_variable_set(:@logger, logger)
800
- expect(logger).to receive(:warn).with(/Could not index/, hash_including(:status, :action, :response))
799
+ expect(logger).to receive(:warn).with(a_string_including "Could not index event to Elasticsearch. status: some_status, action: [:action, :params, {")
801
800
  mock_response = { 'index' => {} }
802
801
  expect do
803
- subject.handle_dlq_status("Could not index event to Elasticsearch.",
804
- [:action, :params, :event], :some_status, mock_response)
802
+ subject.handle_dlq_response("Could not index event to Elasticsearch.", action, :some_status, mock_response)
805
803
  end.to_not raise_error
806
804
  end
807
805
  end
@@ -821,7 +819,7 @@ describe LogStash::Outputs::ElasticSearch do
821
819
  expect(dlq_writer).to receive(:write).once.with(event, /Could not index/)
822
820
  mock_response = { 'index' => { 'error' => { 'type' => 'illegal_argument_exception' } } }
823
821
  action = LogStash::Outputs::ElasticSearch::EventActionTuple.new(:action, :params, event)
824
- subject.handle_dlq_status("Could not index event to Elasticsearch.", action, 404, mock_response)
822
+ subject.handle_dlq_response("Could not index event to Elasticsearch.", action, 404, mock_response)
825
823
  end
826
824
  end
827
825
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-output-elasticsearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.8.0
4
+ version: 11.9.0
5
5
  platform: java
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-15 00:00:00.000000000 Z
11
+ date: 2022-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement