fluent-plugin-elasticsearch 1.13.4 → 1.14.0

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: 35b11b6ba12d6bfa22a084db14fd4503d2f16769dc9db7d30d22dafa3dbad035
4
- data.tar.gz: a0812a380eb81188fa705924ae51df7bbc773b0d565469f48540807bc323ec33
3
+ metadata.gz: 995e9b0bb1ae8dc5fbf60b80c6d038c0b1f5b696aa4e27842ab23b1609eeef74
4
+ data.tar.gz: c1b249d0d02c7381b0a115fccad53e68ea170906101913c36a2b52c294bb3275
5
5
  SHA512:
6
- metadata.gz: 942a5d5e57f7fa81697cdc13ad0024ab40df80d7a2a45a1108cd9acb1909ad87c5b0e4e220ce4df91d35949a12ae413fb0209c59d6f8ddb908beefa2c257799b
7
- data.tar.gz: f4f4d3d3c1e0cb2800a31516052a5ca3aecd5ba0c451bcb3dcbf0e91d7e492bbf8da4caf4f1871d24e898276fb8758c76ae52813ade2ab6884bf061de73cff63
6
+ metadata.gz: 158848be63d3b28213856a45026b65e769e480285b43400a5600a7f5e7d01aed24f44bbbc9fad60d0530f64dad2717cc90d57c0ac84f75c1f46c3f439ab56a5f
7
+ data.tar.gz: 223f97c244f7488f364d3a553e44b0d1fddf4c3f9c93545dd696e84e6a2bfd92abc8565e42ce7b5c402e4ca9bca242b6ac66a88337165579b6b818a7d3261ff2
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .DS_Store
19
+ vendor/
data/History.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ### [Unreleased]
4
4
 
