logstash-output-elasticsearch 11.8.0-java → 11.9.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|