fluent-plugin-elasticsearch 2.0.1 → 2.1.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
  SHA1:
3
- metadata.gz: ba424a0c98f88f3fe3a85339d7e71b526de5dbba
4
- data.tar.gz: ff250dcf41528631a3b39cc81628c8a4a7baccd7
3
+ metadata.gz: 16f9f3e664e714be9d1e7fee4b3f8a1683ec9728
4
+ data.tar.gz: beb9fa49cb58739036a3f2c1cda6ee936fbd873d
5
5
  SHA512:
6
- metadata.gz: 0f12b278f5da2a6dcd37c5ac74dd16360753c67ea5fafd6810f501687fd9b96854e08556a87af849a8a1eb92cf14af176cd65346d9677c70d38e25d8f96467cc
7
- data.tar.gz: 4773c01613026ec8f5ac727388ca033dbd9ab900922c5e00fb6709988cc186b67c22f297b26db22bd35ddbbda7f7605a4ba4f2865078d6c0fdc64e06a0338032
6
+ metadata.gz: 8527ebae2dbdb312cc6ed32af668077f9560b74db612a4f5d381f758e4e9cb4aa6230a6859a4a72c3f2b944b1fbf07292a49804cd4648a81392259938f133bab
7
+ data.tar.gz: 887830c553b5ed0350bf64aba4f38d24c28c557fad5493c3ae5f350049269737645e93addfac89b0c252898d07cc127707a9d35f23470ea45cd35f5df48839e0
data/History.md CHANGED
@@ -4,6 +4,9 @@
4
4
  - Log ES response errors (#230)
5
5
  - Use latest elasticsearch-ruby (#240)
6
6
 
7
+ ### 2.1.0
8
+ - Retry on certain errors from Elasticsearch (#322)
9
+
7
10
  ### 2.0.1
8
11
  - Releasing generating hash id mechanism to avoid records duplication feature.
9
12
 
@@ -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 = '2.0.1'
6
+ s.version = '2.1.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,13 @@
1
+ module Fluent
2
+ module Plugin
3
+ module ElasticsearchConstants
4
+ BODY_DELIMITER = "\n".freeze
5
+ UPDATE_OP = "update".freeze
6
+ UPSERT_OP = "upsert".freeze
7
+ CREATE_OP = "create".freeze
8
+ INDEX_OP = "index".freeze
9
+ ID_FIELD = "_id".freeze
10
+ TIMESTAMP_FIELD = "@timestamp".freeze
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,96 @@
1
+ require_relative 'elasticsearch_constants'
2
+
3
+ class Fluent::Plugin::ElasticsearchErrorHandler
4
+ include Fluent::Plugin::ElasticsearchConstants
5
+
6
+ attr_accessor :records, :bulk_message_count
7
+ class BulkIndexQueueFull < StandardError; end
8
+ class ElasticsearchOutOfMemory < StandardError; end
9
+ class ElasticsearchVersionMismatch < StandardError; end
10
+ class UnrecognizedElasticsearchError < StandardError; end
11
+ class ElasticsearchError < StandardError; end
12
+ def initialize(plugin, records = 0, bulk_message_count = 0)
13
+ @plugin = plugin
14
+ @records = records
15
+ @bulk_message_count = bulk_message_count
16
+ end
17
+
18
+ def handle_error(response)
19
+ errors = Hash.new(0)
20
+ errors_bad_resp = 0
21
+ errors_unrecognized = 0
22
+ successes = 0
23
+ duplicates = 0
24
+ bad_arguments = 0
25
+ response['items'].each do |item|
26
+ if item.has_key?(@plugin.write_operation)
27
+ write_operation = @plugin.write_operation
28
+ elsif INDEX_OP == @plugin.write_operation && item.has_key?(CREATE_OP)
29
+ write_operation = CREATE_OP
30
+ else
31
+ # When we don't have an expected ops field, something changed in the API
32
+ # expected return values (ES 2.x)
33
+ errors_bad_resp += 1
34
+ next
35
+ end
36
+ if item[write_operation].has_key?('status')
37
+ status = item[write_operation]['status']
38
+ else
39
+ # When we don't have a status field, something changed in the API
40
+ # expected return values (ES 2.x)
41
+ errors_bad_resp += 1
42
+ next
43
+ end
44
+ case
45
+ when CREATE_OP == write_operation && 409 == status
46
+ duplicates += 1
47
+ when 400 == status
48
+ bad_arguments += 1
49
+ @plugin.log.debug "Elasticsearch rejected document: #{item}"
50
+ when [429, 500].include?(status)
51
+ if item[write_operation].has_key?('error') && item[write_operation]['error'].has_key?('type')
52
+ type = item[write_operation]['error']['type']
53
+ else
54
+ # When we don't have a type field, something changed in the API
55
+ # expected return values (ES 2.x)
56
+ errors_bad_resp += 1
57
+ next
58
+ end
59
+ errors[type] += 1
60
+ when [200, 201].include?(status)
61
+ successes += 1
62
+ else
63
+ errors_unrecognized += 1
64
+ end
65
+ end
66
+ if errors_bad_resp > 0
67
+ msg = "Unable to parse error response from Elasticsearch, likely an API version mismatch #{response}"
68
+ @plugin.log.error msg
69
+ raise ElasticsearchVersionMismatch, msg
70
+ end
71
+ if bad_arguments > 0
72
+ @plugin.log.warn "Elasticsearch rejected #{bad_arguments} documents due to invalid field arguments"
73
+ end
74
+ if duplicates > 0
75
+ @plugin.log.info "Encountered #{duplicates} duplicate(s) of #{successes} indexing chunk, ignoring"
76
+ end
77
+ msg = "Indexed (op = #{@plugin.write_operation}) #{successes} successfully, #{duplicates} duplicate(s), #{bad_arguments} bad argument(s), #{errors_unrecognized} unrecognized error(s)"
78
+ errors.each_key do |key|
79
+ msg << ", #{errors[key]} #{key} error(s)"
80
+ end
81
+ @plugin.log.debug msg
82
+ if errors_unrecognized > 0
83
+ raise UnrecognizedElasticsearchError, "Unrecognized elasticsearch errors returned, retrying #{response}"
84
+ end
85
+ errors.each_key do |key|
86
+ case key
87
+ when 'out_of_memory_error'
88
+ raise ElasticsearchOutOfMemory, "Elasticsearch has exhausted its heap, retrying"
89
+ when 'es_rejected_execution_exception'
90
+ raise BulkIndexQueueFull, "Bulk index queue is full, retrying"
91
+ else
92
+ raise ElasticsearchError, "Elasticsearch errors returned, retrying #{response}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -10,6 +10,8 @@ rescue LoadError
10
10
  end
11
11
 
12
12
  require 'fluent/plugin/output'
13
+ require_relative 'elasticsearch_constants'
14
+ require_relative 'elasticsearch_error_handler'
13
15
  require_relative 'elasticsearch_index_template'
14
16
  require_relative 'generate_hash_id_support'
15
17
 
@@ -81,6 +83,7 @@ module Fluent::Plugin
81
83
 
82
84
  include Fluent::ElasticsearchIndexTemplate
83
85
  include Fluent::Plugin::GenerateHashIdSupport
86
+ include Fluent::Plugin::ElasticsearchConstants
84
87
 
85
88
  def initialize
86
89
  super
@@ -251,14 +254,6 @@ module Fluent::Plugin
251
254
  end.join(', ')
252
255
  end
253
256
 
254
- BODY_DELIMITER = "\n".freeze
255
- UPDATE_OP = "update".freeze
256
- UPSERT_OP = "upsert".freeze
257
- CREATE_OP = "create".freeze
258
- INDEX_OP = "index".freeze
259
- ID_FIELD = "_id".freeze
260
- TIMESTAMP_FIELD = "@timestamp".freeze
261
-
262
257
  def append_record_to_messages(op, meta, header, record, msgs)
263
258
  case op
264
259
  when UPDATE_OP, UPSERT_OP
@@ -334,8 +329,10 @@ module Fluent::Plugin
334
329
 
335
330
  tag = chunk.metadata.tag
336
331
  logstash_prefix, index_name = expand_placeholders(chunk.metadata)
332
+ @error = Fluent::Plugin::ElasticsearchErrorHandler.new(self)
337
333
 
338
334
  chunk.msgpack_each do |time, record|
335
+ @error.records += 1
339
336
  next unless record.is_a? Hash
340
337
 
341
338
  if @flatten_hashes
@@ -402,6 +399,7 @@ module Fluent::Plugin
402
399
  end
403
400
 
404
401
  append_record_to_messages(@write_operation, meta, header, record, bulk_message)
402
+ @error.bulk_message_count += 1
405
403
  end
406
404
 
407
405
  send_bulk(bulk_message) unless bulk_message.empty?
@@ -420,6 +418,7 @@ module Fluent::Plugin
420
418
  begin
421
419
  response = client.bulk body: data
422
420
  if response['errors']
421
+ @error.handle_error(response)
423
422
  log.error "Could not push log to Elasticsearch: #{response}"
424
423
  end
425
424
  rescue *client.transport.host_unreachable_exceptions => e
@@ -1,6 +1,7 @@
1
1
  require 'helper'
2
2
  require 'date'
3
3
  require 'fluent/test/helpers'
4
+ require 'json'
4
5
  require 'fluent/test/driver/output'
5
6
  require 'flexmock/test_unit'
6
7
 
@@ -59,6 +60,130 @@ class ElasticsearchOutput < Test::Unit::TestCase
59
60
  end
60
61
  end
61
62
 
63
+ def make_response_body(req, error_el = nil, error_status = nil, error = nil)
64
+ req_index_cmds = req.body.split("\n").map { |r| JSON.parse(r) }
65
+ items = []
66
+ count = 0
67
+ ids = 1
68
+ op = nil
69
+ index = nil
70
+ type = nil
71
+ id = nil
72
+ req_index_cmds.each do |cmd|
73
+ if count.even?
74
+ op = cmd.keys[0]
75
+ index = cmd[op]['_index']
76
+ type = cmd[op]['_type']
77
+ if cmd[op].has_key?('_id')
78
+ id = cmd[op]['_id']
79
+ else
80
+ # Note: this appears to be an undocumented feature of Elasticsearch
81
+ # https://www.elastic.co/guide/en/elasticsearch/reference/2.4/docs-bulk.html
82
+ # When you submit an "index" write_operation, with no "_id" field in the
83
+ # metadata header, Elasticsearch will turn this into a "create"
84
+ # operation in the response.
85
+ if "index" == op
86
+ op = "create"
87
+ end
88
+ id = ids
89
+ ids += 1
90
+ end
91
+ else
92
+ item = {
93
+ op => {
94
+ '_index' => index, '_type' => type, '_id' => id, '_version' => 1,
95
+ '_shards' => { 'total' => 1, 'successful' => 1, 'failed' => 0 },
96
+ 'status' => op == 'create' ? 201 : 200
97
+ }
98
+ }
99
+ items.push(item)
100
+ end
101
+ count += 1
102
+ end
103
+ if !error_el.nil? && !error_status.nil? && !error.nil?
104
+ op = items[error_el].keys[0]
105
+ items[error_el][op].delete('_version')
106
+ items[error_el][op].delete('_shards')
107
+ items[error_el][op]['error'] = error
108
+ items[error_el][op]['status'] = error_status
109
+ errors = true
110
+ else
111
+ errors = false
112
+ end
113
+ @index_cmds = items
114
+ body = { 'took' => 6, 'errors' => errors, 'items' => items }
115
+ return body.to_json
116
+ end
117
+
118
+ def stub_elastic_bad_argument(url="http://localhost:9200/_bulk")
119
+ error = {
120
+ "type" => "mapper_parsing_exception",
121
+ "reason" => "failed to parse [...]",
122
+ "caused_by" => {
123
+ "type" => "illegal_argument_exception",
124
+ "reason" => "Invalid format: \"...\""
125
+ }
126
+ }
127
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 400, error), :headers => { 'Content-Type' => 'json' } } })
128
+ end
129
+
130
+ def stub_elastic_bulk_error(url="http://localhost:9200/_bulk")
131
+ error = {
132
+ "type" => "some-unrecognized-error",
133
+ "reason" => "some message printed here ...",
134
+ }
135
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 500, error), :headers => { 'Content-Type' => 'json' } } })
136
+ end
137
+
138
+ def stub_elastic_bulk_rejected(url="http://localhost:9200/_bulk")
139
+ error = {
140
+ "type" => "es_rejected_execution_exception",
141
+ "reason" => "rejected execution of org.elasticsearch.transport.TransportService$4@1a34d37a on EsThreadPoolExecutor[bulk, queue capacity = 50, org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor@312a2162[Running, pool size = 32, active threads = 32, queued tasks = 50, completed tasks = 327053]]"
142
+ }
143
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 429, error), :headers => { 'Content-Type' => 'json' } } })
144
+ end
145
+
146
+ def stub_elastic_out_of_memory(url="http://localhost:9200/_bulk")
147
+ error = {
148
+ "type" => "out_of_memory_error",
149
+ "reason" => "Java heap space"
150
+ }
151
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 500, error), :headers => { 'Content-Type' => 'json' } } })
152
+ end
153
+
154
+ def stub_elastic_unrecognized_error(url="http://localhost:9200/_bulk")
155
+ error = {
156
+ "type" => "some-other-type",
157
+ "reason" => "some-other-reason"
158
+ }
159
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 504, error), :headers => { 'Content-Type' => 'json' } } })
160
+ end
161
+
162
+ def stub_elastic_version_mismatch(url="http://localhost:9200/_bulk")
163
+ error = {
164
+ "category" => "some-other-type",
165
+ "reason" => "some-other-reason"
166
+ }
167
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 1, 500, error), :headers => { 'Content-Type' => 'json' } } })
168
+ end
169
+
170
+ def stub_elastic_index_to_create(url="http://localhost:9200/_bulk")
171
+ error = {
172
+ "category" => "some-other-type",
173
+ "reason" => "some-other-reason",
174
+ "type" => "some-other-type"
175
+ }
176
+ stub_request(:post, url).to_return(lambda { |req| { :status => 200, :body => make_response_body(req, 0, 500, error), :headers => { 'Content-Type' => 'json' } } })
177
+ end
178
+
179
+ def stub_elastic_unexpected_response_op(url="http://localhost:9200/_bulk")
180
+ error = {
181
+ "category" => "some-other-type",
182
+ "reason" => "some-other-reason"
183
+ }
184
+ 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' } } })
185
+ end
186
+
62
187
  def test_configure
