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 +4 -4
- data/CHANGELOG.md +3 -0
- data/docs/index.asciidoc +9 -0
- data/lib/logstash/outputs/elasticsearch.rb +50 -3
- data/lib/logstash/plugin_mixins/elasticsearch/common.rb +15 -11
- data/logstash-output-elasticsearch.gemspec +1 -1
- data/spec/integration/outputs/index_spec.rb +101 -27
- data/spec/unit/outputs/elasticsearch_spec.rb +7 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad30f3f3af7faaebf3409ac23375e99531def7d30940330f0f617e007f341b1d
|
4
|
+
data.tar.gz: d06a9954211995b266d08cfbc6586164275d5d269ef5716ba18d4f7e3ca4cf5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
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 =>
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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) {
|
84
|
+
let (:index) { "%{[index_name]}_dynamic" }
|
52
85
|
let(:type) { ESHelper.es_version_satisfies?("< 7") ? "doc" : "_doc" }
|
53
|
-
let(:event_count) { 1
|
54
|
-
let(:
|
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
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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.
|
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(
|
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.
|
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(
|
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.
|
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.
|
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.
|
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-
|
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
|