5
+ ### 1.14.0
6
+ - introduce dead letter queue to handle issues unpacking file buffer chunks (#398)
7
+
5
8
  ### 1.13.4
6
9
  - backport auth: Fix missing auth tokens after reloading connections (#397)
7
10
 
data/README.md CHANGED
@@ -49,6 +49,7 @@ Note: For Amazon Elasticsearch Service please consider using [fluent-plugin-aws-
49
49
  + [time_parse_error_tag](#time_parse_error_tag)
50
50
  + [reconnect_on_error](#reconnect_on_error)
51
51
  + [with_transporter_log](#with_transporter_log)
52
+ + [dlq_handler](#dlq_handler)
52
53
  + [Client/host certificate options](#clienthost-certificate-options)
53
54
  + [Proxy Support](#proxy-support)
54
55
  + [Buffered output options](#buffered-output-options)
@@ -474,6 +475,17 @@ We recommend to set this true if you start to debug this plugin.
474
475
  with_transporter_log true
475
476
  ```
476
477
 
478
+ ### dlq_handler
479
+ Adds an error handler for processing corrupt messages from message buffers.
480
+ There are [known cases](https://bugzilla.redhat.com/show_bug.cgi?id=1562004) where
481
+ fluentd is stuck processing messages because the file buffer is corrupt. Fluentd
482
+ is unable to clear faulty buffer chunks.
483
+
484
+ ```
485
+ dlq_handler {'type':'drop'} #default is to log and drop messages
486
+ dlq_handler {'type':'file', 'dir':'/tmp/fluentd/dlq', 'max_files':5, 'max_file_size':104857600}
487
+ ```
488
+
477
489
  ### Client/host certificate options
478
490
 
479
491
  Need to verify Elasticsearch's certificate? You can use the following parameter to specify a CA instead of using an environment variable.
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'fluent-plugin-elasticsearch'
6
- s.version = '1.13.4'
6
+ s.version = '1.14.0'
7
7
  s.authors = ['diogo', 'pitr']
8
8
  s.email = ['pitr.vern@gmail.com', 'me@diogoterror.com']
9
9
  s.description = %q{Elasticsearch output plugin for Fluent event collector}
@@ -0,0 +1,10 @@
1
+
2
+ module Fluent::DeadLetterQueueDropHandler
3
+ def handle_chunk_error(out_plugin, tag, error, time, record)
4
+ begin
5
+ log.error("Dropping record from '#{tag}': error:#{error} time:#{time} record:#{record}")
6
+ rescue=>e
7
+ log.error("Error while trying to log and drop message from chunk '#{tag}' #{e.message}")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+
3
+ module Fluent::DeadLetterQueueFileHandler
4
+
5
+ def handle_chunk_error(out_plugin, tag, error, time, record)
6
+ begin
7
+ @dlq_file.info({processed_at: Time.now.utc, tag: tag, error: "#{error.message}", time: time, record: record}.to_json)
8
+ rescue=>e
9
+ log.error("Error while trying to log and drop message from chunk '#{tag}' #{e.message}")
10
+ end
11
+ end
12
+
13
+ end
14
+
@@ -69,6 +69,7 @@ class Fluent::ElasticsearchOutput < Fluent::ObjectBufferedOutput
69
69
  config_param :reconnect_on_error, :bool, :default => false
70
70
  config_param :pipeline, :string, :default => nil
71
71
  config_param :with_transporter_log, :bool, :default => false
72
+ config_param :dlq_handler, :hash, :default => { 'type' =>'drop' }
72
73
 
73
74
  include Fluent::ElasticsearchIndexTemplate
74
75
  include Fluent::ElasticsearchConstants
@@ -129,6 +130,44 @@ class Fluent::ElasticsearchOutput < Fluent::ObjectBufferedOutput
129
130
  log_level = conf['@log_level'] || conf['log_level']
130
131
  log.warn "Consider to specify log_level with @log_level." unless log_level
131
132
  end
133
+
134
+ configure_dlq_handler
135
+
136
+ end
137
+
138
+ def configure_dlq_handler
139
+ dlq_type = @dlq_handler && @dlq_handler.is_a?(Hash) ? dlq_type = @dlq_handler['type'] : nil
140
+ return unless dlq_type
141
+
142
+ case dlq_type.downcase
143
+ when 'drop'
144
+ log.info('Configuring the DROP dead letter queue handler')
145
+ require_relative 'dead_letter_queue_drop_handler'
146
+ extend Fluent::DeadLetterQueueDropHandler
147
+ when 'file'
148
+ log.info("Configuring the File dead letter queue handler: ")
149
+ dir = @dlq_handler ['dir'] || '/var/lib/fluentd/dlq'
150
+ shift_age = @dlq_handler['max_files'] || 0
151
+ shift_size = @dlq_handler['max_file_size'] || 1048576
152
+ log.info("Configuring the File dead letter queue handler: ")
153
+ log.info(" Directory: #{dir}")
154
+ log.info(" Max number of DLQ files: #{shift_age}")
155
+ log.info(" Max file size: #{shift_size}")
156
+ unless Dir.exists?(dir)
157
+ Dir.mkdir(dir)
158
+ log.info("Created DLQ directory: '#{dir}'")
159
+ end
160
+ require 'logger'
161
+ require 'json'
162
+ file = File.join(dir, 'dlq')
163
+ @dlq_file = Logger.new(file, shift_age, shift_size)
164
+ @dlq_file.level = Logger::INFO
165
+ @dlq_file.formatter = proc { |severity, datetime, progname, msg| "#{msg.dump}\n" }
166
+ log.info ("Created DLQ file #{file}")
167
+
168
+ require_relative 'dead_letter_queue_file_handler'
169
+ extend Fluent::DeadLetterQueueFileHandler
170
+ end
132
171
  end
133
172
 
134
173
  def create_meta_config_map
@@ -321,76 +360,83 @@ class Fluent::ElasticsearchOutput < Fluent::ObjectBufferedOutput
321
360
  chunk.msgpack_each do |time, record|
322
361
  @error.records += 1
323
362
  next unless record.is_a? Hash
324
-
325
- if @flatten_hashes
326
- record = flatten_record(record)
363
+ begin
364
+ process_message(tag, meta, header, time, record, bulk_message)
365
+ rescue=>e
366
+ handle_chunk_error(self, tag, e, time, record)
327
367
  end
368
+ end
328
369
 
329
- if @hash_config
330
- record = generate_hash_id_key(record)
331
- end
370
+ send_bulk(bulk_message) unless bulk_message.empty?
371
+ bulk_message.clear
372
+ end
332
373
 
333
- dt = nil
334
- if @logstash_format || @include_timestamp
335
- if record.has_key?(TIMESTAMP_FIELD)
336
- rts = record[TIMESTAMP_FIELD]
337
- dt = parse_time(rts, time, tag)
338
- elsif record.has_key?(@time_key)
339
- rts = record[@time_key]
340
- dt = parse_time(rts, time, tag)
341
- record[TIMESTAMP_FIELD] = rts unless @time_key_exclude_timestamp
342
- else
343
- dt = Time.at(time).to_datetime
344
- record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision)
345
- end
346
- end
374
+ def process_message(tag, meta, header, time, record, bulk_message)
375
+ if @flatten_hashes
376
+ record = flatten_record(record)
377
+ end
347
378
 
348
- target_index_parent, target_index_child_key = @target_index_key ? get_parent_of(record, @target_index_key) : nil
349
- if target_index_parent && target_index_parent[target_index_child_key]
350
- target_index = target_index_parent.delete(target_index_child_key)
351
- elsif @logstash_format
352
- dt = dt.new_offset(0) if @utc_index
353
- target_index = "#{@logstash_prefix}#{@logstash_prefix_separator}#{dt.strftime(@logstash_dateformat)}"
379
+ if @hash_config
380
+ record = generate_hash_id_key(record)
381
+ end
382
+
383
+ dt = nil
384
+ if @logstash_format || @include_timestamp
385
+ if record.has_key?(TIMESTAMP_FIELD)
386
+ rts = record[TIMESTAMP_FIELD]
387
+ dt = parse_time(rts, time, tag)
388
+ elsif record.has_key?(@time_key)
389
+ rts = record[@time_key]
390
+ dt = parse_time(rts, time, tag)
391
+ record[TIMESTAMP_FIELD] = rts unless @time_key_exclude_timestamp
354
392
  else
355
- target_index = @index_name
393
+ dt = Time.at(time).to_datetime
394
+ record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision)
356
395
  end
396
+ end
357
397
 
358
- # Change target_index to lower-case since Elasticsearch doesn't
359
- # allow upper-case characters in index names.
360
- target_index = target_index.downcase
361
- if @include_tag_key
362
- record[@tag_key] = tag
363
- end
398
+ target_index_parent, target_index_child_key = @target_index_key ? get_parent_of(record, @target_index_key) : nil
399
+ if target_index_parent && target_index_parent[target_index_child_key]
400
+ target_index = target_index_parent.delete(target_index_child_key)
401
+ elsif @logstash_format
402
+ dt = dt.new_offset(0) if @utc_index
403
+ target_index = "#{@logstash_prefix}#{@logstash_prefix_separator}#{dt.strftime(@logstash_dateformat)}"
404
+ else
405
+ target_index = @index_name
406
+ end
364
407
 
365
- target_type_parent, target_type_child_key = @target_type_key ? get_parent_of(record, @target_type_key) : nil
366
- if target_type_parent && target_type_parent[target_type_child_key]
367
- target_type = target_type_parent.delete(target_type_child_key)
368
- else
369
- target_type = @type_name
370
- end
408
+ # Change target_index to lower-case since Elasticsearch doesn't
409
+ # allow upper-case characters in index names.
410
+ target_index = target_index.downcase
411
+ if @include_tag_key
412
+ record[@tag_key] = tag
413
+ end
371
414
 
372
- meta.clear
373
- meta["_index".freeze] = target_index
374
- meta["_type".freeze] = target_type
415
+ target_type_parent, target_type_child_key = @target_type_key ? get_parent_of(record, @target_type_key) : nil
416
+ if target_type_parent && target_type_parent[target_type_child_key]
417
+ target_type = target_type_parent.delete(target_type_child_key)
418
+ else
419
+ target_type = @type_name
420
+ end
375
421
 
376
- if @pipeline
377
- meta["pipeline".freeze] = @pipeline
378
- end
422
+ meta.clear
423
+ meta["_index".freeze] = target_index
424
+ meta["_type".freeze] = target_type
379
425
 
380
- @meta_config_map.each do |record_key, meta_key|
381
- meta[meta_key] = record[record_key] if record[record_key]
382
- end
426
+ if @pipeline
427
+ meta["pipeline".freeze] = @pipeline
428
+ end
383
429
 
384
- if @remove_keys
385
- @remove_keys.each { |key| record.delete(key) }
386
- end
430
+ @meta_config_map.each do |record_key, meta_key|
431
+ meta[meta_key] = record[record_key] if record[record_key]
432
+ end
387
433
 
388
- append_record_to_messages(@write_operation, meta, header, record, bulk_message)
389
- @error.bulk_message_count += 1
434
+ if @remove_keys
435
+ @remove_keys.each { |key| record.delete(key) }
390
436
  end
391
437
 
392
- send_bulk(bulk_message) unless bulk_message.empty?
393
- bulk_message.clear
438
+ append_record_to_messages(@write_operation, meta, header, record, bulk_message)
439
+ @error.bulk_message_count += 1
394
440
  end
395
441
 
396
442
  # returns [parent, child_key] of child described by path array in record's tree
@@ -37,7 +37,7 @@ class ElasticsearchOutput < Test::Unit::TestCase
37
37
 
38
38
  def stub_elastic(url="http://localhost:9200/_bulk")
39
39
  stub_request(:post, url).with do |req|
40
- @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
40
+ @index_cmds = req.body.split("\n").map {|r| JSON.parse(r) }
41
41
  end
42
42
  end
43
43
 
@@ -181,6 +181,11 @@ class ElasticsearchOutput < Test::Unit::TestCase
181
181
  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' } } })
182
182
  end
183
183
 
184
+ def assert_logs_include(logs, msg)
185
+ matches = logs.grep /#{msg}/
186
+ assert_equal(1, matches.length, "Logs do not contain '#{msg}' '#{logs}'")
187
+ end
188
+
184
189
  def test_configure
185
190
  config = %{
186
191
  host logs.google.com
@@ -203,6 +208,38 @@ class ElasticsearchOutput < Test::Unit::TestCase
203
208
  assert_nil instance.client_cert
204
209
  assert_nil instance.client_key_pass
205
210
  assert_false instance.with_transporter_log
211
+ assert_not_nil instance.dlq_handler
212
+ assert_equal 'drop', instance.dlq_handler['type']
213
+ end
214
+
215
+ def test_configure_with_dlq_file_handler
216
+ require 'tmpdir'
217
+ dir = Dir.mktmpdir
218
+ config = %Q{
219
+ host logs.google.com
220
+ port 777
221
+ scheme https
222
+ path /es/
223
+ user john
224
+ password doe
225
+ dlq_handler {"type":"file", "dir":"#{dir}"}
226
+ }
227
+ instance = driver('test', config).instance
228
+
229
+ assert_equal 'logs.google.com', instance.host
230
+ assert_equal 777, instance.port
231
+ assert_equal 'https', instance.scheme
232
+ assert_equal '/es/', instance.path
233
+ assert_equal 'john', instance.user
234
+ assert_equal 'doe', instance.password
235
+ assert_equal :TLSv1, instance.ssl_version
236
+ assert_nil instance.client_key
237
+ assert_nil instance.client_cert
238
+ assert_nil instance.client_key_pass
239
+ assert_false instance.with_transporter_log
240
+ assert_not_nil instance.dlq_handler
241
+ assert_equal 'file', instance.dlq_handler['type']
242
+ assert_true Dir.exists?(dir)
206
243
  end
207
244
 
208
245
  def test_template_already_present
@@ -592,6 +629,30 @@ class ElasticsearchOutput < Test::Unit::TestCase
592
629
  assert_requested(elastic_request)
593
630
  end
594
631
 
632
+ def test_write_message_with_dlq_drop_handler
633
+ driver.configure("target_index_key bad_value\n")
634
+ log = driver.instance.router.emit_error_handler.log
635
+ stub_elastic_ping
636
+ stub_elastic
637
+ driver.emit({'bad_value'=>"\255"})
638
+ driver.run
639
+ assert_logs_include(log.out.logs, 'Dropping')
640
+ end
641
+
642
+ def test_write_message_with_dlq_file_handler
643
+ log = driver.instance.router.emit_error_handler.log
644
+ dir = Dir.mktmpdir
645
+ driver.configure("dlq_handler {\"type\":\"file\", \"dir\":\"#{dir}\"}\n
646
+ target_index_key bad_value\n
647
+ ")
648
+ stub_elastic_ping
649
+ stub_elastic
650
+ driver.emit({'bad_value'=>"\255"})
651
+ driver.run
652
+ logs = File.readlines(File.join(dir,'dlq'))
653
+ assert_logs_include(logs, 'invalid')
654
+ end
655
+
595
656
  def test_writes_to_default_index
596
657
  stub_elastic_ping
597
658
  stub_elastic
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-elasticsearch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.4
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - diogo
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-04-10 00:00:00.000000000 Z
12
+ date: 2018-04-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fluentd
@@ -144,6 +144,8 @@ files:
144
144
  - README.md
145
145
  - Rakefile
146
146
  - fluent-plugin-elasticsearch.gemspec
147
+ - lib/fluent/plugin/dead_letter_queue_drop_handler.rb
148
+ - lib/fluent/plugin/dead_letter_queue_file_handler.rb
147
149
  - lib/fluent/plugin/elasticsearch_constants.rb
148
150
  - lib/fluent/plugin/elasticsearch_error_handler.rb
149
151
  - lib/fluent/plugin/elasticsearch_index_template.rb