fluent-plugin-elasticsearch2 3.5.5

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.
@@ -0,0 +1,262 @@
1
+ # encoding: UTF-8
2
+ require_relative 'out_elasticsearch'
3
+
4
+ module Fluent::Plugin
5
+ class ElasticsearchOutputDynamic < ElasticsearchOutput
6
+
7
+ Fluent::Plugin.register_output('elasticsearch_dynamic', self)
8
+
9
+ helpers :event_emitter
10
+
11
+ config_param :delimiter, :string, :default => "."
12
+
13
+ DYNAMIC_PARAM_NAMES = %W[hosts host port include_timestamp logstash_format logstash_prefix logstash_dateformat time_key utc_index index_name tag_key type_name id_key parent_key routing_key write_operation]
14
+ DYNAMIC_PARAM_SYMBOLS = DYNAMIC_PARAM_NAMES.map { |n| "@#{n}".to_sym }
15
+
16
+ RequestInfo = Struct.new(:host, :index)
17
+
18
+ attr_reader :dynamic_config
19
+
20
+ def configure(conf)
21
+ super
22
+
23
+ # evaluate all configurations here
24
+ @dynamic_config = {}
25
+ DYNAMIC_PARAM_SYMBOLS.each_with_index { |var, i|
26
+ value = expand_param(self.instance_variable_get(var), nil, nil, nil)
27
+ key = DYNAMIC_PARAM_NAMES[i]
28
+ @dynamic_config[key] = value.to_s
29
+ }
30
+ # end eval all configs
31
+ end
32
+
33
+ def create_meta_config_map
34
+ {'id_key' => '_id', 'parent_key' => '_parent', 'routing_key' => @routing_key_name}
35
+ end
36
+
37
+
38
+ def client(host = nil)
39
+ # check here to see if we already have a client connection for the given host
40
+ connection_options = get_connection_options(host)
41
+
42
+ @_es = nil unless is_existing_connection(connection_options[:hosts])
43
+
44
+ @_es ||= begin
45
+ @current_config = connection_options[:hosts].clone
46
+ adapter_conf = lambda {|f| f.adapter @http_backend, @backend_options }
47
+ transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(connection_options.merge(
48
+ options: {
49
+ reload_connections: @reload_connections,
50
+ reload_on_failure: @reload_on_failure,
51
+ resurrect_after: @resurrect_after,
52
+ logger: @transport_logger,
53
+ transport_options: {
54
+ headers: { 'Content-Type' => @content_type.to_s },
55
+ request: { timeout: @request_timeout },
56
+ ssl: { verify: @ssl_verify, ca_file: @ca_file, version: @ssl_version }
57
+ },
58
+ http: {
59
+ user: @user,
60
+ password: @password
61
+ }
62
+ }), &adapter_conf)
63
+ Elasticsearch::Client.new transport: transport
64
+ end
65
+ end
66
+
67
+ def get_connection_options(con_host)
68
+ raise "`password` must be present if `user` is present" if @user && !@password
69
+
70
+ hosts = if con_host || @hosts
71
+ (con_host || @hosts).split(',').map do |host_str|
72
+ # Support legacy hosts format host:port,host:port,host:port...
73
+ if host_str.match(%r{^[^:]+(\:\d+)?$})
74
+ {
75
+ host: host_str.split(':')[0],
76
+ port: (host_str.split(':')[1] || @port).to_i,
77
+ scheme: @scheme.to_s
78
+ }
79
+ else
80
+ # New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
81
+ uri = URI(get_escaped_userinfo(host_str))
82
+ %w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
83
+ hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
84
+ hash
85
+ end
86
+ end
87
+ end.compact
88
+ else
89
+ [{host: @host, port: @port.to_i, scheme: @scheme.to_s}]
90
+ end.each do |host|
91
+ host.merge!(user: @user, password: @password) if !host[:user] && @user
92
+ host.merge!(path: @path) if !host[:path] && @path
93
+ end
94
+
95
+ {
96
+ hosts: hosts
97
+ }
98
+ end
99
+
100
+ def connection_options_description(host)
101
+ get_connection_options(host)[:hosts].map do |host_info|
102
+ attributes = host_info.dup
103
+ attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
104
+ attributes.inspect
105
+ end.join(', ')
106
+ end
107
+
108
+ def multi_workers_ready?
109
+ true
110
+ end
111
+
112
+ def write(chunk)
113
+ bulk_message = Hash.new { |h,k| h[k] = '' }
114
+ dynamic_conf = @dynamic_config.clone
115
+
116
+ headers = {
117
+ UPDATE_OP => {},
118
+ UPSERT_OP => {},
119
+ CREATE_OP => {},
120
+ INDEX_OP => {}
121
+ }
122
+
123
+ tag = chunk.metadata.tag
124
+
125
+ chunk.msgpack_each do |time, record|
126
+ next unless record.is_a? Hash
127
+
128
+ begin
129
+ # evaluate all configurations here
130
+ DYNAMIC_PARAM_SYMBOLS.each_with_index { |var, i|
131
+ k = DYNAMIC_PARAM_NAMES[i]
132
+ v = self.instance_variable_get(var)
133
+ # check here to determine if we should evaluate
134
+ if dynamic_conf[k] != v
135
+ value = expand_param(v, tag, time, record)
136
+ dynamic_conf[k] = value
137
+ end
138
+ }
139
+ # end eval all configs
140
+ rescue => e
141
+ # handle dynamic parameters misconfigurations
142
+ router.emit_error_event(tag, time, record, e)
143
+ next
144
+ end
145
+
146
+ if eval_or_val(dynamic_conf['logstash_format']) || eval_or_val(dynamic_conf['include_timestamp'])
147
+ if record.has_key?("@timestamp")
148
+ time = Time.parse record["@timestamp"]
149
+ elsif record.has_key?(dynamic_conf['time_key'])
150
+ time = Time.parse record[dynamic_conf['time_key']]
151
+ record['@timestamp'] = record[dynamic_conf['time_key']] unless time_key_exclude_timestamp
152
+ else
153
+ record.merge!({"@timestamp" => Time.at(time).iso8601(@time_precision)})
154
+ end
155
+ end
156
+
157
+ if eval_or_val(dynamic_conf['logstash_format'])
158
+ if eval_or_val(dynamic_conf['utc_index'])
159
+ target_index = "#{dynamic_conf['logstash_prefix']}#{@logstash_prefix_separator}#{Time.at(time).getutc.strftime("#{dynamic_conf['logstash_dateformat']}")}"
160
+ else
161
+ target_index = "#{dynamic_conf['logstash_prefix']}#{@logstash_prefix_separator}#{Time.at(time).strftime("#{dynamic_conf['logstash_dateformat']}")}"
162
+ end
163
+ else
164
+ target_index = dynamic_conf['index_name']
165
+ end
166
+
167
+ # Change target_index to lower-case since Elasticsearch doesn't
168
+ # allow upper-case characters in index names.
169
+ target_index = target_index.downcase
170
+
171
+ if @include_tag_key
172
+ record.merge!(dynamic_conf['tag_key'] => tag)
173
+ end
174
+
175
+ if dynamic_conf['hosts']
176
+ host = dynamic_conf['hosts']
177
+ else
178
+ host = "#{dynamic_conf['host']}:#{dynamic_conf['port']}"
179
+ end
180
+
181
+ if @include_index_in_url
182
+ key = RequestInfo.new(host, target_index)
183
+ meta = {"_type" => dynamic_conf['type_name']}
184
+ else
185
+ key = RequestInfo.new(host, nil)
186
+ meta = {"_index" => target_index, "_type" => dynamic_conf['type_name']}
187
+ end
188
+
189
+ @meta_config_map.each_pair do |config_name, meta_key|
190
+ if dynamic_conf[config_name] && accessor = record_accessor_create(dynamic_conf[config_name])
191
+ if raw_value = accessor.call(record)
192
+ meta[meta_key] = raw_value
193
+ end
194
+ end
195
+ end
196
+
197
+ if @remove_keys
198
+ @remove_keys.each { |key| record.delete(key) }
199
+ end
200
+
201
+ write_op = dynamic_conf["write_operation"]
202
+ append_record_to_messages(write_op, meta, headers[write_op], record, bulk_message[key])
203
+ end
204
+
205
+ bulk_message.each do |info, msgs|
206
+ send_bulk(msgs, info.host, info.index) unless msgs.empty?
207
+ msgs.clear
208
+ end
209
+ end
210
+
211
+ def send_bulk(data, host, index)
212
+ begin
213
+ response = client(host).bulk body: data, index: index
214
+ if response['errors']
215
+ log.error "Could not push log to Elasticsearch: #{response}"
216
+ end
217
+ rescue => e
218
+ @_es = nil if @reconnect_on_error
219
+ # FIXME: identify unrecoverable errors and raise UnrecoverableRequestFailure instead
220
+ raise RecoverableRequestFailure, "could not push logs to Elasticsearch cluster (#{connection_options_description(host)}): #{e.message}"
221
+ end
222
+ end
223
+
224
+ def eval_or_val(var)
225
+ return var unless var.is_a?(String)
226
+ eval(var)
227
+ end
228
+
229
+ def expand_param(param, tag, time, record)
230
+ # check for '${ ... }'
231
+ # yes => `eval`
232
+ # no => return param
233
+ return param if (param =~ /\${.+}/).nil?
234
+
235
+ # check for 'tag_parts[]'
236
+ # separated by a delimiter (default '.')
237
+ tag_parts = tag.split(@delimiter) unless (param =~ /tag_parts\[.+\]/).nil? || tag.nil?
238
+
239
+ # pull out section between ${} then eval
240
+ inner = param.clone
241
+ while inner.match(/\${.+}/)
242
+ to_eval = inner.match(/\${(.+?)}/){$1}
243
+
244
+ if !(to_eval =~ /record\[.+\]/).nil? && record.nil?
245
+ return to_eval
246
+ elsif !(to_eval =~/tag_parts\[.+\]/).nil? && tag_parts.nil?
247
+ return to_eval
248
+ elsif !(to_eval =~/time/).nil? && time.nil?
249
+ return to_eval
250
+ else
251
+ inner.sub!(/\${.+?}/, eval( to_eval ))
252
+ end
253
+ end
254
+ inner
255
+ end
256
+
257
+ def is_valid_expand_param_type(param)
258
+ return false if [:@buffer_type].include?(param)
259
+ return self.instance_variable_get(param).is_a?(String)
260
+ end
261
+ end
262
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter do |src|
4
+ !(src.filename =~ /^#{SimpleCov.root}\/lib/)
5
+ end
6
+ end
7
+
8
+ require 'coveralls'
9
+ Coveralls.wear!
10
+
11
+ # needs to be after simplecov but before test/unit, because fluentd sets default
12
+ # encoding to ASCII-8BIT, but coverall might load git data which could contain a
13
+ # UTF-8 character
14
+ at_exit do
15
+ Encoding.default_internal = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_internal)
16
+ Encoding.default_external = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_external)
17
+ end
18
+
19
+ require 'test/unit'
20
+ require 'fluent/test'
21
+ require 'minitest/pride'
22
+
23
+ require 'webmock/test_unit'
24
+ WebMock.disable_net_connect!
@@ -0,0 +1,9 @@
1
+ {
2
+ "order": 5,
3
+ "template": "--index_prefix-----appid---*",
4
+ "settings": {},
5
+ "mappings": {},
6
+ "aliases": {
7
+ "--appid---alias": {}
8
+ }
9
+ }
@@ -0,0 +1,525 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/out_elasticsearch'
3
+ require 'fluent/plugin/elasticsearch_error_handler'
4
+ require 'json'
5
+
6
+ class TestElasticsearchErrorHandler < Test::Unit::TestCase
7
+
8
+ class TestPlugin
9
+ attr_reader :log
10
+ attr_reader :write_operation, :error_events
11
+ attr_accessor :unrecoverable_error_types
12
+ attr_accessor :log_es_400_reason
13
+ def initialize(log, log_es_400_reason = false)
14
+ @log = log
15
+ @write_operation = 'index'
16
+ @error_events = []
17
+ @unrecoverable_error_types = ["out_of_memory_error", "es_rejected_execution_exception"]
18
+ @log_es_400_reason = log_es_400_reason
19
+ end
20
+
21
+ def router
22
+ self
23
+ end
24
+
25
+ def emit_error_event(tag, time, record, e)
26
+ @error_events << {:tag => tag, :time=>time, :record=>record, :error=>e}
27
+ end
28
+
29
+ def process_message(tag, meta, header, time, record, extracted_values)
30
+ return [meta, header, record]
31
+ end
32
+
33
+ def append_record_to_messages(op, meta, header, record, msgs)
34
+ if record.has_key?('raise') && record['raise']
35
+ raise Exception('process_message')
36
+ end
37
+ return true
38
+ end
39
+ end
40
+
41
+ class MockChunk
42
+ def initialize(records)
43
+ @records = records
44
+ @index = 0
45
+ end
46
+ def msgpack_each
47
+ @records.each { |item| yield(item[:time],item[:record]) }
48
+ end
49
+ end
50
+
51
+ def setup
52
+ Fluent::Test.setup
53
+ @log_device = Fluent::Test::DummyLogDevice.new
54
+ dl_opts = {:log_level => ServerEngine::DaemonLogger::INFO}
55
+ logger = ServerEngine::DaemonLogger.new(@log_device, dl_opts)
56
+ @log = Fluent::Log.new(logger)
57
+ @plugin = TestPlugin.new(@log)
58
+ @handler = Fluent::Plugin::ElasticsearchErrorHandler.new(@plugin)
59
+ end
60
+
61
+ def parse_response(value)
62
+ JSON.parse(value)
63
+ end
64
+
65
+ class TEST400ResponseReason < self
66
+ def setup
67
+ Fluent::Test.setup
68
+ @log_device = Fluent::Test::DummyLogDevice.new
69
+ dl_opts = {:log_level => ServerEngine::DaemonLogger::DEBUG}
70
+ logger = ServerEngine::DaemonLogger.new(@log_device, dl_opts)
71
+ @log = Fluent::Log.new(logger)
72
+ @plugin = TestPlugin.new(@log)
73
+ @handler = Fluent::Plugin::ElasticsearchErrorHandler.new(@plugin)
74
+ end
75
+
76
+ def test_400_responses_reason_log
77
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
78
+ response = parse_response(%({
79
+ "took" : 0,
80
+ "errors" : true,
81
+ "items" : [
82
+ {
83
+ "create" : {
84
+ "_index" : "foo",
85
+ "status" : 400,
86
+ "error" : {
87
+ "type" : "mapper_parsing_exception",
88
+ "reason" : "failed to parse"
89
+ }
90
+ }
91
+ }
92
+ ]
93
+ }))
94
+ chunk = MockChunk.new(records)
95
+ dummy_extracted_values = []
96
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
97
+ assert_equal(1, @plugin.error_events.size)
98
+ expected_log = "failed to parse"
99
+ exception_message = @plugin.error_events.first[:error].message
100
+ assert_true(exception_message.include?(expected_log),
101
+ "Exception do not contain '#{exception_message}' '#{expected_log}'")
102
+ assert_true(@plugin.error_events[0][:error].respond_to?(:backtrace))
103
+ end
104
+ end
105
+
106
+ class TEST400ResponseReasonNoDebug < self
107
+ def setup
108
+ Fluent::Test.setup
109
+ @log_device = Fluent::Test::DummyLogDevice.new
110
+ dl_opts = {:log_level => ServerEngine::DaemonLogger::INFO}
111
+ logger = ServerEngine::DaemonLogger.new(@log_device, dl_opts)
112
+ @log = Fluent::Log.new(logger)
113
+ @plugin = TestPlugin.new(@log)
114
+ @handler = Fluent::Plugin::ElasticsearchErrorHandler.new(@plugin)
115
+ @plugin.log_es_400_reason = true
116
+ end
117
+
118
+ def test_400_responses_reason_log
119
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
120
+ response = parse_response(%({
121
+ "took" : 0,
122
+ "errors" : true,
123
+ "items" : [
124
+ {
125
+ "create" : {
126
+ "_index" : "foo",
127
+ "status" : 400,
128
+ "error" : {
129
+ "type" : "mapper_parsing_exception",
130
+ "reason" : "failed to parse"
131
+ }
132
+ }
133
+ }
134
+ ]
135
+ }))
136
+ chunk = MockChunk.new(records)
137
+ dummy_extracted_values = []
138
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
139
+ assert_equal(1, @plugin.error_events.size)
140
+ expected_log = "failed to parse"
141
+ exception_message = @plugin.error_events.first[:error].message
142
+ assert_true(exception_message.include?(expected_log),
143
+ "Exception do not contain '#{exception_message}' '#{expected_log}'")
144
+ assert_true(@plugin.error_events[0][:error].respond_to?(:backtrace))
145
+ end
146
+ end
147
+
148
+ def test_nil_items_responses
149
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
150
+ response = parse_response(%({
151
+ "took" : 0,
152
+ "errors" : true,
153
+ "items" : [{}]
154
+ }))
155
+ chunk = MockChunk.new(records)
156
+ dummy_extracted_values = []
157
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
158
+ assert_equal(0, @plugin.error_events.size)
159
+ assert_nil(@plugin.error_events[0])
160
+ end
161
+
162
+ def test_blocked_items_responses
163
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
164
+ response = parse_response(%({
165
+ "took" : 0,
166
+ "errors" : true,
167
+ "items" : [
168
+ {
169
+ "create" : {
170
+ "_index" : "foo",
171
+ "status" : 503,
172
+ "error" : "ClusterBlockException[blocked by: [SERVICE_UNAVAILABLE/1/state not recovered / initialized];]"
173
+ }
174
+ }
175
+ ]
176
+ }))
177
+ chunk = MockChunk.new(records)
178
+ dummy_extracted_values = []
179
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
180
+ assert_equal(1, @plugin.error_events.size)
181
+ assert_true(@plugin.error_events[0][:error].respond_to?(:backtrace))
182
+ end
183
+
184
+ def test_dlq_400_responses
185
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
186
+ response = parse_response(%({
187
+ "took" : 0,
188
+ "errors" : true,
189
+ "items" : [
190
+ {
191
+ "create" : {
192
+ "_index" : "foo",
193
+ "status" : 400,
194
+ "_type" : "bar",
195
+ "reason":"unrecognized error"
196
+ }
197
+ }
198
+ ]
199
+ }))
200
+ chunk = MockChunk.new(records)
201
+ dummy_extracted_values = []
202
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
203
+ assert_equal(1, @plugin.error_events.size)
204
+ assert_true(@plugin.error_events[0][:error].respond_to?(:backtrace))
205
+ end
206
+
207
+ def test_out_of_memory_responses
208
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
209
+ response = parse_response(%({
210
+ "took" : 0,
211
+ "errors" : true,
212
+ "items" : [
213
+ {
214
+ "create" : {
215
+ "_index" : "foo",
216
+ "status" : 500,
217
+ "_type" : "bar",
218
+ "error" : {
219
+ "type" : "out_of_memory_error",
220
+ "reason":"Java heap space"
221
+ }
222
+ }
223
+ }
224
+ ]
225
+ }))
226
+
227
+ chunk = MockChunk.new(records)
228
+ dummy_extracted_values = []
229
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchRequestAbortError) do
230
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
231
+ end
232
+ end
233
+
234
+ def test_es_rejected_execution_exception_responses
235
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
236
+ response = parse_response(%({
237
+ "took" : 0,
238
+ "errors" : true,
239
+ "items" : [
240
+ {
241
+ "create" : {
242
+ "_index" : "foo",
243
+ "status" : 429,
244
+ "_type" : "bar",
245
+ "error" : {
246
+ "type" : "es_rejected_execution_exception",
247
+ "reason":"rejected execution of org.elasticsearch.transport.TransportService"
248
+ }
249
+ }
250
+ }
251
+ ]
252
+ }))
253
+
254
+ chunk = MockChunk.new(records)
255
+ dummy_extracted_values = []
256
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchRequestAbortError) do
257
+ @handler.handle_error(response, 'atag', chunk, records.length, dummy_extracted_values)
258
+ end
259
+ end
260
+
261
+ def test_es_rejected_execution_exception_responses_as_not_error
262
+ plugin = TestPlugin.new(@log)
263
+ plugin.unrecoverable_error_types = ["out_of_memory_error"]
264
+ handler = Fluent::Plugin::ElasticsearchErrorHandler.new(plugin)
265
+ records = [{time: 123, record: {"foo" => "bar", '_id' => 'abc'}}]
266
+ response = parse_response(%({
267
+ "took" : 0,
268
+ "errors" : true,
269
+ "items" : [
270
+ {
271
+ "create" : {
272
+ "_index" : "foo",
273
+ "status" : 429,
274
+ "_type" : "bar",
275
+ "error" : {
276
+ "type" : "es_rejected_execution_exception",
277
+ "reason":"rejected execution of org.elasticsearch.transport.TransportService"
278
+ }
279
+ }
280
+ }
281
+ ]
282
+ }))
283
+
284
+ begin
285
+ failed = false
286
+ chunk = MockChunk.new(records)
287
+ dummy_extracted_values = []
288
+ handler.handle_error(response, 'atag', chunk, response['items'].length, dummy_extracted_values)
289
+ rescue Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchRequestAbortError, Fluent::Plugin::ElasticsearchOutput::RetryStreamError=>e
290
+ failed = true
291
+ records = [].tap do |records|
292
+ next unless e.respond_to?(:retry_stream)
293
+ e.retry_stream.each {|time, record| records << record}
294
+ end
295
+ # should retry chunk when unrecoverable error is not thrown
296
+ assert_equal 1, records.length
297
+ end
298
+ assert_true failed
299
+ end
300
+
301
+ def test_retry_error
302
+ records = []
303
+ error_records = Hash.new(false)
304
+ error_records.merge!({0=>true, 4=>true, 9=>true})
305
+ 10.times do |i|
306
+ records << {time: 12345, record: {"message"=>"record #{i}","_id"=>i,"raise"=>error_records[i]}}
307
+ end
308
+ chunk = MockChunk.new(records)
309
+
310
+ response = parse_response(%({
311
+ "took" : 1,
312
+ "errors" : true,
313
+ "items" : [
314
+ {
315
+ "create" : {
316
+ "_index" : "foo",
317
+ "_type" : "bar",
318
+ "_id" : "1",
319
+ "status" : 201
320
+ }
321
+ },
322
+ {
323
+ "create" : {
324
+ "_index" : "foo",
325
+ "_type" : "bar",
326
+ "_id" : "2",
327
+ "status" : 500,
328
+ "error" : {
329
+ "type" : "some unrecognized type",
330
+ "reason":"unrecognized error"
331
+ }
332
+ }
333
+ },
334
+ {
335
+ "create" : {
336
+ "_index" : "foo",
337
+ "_type" : "bar",
338
+ "_id" : "3",
339
+ "status" : 409
340
+ }
341
+ },
342
+ {
343
+ "create" : {
344
+ "_index" : "foo",
345
+ "_type" : "bar",
346
+ "_id" : "5",
347
+ "status" : 500,
348
+ "error" : {
349
+ "reason":"unrecognized error - no type field"
350
+ }
351
+ }
352
+ },
353
+ {
354
+ "create" : {
355
+ "_index" : "foo",
356
+ "_type" : "bar",
357
+ "_id" : "6",
358
+ "status" : 400,
359
+ "error" : {
360
+ "type" : "mapper_parsing_exception",
361
+ "reason":"failed to parse"
362
+ }
363
+ }
364
+ },
365
+ {
366
+ "create" : {
367
+ "_index" : "foo",
368
+ "_type" : "bar",
369
+ "_id" : "7",
370
+ "status" : 400,
371
+ "error" : {
372
+ "type" : "some unrecognized type",
373
+ "reason":"unrecognized error"
374
+ }
375
+ }
376
+ },
377
+ {
378
+ "create" : {
379
+ "_index" : "foo",
380
+ "_type" : "bar",
381
+ "_id" : "8",
382
+ "status" : 500,
383
+ "error" : {
384
+ "type" : "some unrecognized type",
385
+ "reason":"unrecognized error"
386
+ }
387
+ }
388
+ }
389
+ ]
390
+ }))
391
+
392
+ begin
393
+ failed = false
394
+ dummy_extracted_values = []
395
+ @handler.handle_error(response, 'atag', chunk, response['items'].length, dummy_extracted_values)
396
+ rescue Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchRequestAbortError, Fluent::Plugin::ElasticsearchOutput::RetryStreamError=>e
397
+ failed = true
398
+ records = [].tap do |records|
399
+ next unless e.respond_to?(:retry_stream)
400
+ e.retry_stream.each {|time, record| records << record}
401
+ end
402
+ assert_equal 2, records.length
403
+ assert_equal 2, records[0]['_id']
404
+ assert_equal 8, records[1]['_id']
405
+ error_ids = @plugin.error_events.collect {|h| h[:record]['_id']}
406
+ assert_equal 3, error_ids.length
407
+ assert_equal [5, 6, 7], error_ids
408
+ @plugin.error_events.collect {|h| h[:error]}.each do |e|
409
+ assert_true e.respond_to?(:backtrace)
410
+ end
411
+ end
412
+ assert_true failed
413
+
414
+ end
415
+
416
+ def test_unrecoverable_error_included_in_responses
417
+ records = []
418
+ error_records = Hash.new(false)
419
+ error_records.merge!({0=>true, 4=>true, 9=>true})
420
+ 10.times do |i|
421
+ records << {time: 12345, record: {"message"=>"record #{i}","_id"=>i,"raise"=>error_records[i]}}
422
+ end
423
+ chunk = MockChunk.new(records)
424
+
425
+ response = parse_response(%({
426
+ "took" : 1,
427
+ "errors" : true,
428
+ "items" : [
429
+ {
430
+ "create" : {
431
+ "_index" : "foo",
432
+ "_type" : "bar",
433
+ "_id" : "1",
434
+ "status" : 201
435
+ }
436
+ },
437
+ {
438
+ "create" : {
439
+ "_index" : "foo",
440
+ "_type" : "bar",
441
+ "_id" : "2",
442
+ "status" : 500,
443
+ "error" : {
444
+ "type" : "some unrecognized type",
445
+ "reason":"unrecognized error"
446
+ }
447
+ }
448
+ },
449
+ {
450
+ "create" : {
451
+ "_index" : "foo",
452
+ "_type" : "bar",
453
+ "_id" : "3",
454
+ "status" : 409
455
+ }
456
+ },
457
+ {
458
+ "create" : {
459
+ "_index" : "foo",
460
+ "_type" : "bar",
461
+ "_id" : "5",
462
+ "status" : 500,
463
+ "error" : {
464
+ "reason":"unrecognized error - no type field"
465
+ }
466
+ }
467
+ },
468
+ {
469
+ "create" : {
470
+ "_index" : "foo",
471
+ "_type" : "bar",
472
+ "_id" : "6",
473
+ "status" : 500,
474
+ "_type" : "bar",
475
+ "error" : {
476
+ "type" : "out_of_memory_error",
477
+ "reason":"Java heap space"
478
+ }
479
+ }
480
+ },
481
+ {
482
+ "create" : {
483
+ "_index" : "foo",
484
+ "_type" : "bar",
485
+ "_id" : "7",
486
+ "status" : 400,
487
+ "error" : {
488
+ "type" : "some unrecognized type",
489
+ "reason":"unrecognized error"
490
+ }
491
+ }
492
+ },
493
+ {
494
+ "create" : {
495
+ "_index" : "foo",
496
+ "_type" : "bar",
497
+ "_id" : "8",
498
+ "status" : 500,
499
+ "error" : {
500
+ "type" : "some unrecognized type",
501
+ "reason":"unrecognized error"
502
+ }
503
+ }
504
+ }
505
+ ]
506
+ }))
507
+
508
+ begin
509
+ failed = false
510
+ dummy_extracted_values = []
511
+ @handler.handle_error(response, 'atag', chunk, response['items'].length, dummy_extracted_values)
512
+ rescue Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchRequestAbortError, Fluent::Plugin::ElasticsearchOutput::RetryStreamError=>e
513
+ failed = true
514
+ records = [].tap do |records|
515
+ next unless e.respond_to?(:retry_stream)
516
+ e.retry_stream.each {|time, record| records << record}
517
+ end
518
+ # should drop entire chunk when unrecoverable error response is replied
519
+ assert_equal 0, records.length
520
+ end
521
+ assert_true failed
522
+
523
+ end
524
+
525
+ end