63
188
  config = %{
64
189
  host logs.google.com
@@ -1394,6 +1519,112 @@ class ElasticsearchOutput < Test::Unit::TestCase
1394
1519
  assert_equal(connection_resets, 1)
1395
1520
  end
1396
1521
 
1522
+ def test_bulk_bad_arguments
1523
+ driver = driver('@log_level debug')
1524
+
1525
+ stub_elastic_ping
1526
+ stub_elastic_bad_argument
1527
+
1528
+ driver.run(default_tag: 'test', shutdown: false) do
1529
+ driver.feed(sample_record)
1530
+ driver.feed(sample_record)
1531
+ driver.feed(sample_record)
1532
+ end
1533
+
1534
+ matches = driver.logs.grep /Elasticsearch rejected document:/
1535
+ assert_equal(1, matches.length, "Message 'Elasticsearch rejected document: ...' was not emitted")
1536
+ matches = driver.logs.grep /documents due to invalid field arguments/
1537
+ assert_equal(1, matches.length, "Message 'Elasticsearch rejected # documents due to invalid field arguments ...' was not emitted")
1538
+ end
1539
+
1540
+ def test_bulk_error
1541
+ stub_elastic_ping
1542
+ stub_elastic_bulk_error
1543
+
1544
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchError) {
1545
+ driver.run(default_tag: 'test', shutdown: false) do
1546
+ driver.feed(sample_record)
1547
+ driver.feed(sample_record)
1548
+ driver.feed(sample_record)
1549
+ end
1550
+ }
1551
+ end
1552
+
1553
+ def test_bulk_error_version_mismatch
1554
+ stub_elastic_ping
1555
+ stub_elastic_version_mismatch
1556
+
1557
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchVersionMismatch) {
1558
+ driver.run(default_tag: 'test', shutdown: false) do
1559
+ driver.feed(sample_record)
1560
+ driver.feed(sample_record)
1561
+ driver.feed(sample_record)
1562
+ end
1563
+ }
1564
+ end
1565
+
1566
+ def test_bulk_error_unrecognized_error
1567
+ stub_elastic_ping
1568
+ stub_elastic_unrecognized_error
1569
+
1570
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::UnrecognizedElasticsearchError) {
1571
+ driver.run(default_tag: 'test', shutdown: false) do
1572
+ driver.feed(sample_record)
1573
+ driver.feed(sample_record)
1574
+ driver.feed(sample_record)
1575
+ end
1576
+ }
1577
+ end
1578
+
1579
+ def test_bulk_error_out_of_memory
1580
+ stub_elastic_ping
1581
+ stub_elastic_out_of_memory
1582
+
1583
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchOutOfMemory) {
1584
+ driver.run(default_tag: 'test', shutdown: false) do
1585
+ driver.feed(sample_record)
1586
+ driver.feed(sample_record)
1587
+ driver.feed(sample_record)
1588
+ end
1589
+ }
1590
+ end
1591
+
1592
+ def test_bulk_error_queue_full
1593
+ stub_elastic_ping
1594
+ stub_elastic_bulk_rejected
1595
+
1596
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::BulkIndexQueueFull) {
1597
+ driver.run(default_tag: 'test', shutdown: false) do
1598
+ driver.feed(sample_record)
1599
+ driver.feed(sample_record)
1600
+ driver.feed(sample_record)
1601
+ end
1602
+ }
1603
+ end
1604
+
1605
+ def test_bulk_index_into_a_create
1606
+ stub_elastic_ping
1607
+ stub_elastic_index_to_create
1608
+
1609
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchError) {
1610
+ driver.run(default_tag: 'test', shutdown: false) do
1611
+ driver.feed(sample_record)
1612
+ end
1613
+ }
1614
+ assert(index_cmds[0].has_key?("create"))
1615
+ end
1616
+
1617
+ def test_bulk_unexpected_response_op
1618
+ stub_elastic_ping
1619
+ stub_elastic_unexpected_response_op
1620
+
1621
+ assert_raise(Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchVersionMismatch) {
1622
+ driver.run(default_tag: 'test', shutdown: false) do
1623
+ driver.feed(sample_record)
1624
+ end
1625
+ }
1626
+ end
1627
+
1397
1628
  def test_update_should_not_write_if_theres_no_id
1398
1629
  driver.configure("write_operation update\n")
1399
1630
  stub_elastic_ping
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: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - diogo
@@ -143,6 +143,8 @@ files:
143
143
  - README.md
144
144
  - Rakefile
145
145
  - fluent-plugin-elasticsearch.gemspec
146
+ - lib/fluent/plugin/elasticsearch_constants.rb
147
+ - lib/fluent/plugin/elasticsearch_error_handler.rb
146
148
  - lib/fluent/plugin/elasticsearch_index_template.rb
147
149
  - lib/fluent/plugin/generate_hash_id_support.rb
148
150
  - lib/fluent/plugin/out_elasticsearch.rb