fluent-plugin-elasticsearch 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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