fluent-plugin-elasticsearch 3.5.3 → 3.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,110 +1,112 @@
1
- require 'fluent/error'
2
- require_relative './elasticsearch_error'
3
-
4
- module Fluent::ElasticsearchIndexTemplate
5
- def get_template(template_file)
6
- if !File.exists?(template_file)
7
- raise "If you specify a template_name you must specify a valid template file (checked '#{template_file}')!"
8
- end
9
- file_contents = IO.read(template_file).gsub(/\n/,'')
10
- JSON.parse(file_contents)
11
- end
12
-
13
- def get_custom_template(template_file, customize_template)
14
- if !File.exists?(template_file)
15
- raise "If you specify a template_name you must specify a valid template file (checked '#{template_file}')!"
16
- end
17
- file_contents = IO.read(template_file).gsub(/\n/,'')
18
- customize_template.each do |key, value|
19
- file_contents = file_contents.gsub(key,value.downcase)
20
- end
21
- JSON.parse(file_contents)
22
- end
23
-
24
- def template_exists?(name)
25
- client.indices.get_template(:name => name)
26
- return true
27
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
28
- return false
29
- end
30
-
31
- def retry_operate(max_retries, fail_on_retry_exceed = true)
32
- return unless block_given?
33
- retries = 0
34
- begin
35
- yield
36
- rescue *client.transport.host_unreachable_exceptions, Timeout::Error => e
37
- @_es = nil
38
- @_es_info = nil
39
- if retries < max_retries
40
- retries += 1
41
- sleep 2**retries
42
- log.warn "Could not communicate to Elasticsearch, resetting connection and trying again. #{e.message}"
43
- retry
44
- end
45
- message = "Could not communicate to Elasticsearch after #{retries} retries. #{e.message}"
46
- log.warn message
47
- raise Fluent::Plugin::ElasticsearchError::RetryableOperationExhaustedFailure,
48
- message if fail_on_retry_exceed
49
- end
50
- end
51
-
52
- def template_put(name, template)
53
- client.indices.put_template(:name => name, :body => template)
54
- end
55
-
56
- def indexcreation(index_name)
57
- client.indices.create(:index => index_name)
58
- rescue Elasticsearch::Transport::Transport::Error => e
59
- log.error("Error while index creation - #{index_name}: #{e.inspect}")
60
- end
61
-
62
- def template_install(name, template_file, overwrite)
63
- if overwrite
64
- template_put(name, get_template(template_file))
65
- log.info("Template '#{name}' overwritten with #{template_file}.")
66
- return
67
- end
68
- if !template_exists?(name)
69
- template_put(name, get_template(template_file))
70
- log.info("Template configured, but no template installed. Installed '#{name}' from #{template_file}.")
71
- else
72
- log.info("Template configured and already installed.")
73
- end
74
- end
75
-
76
- def template_custom_install(template_name, template_file, overwrite, customize_template, index_prefix, rollover_index, deflector_alias_name, app_name, index_date_pattern)
77
- template_custom_name=template_name.downcase
78
- if overwrite
79
- template_put(template_custom_name, get_custom_template(template_file, customize_template))
80
- log.info("Template '#{template_custom_name}' overwritten with #{template_file}.")
81
- else
82
- if !template_exists?(template_custom_name)
83
- template_put(template_custom_name, get_custom_template(template_file, customize_template))
84
- log.info("Template configured, but no template installed. Installed '#{template_custom_name}' from #{template_file}.")
85
- else
86
- log.info("Template configured and already installed.")
87
- end
88
- end
89
-
90
- if rollover_index
91
- if !client.indices.exists_alias(:name => deflector_alias_name)
92
- index_name_temp='<'+index_prefix.downcase+'-'+app_name.downcase+'-{'+index_date_pattern+'}-000001>'
93
- indexcreation(index_name_temp)
94
- client.indices.put_alias(:index => index_name_temp, :name => deflector_alias_name)
95
- log.info("The alias '#{deflector_alias_name}' is created for the index '#{index_name_temp}'")
96
- else
97
- log.info("The alias '#{deflector_alias_name}' is already present")
98
- end
99
- else
100
- log.info("No index and alias creation action performed because rollover_index is set to '#{rollover_index}'")
101
- end
102
- end
103
-
104
- def templates_hash_install(templates, overwrite)
105
- templates.each do |key, value|
106
- template_install(key, value, overwrite)
107
- end
108
- end
109
-
110
- end
1
+ require 'fluent/error'
2
+ require_relative './elasticsearch_error'
3
+
4
+ module Fluent::ElasticsearchIndexTemplate
5
+ def get_template(template_file)
6
+ if !File.exists?(template_file)
7
+ raise "If you specify a template_name you must specify a valid template file (checked '#{template_file}')!"
8
+ end
9
+ file_contents = IO.read(template_file).gsub(/\n/,'')
10
+ JSON.parse(file_contents)
11
+ end
12
+
13
+ def get_custom_template(template_file, customize_template)
14
+ if !File.exists?(template_file)
15
+ raise "If you specify a template_name you must specify a valid template file (checked '#{template_file}')!"
16
+ end
17
+ file_contents = IO.read(template_file).gsub(/\n/,'')
18
+ customize_template.each do |key, value|
19
+ file_contents = file_contents.gsub(key,value.downcase)
20
+ end
21
+ JSON.parse(file_contents)
22
+ end
23
+
24
+ def template_exists?(name)
25
+ client.indices.get_template(:name => name)
26
+ return true
27
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
28
+ return false
29
+ end
30
+
31
+ def retry_operate(max_retries, fail_on_retry_exceed = true)
32
+ return unless block_given?
33
+ retries = 0
34
+ begin
35
+ yield
36
+ rescue *client.transport.host_unreachable_exceptions, Timeout::Error => e
37
+ @_es = nil
38
+ @_es_info = nil
39
+ if retries < max_retries
40
+ retries += 1
41
+ wait_seconds = 2**retries
42
+ sleep wait_seconds
43
+ log.warn "Could not communicate to Elasticsearch, resetting connection and trying again. #{e.message}"
44
+ log.warn "Remaining retry: #{max_retries - retries}. Retry to communicate after #{wait_seconds} second(s)."
45
+ retry
46
+ end
47
+ message = "Could not communicate to Elasticsearch after #{retries} retries. #{e.message}"
48
+ log.warn message
49
+ raise Fluent::Plugin::ElasticsearchError::RetryableOperationExhaustedFailure,
50
+ message if fail_on_retry_exceed
51
+ end
52
+ end
53
+
54
+ def template_put(name, template)
55
+ client.indices.put_template(:name => name, :body => template)
56
+ end
57
+
58
+ def indexcreation(index_name)
59
+ client.indices.create(:index => index_name)
60
+ rescue Elasticsearch::Transport::Transport::Error => e
61
+ log.error("Error while index creation - #{index_name}: #{e.inspect}")
62
+ end
63
+
64
+ def template_install(name, template_file, overwrite)
65
+ if overwrite
66
+ template_put(name, get_template(template_file))
67
+ log.info("Template '#{name}' overwritten with #{template_file}.")
68
+ return
69
+ end
70
+ if !template_exists?(name)
71
+ template_put(name, get_template(template_file))
72
+ log.info("Template configured, but no template installed. Installed '#{name}' from #{template_file}.")
73
+ else
74
+ log.info("Template configured and already installed.")
75
+ end
76
+ end
77
+
78
+ def template_custom_install(template_name, template_file, overwrite, customize_template, index_prefix, rollover_index, deflector_alias_name, app_name, index_date_pattern)
79
+ template_custom_name=template_name.downcase
80
+ if overwrite
81
+ template_put(template_custom_name, get_custom_template(template_file, customize_template))
82
+ log.info("Template '#{template_custom_name}' overwritten with #{template_file}.")
83
+ else
84
+ if !template_exists?(template_custom_name)
85
+ template_put(template_custom_name, get_custom_template(template_file, customize_template))
86
+ log.info("Template configured, but no template installed. Installed '#{template_custom_name}' from #{template_file}.")
87
+ else
88
+ log.info("Template configured and already installed.")
89
+ end
90
+ end
91
+
92
+ if rollover_index
93
+ if !client.indices.exists_alias(:name => deflector_alias_name)
94
+ index_name_temp='<'+index_prefix.downcase+'-'+app_name.downcase+'-{'+index_date_pattern+'}-000001>'
95
+ indexcreation(index_name_temp)
96
+ client.indices.put_alias(:index => index_name_temp, :name => deflector_alias_name)
97
+ log.info("The alias '#{deflector_alias_name}' is created for the index '#{index_name_temp}'")
98
+ else
99
+ log.info("The alias '#{deflector_alias_name}' is already present")
100
+ end
101
+ else
102
+ log.info("No index and alias creation action performed because rollover_index is set to '#{rollover_index}'")
103
+ end
104
+ end
105
+
106
+ def templates_hash_install(templates, overwrite)
107
+ templates.each do |key, value|
108
+ template_install(key, value, overwrite)
109
+ end
110
+ end
111
+
112
+ end
@@ -1,10 +1,10 @@
1
- require 'elasticsearch'
2
-
3
- class Fluent::Plugin::ElasticsearchSimpleSniffer < Elasticsearch::Transport::Transport::Sniffer
4
-
5
- def hosts
6
- @transport.logger.debug "In Fluent::Plugin::ElasticsearchSimpleSniffer hosts #{@transport.hosts}" if @transport.logger
7
- @transport.hosts
8
- end
9
-
10
- end
1
+ require 'elasticsearch'
2
+
3
+ class Fluent::Plugin::ElasticsearchSimpleSniffer < Elasticsearch::Transport::Transport::Sniffer
4
+
5
+ def hosts
6
+ @transport.logger.debug "In Fluent::Plugin::ElasticsearchSimpleSniffer hosts #{@transport.hosts}" if @transport.logger
7
+ @transport.hosts
8
+ end
9
+
10
+ end
@@ -1,25 +1,25 @@
1
- require 'securerandom'
2
- require 'base64'
3
- require 'fluent/plugin/filter'
4
-
5
- module Fluent::Plugin
6
- class ElasticsearchGenidFilter < Filter
7
- Fluent::Plugin.register_filter('elasticsearch_genid', self)
8
-
9
- config_param :hash_id_key, :string, :default => '_hash'
10
-
11
- def initialize
12
- super
13
- end
14
-
15
- def configure(conf)
16
- super
17
- end
18
-
19
- def filter(tag, time, record)
20
- record[@hash_id_key] = Base64.strict_encode64(SecureRandom.uuid)
21
- record
22
- end
23
-
24
- end
25
- end
1
+ require 'securerandom'
2
+ require 'base64'
3
+ require 'fluent/plugin/filter'
4
+
5
+ module Fluent::Plugin
6
+ class ElasticsearchGenidFilter < Filter
7
+ Fluent::Plugin.register_filter('elasticsearch_genid', self)
8
+
9
+ config_param :hash_id_key, :string, :default => '_hash'
10
+
11
+ def initialize
12
+ super
13
+ end
14
+
15
+ def configure(conf)
16
+ super
17
+ end
18
+
19
+ def filter(tag, time, record)
20
+ record[@hash_id_key] = Base64.strict_encode64(SecureRandom.uuid)
21
+ record
22
+ end
23
+
24
+ end
25
+ end
@@ -1,22 +1,22 @@
1
- require 'oj'
2
-
3
- module Fluent::Plugin
4
- module Serializer
5
-
6
- class Oj
7
- include Elasticsearch::Transport::Transport::Serializer::Base
8
-
9
- # De-serialize a Hash from JSON string
10
- #
11
- def load(string, options={})
12
- ::Oj.load(string, options)
13
- end
14
-
15
- # Serialize a Hash to JSON string
16
- #
17
- def dump(object, options={})
18
- ::Oj.dump(object, options)
19
- end
20
- end
21
- end
22
- end
1
+ require 'oj'
2
+
3
+ module Fluent::Plugin
4
+ module Serializer
5
+
6
+ class Oj
7
+ include Elasticsearch::Transport::Transport::Serializer::Base
8
+
9
+ # De-serialize a Hash from JSON string
10
+ #
11
+ def load(string, options={})
12
+ ::Oj.load(string, options)
13
+ end
14
+
15
+ # Serialize a Hash to JSON string
16
+ #
17
+ def dump(object, options={})
18
+ ::Oj.dump(object, options)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,776 +1,777 @@
1
- # encoding: UTF-8
2
- require 'date'
3
- require 'excon'
4
- require 'elasticsearch'
5
- require 'json'
6
- require 'uri'
7
- begin
8
- require 'strptime'
9
- rescue LoadError
10
- end
11
-
12
- require 'fluent/plugin/output'
13
- require 'fluent/event'
14
- require 'fluent/error'
15
- require 'fluent/time'
16
- require 'fluent/log-ext'
17
- require_relative 'elasticsearch_constants'
18
- require_relative 'elasticsearch_error'
19
- require_relative 'elasticsearch_error_handler'
20
- require_relative 'elasticsearch_index_template'
21
- begin
22
- require_relative 'oj_serializer'
23
- rescue LoadError
24
- end
25
-
26
- module Fluent::Plugin
27
- class ElasticsearchOutput < Output
28
- class RecoverableRequestFailure < StandardError; end
29
- class UnrecoverableRequestFailure < Fluent::UnrecoverableError; end
30
-
31
- # MissingIdFieldError is raised for records that do not
32
- # include the field for the unique record identifier
33
- class MissingIdFieldError < StandardError; end
34
-
35
- # RetryStreamError privides a stream to be
36
- # put back in the pipeline for cases where a bulk request
37
- # failed (e.g some records succeed while others failed)
38
- class RetryStreamError < StandardError
39
- attr_reader :retry_stream
40
- def initialize(retry_stream)
41
- @retry_stream = retry_stream
42
- end
43
- end
44
-
45
- RequestInfo = Struct.new(:host, :index)
46
-
47
- helpers :event_emitter, :compat_parameters, :record_accessor
48
-
49
- Fluent::Plugin.register_output('elasticsearch', self)
50
-
51
- DEFAULT_BUFFER_TYPE = "memory"
52
- DEFAULT_ELASTICSEARCH_VERSION = 5 # For compatibility.
53
- DEFAULT_TYPE_NAME_ES_7x = "_doc".freeze
54
- DEFAULT_TYPE_NAME = "fluentd".freeze
55
- DEFAULT_RELOAD_AFTER = -1
56
- TARGET_BULK_BYTES = 20 * 1024 * 1024
57
-
58
- config_param :host, :string, :default => 'localhost'
59
- config_param :port, :integer, :default => 9200
60
- config_param :user, :string, :default => nil
61
- config_param :password, :string, :default => nil, :secret => true
62
- config_param :path, :string, :default => nil
63
- config_param :scheme, :enum, :list => [:https, :http], :default => :http
64
- config_param :hosts, :string, :default => nil
65
- config_param :target_index_key, :string, :default => nil
66
- config_param :target_type_key, :string, :default => nil,
67
- :deprecated => <<EOC
68
- Elasticsearch 7.x or above will ignore this config. Please use fixed type_name instead.
69
- EOC
70
- config_param :time_key_format, :string, :default => nil
71
- config_param :time_precision, :integer, :default => 9
72
- config_param :include_timestamp, :bool, :default => false
73
- config_param :logstash_format, :bool, :default => false
74
- config_param :logstash_prefix, :string, :default => "logstash"
75
- config_param :logstash_prefix_separator, :string, :default => '-'
76
- config_param :logstash_dateformat, :string, :default => "%Y.%m.%d"
77
- config_param :utc_index, :bool, :default => true
78
- config_param :type_name, :string, :default => DEFAULT_TYPE_NAME
79
- config_param :index_name, :string, :default => "fluentd"
80
- config_param :id_key, :string, :default => nil
81
- config_param :write_operation, :string, :default => "index"
82
- config_param :parent_key, :string, :default => nil
83
- config_param :routing_key, :string, :default => nil
84
- config_param :request_timeout, :time, :default => 5
85
- config_param :reload_connections, :bool, :default => true
86
- config_param :reload_on_failure, :bool, :default => false
87
- config_param :retry_tag, :string, :default=>nil
88
- config_param :resurrect_after, :time, :default => 60
89
- config_param :time_key, :string, :default => nil
90
- config_param :time_key_exclude_timestamp, :bool, :default => false
91
- config_param :ssl_verify , :bool, :default => true
92
- config_param :client_key, :string, :default => nil
93
- config_param :client_cert, :string, :default => nil
94
- config_param :client_key_pass, :string, :default => nil
95
- config_param :ca_file, :string, :default => nil
96
- config_param :ssl_version, :enum, list: [:SSLv23, :TLSv1, :TLSv1_1, :TLSv1_2], :default => :TLSv1
97
- config_param :remove_keys, :string, :default => nil
98
- config_param :remove_keys_on_update, :string, :default => ""
99
- config_param :remove_keys_on_update_key, :string, :default => nil
100
- config_param :flatten_hashes, :bool, :default => false
101
- config_param :flatten_hashes_separator, :string, :default => "_"
102
- config_param :template_name, :string, :default => nil
103
- config_param :template_file, :string, :default => nil
104
- config_param :template_overwrite, :bool, :default => false
105
- config_param :customize_template, :hash, :default => nil
106
- config_param :rollover_index, :string, :default => false
107
- config_param :index_date_pattern, :string, :default => "now/d"
108
- config_param :deflector_alias, :string, :default => nil
109
- config_param :index_prefix, :string, :default => "logstash"
110
- config_param :application_name, :string, :default => "default"
111
- config_param :templates, :hash, :default => nil
112
- config_param :max_retry_putting_template, :integer, :default => 10
113
- config_param :fail_on_putting_template_retry_exceed, :bool, :default => true
114
- config_param :max_retry_get_es_version, :integer, :default => 15
115
- config_param :include_tag_key, :bool, :default => false
116
- config_param :tag_key, :string, :default => 'tag'
117
- config_param :time_parse_error_tag, :string, :default => 'Fluent::ElasticsearchOutput::TimeParser.error'
118
- config_param :reconnect_on_error, :bool, :default => false
119
- config_param :pipeline, :string, :default => nil
120
- config_param :with_transporter_log, :bool, :default => false
121
- config_param :emit_error_for_missing_id, :bool, :default => false
122
- config_param :sniffer_class_name, :string, :default => nil
123
- config_param :reload_after, :integer, :default => DEFAULT_RELOAD_AFTER
124
- config_param :content_type, :enum, list: [:"application/json", :"application/x-ndjson"], :default => :"application/json",
125
- :deprecated => <<EOC
126
- elasticsearch gem v6.0.2 starts to use correct Content-Type. Please upgrade elasticserach gem and stop to use this option.
127
- see: https://github.com/elastic/elasticsearch-ruby/pull/514
128
- EOC
129
- config_param :include_index_in_url, :bool, :default => false
130
- config_param :http_backend, :enum, list: [:excon, :typhoeus], :default => :excon
131
- config_param :validate_client_version, :bool, :default => false
132
- config_param :prefer_oj_serializer, :bool, :default => false
133
- config_param :unrecoverable_error_types, :array, :default => ["out_of_memory_error", "es_rejected_execution_exception"]
134
- config_param :verify_es_version_at_startup, :bool, :default => true
135
- config_param :default_elasticsearch_version, :integer, :default => DEFAULT_ELASTICSEARCH_VERSION
136
- config_param :log_es_400_reason, :bool, :default => false
137
- config_param :custom_headers, :hash, :default => {}
138
- config_param :suppress_doc_wrap, :bool, :default => false
139
- config_param :ignore_exceptions, :array, :default => [], value_type: :string, :desc => "Ignorable exception list"
140
- config_param :exception_backup, :bool, :default => true, :desc => "Chunk backup flag when ignore exception occured"
141
- config_param :bulk_message_request_threshold, :size, :default => TARGET_BULK_BYTES
142
-
143
- config_section :buffer do
144
- config_set_default :@type, DEFAULT_BUFFER_TYPE
145
- config_set_default :chunk_keys, ['tag']
146
- config_set_default :timekey_use_utc, true
147
- end
148
-
149
- include Fluent::ElasticsearchIndexTemplate
150
- include Fluent::Plugin::ElasticsearchConstants
151
-
152
- def initialize
153
- super
154
- end
155
-
156
- def configure(conf)
157
- compat_parameters_convert(conf, :buffer)
158
-
159
- super
160
- raise Fluent::ConfigError, "'tag' in chunk_keys is required." if not @chunk_key_tag
161
-
162
- @time_parser = create_time_parser
163
- @backend_options = backend_options
164
-
165
- if @remove_keys
166
- @remove_keys = @remove_keys.split(/\s*,\s*/)
167
- end
168
-
169
- if @target_index_key && @target_index_key.is_a?(String)
170
- @target_index_key = @target_index_key.split '.'
171
- end
172
-
173
- if @target_type_key && @target_type_key.is_a?(String)
174
- @target_type_key = @target_type_key.split '.'
175
- end
176
-
177
- if @remove_keys_on_update && @remove_keys_on_update.is_a?(String)
178
- @remove_keys_on_update = @remove_keys_on_update.split ','
179
- end
180
-
181
- raise Fluent::ConfigError, "'max_retry_putting_template' must be positive number." if @max_retry_putting_template < 0
182
-
183
- # Raise error when using host placeholders and template features at same time.
184
- valid_host_placeholder = placeholder?(:host_placeholder, @host)
185
- if valid_host_placeholder && (@template_name && @template_file || @templates)
186
- raise Fluent::ConfigError, "host placeholder and template installation are exclusive features."
187
- end
188
-
189
- if !Fluent::Engine.dry_run_mode
190
- if @template_name && @template_file
191
- retry_operate(@max_retry_putting_template, @fail_on_putting_template_retry_exceed) do
192
- if @customize_template
193
- if @rollover_index
194
- raise Fluent::ConfigError, "'deflector_alias' must be provided if 'rollover_index' is set true ." if not @deflector_alias
195
- end
196
- template_custom_install(@template_name, @template_file, @template_overwrite, @customize_template, @index_prefix, @rollover_index, @deflector_alias, @application_name, @index_date_pattern)
197
- else
198
- template_install(@template_name, @template_file, @template_overwrite)
199
- end
200
- end
201
- elsif @templates
202
- retry_operate(@max_retry_putting_template, @fail_on_putting_template_retry_exceed) do
203
- templates_hash_install(@templates, @template_overwrite)
204
- end
205
- end
206
- end
207
-
208
- @serializer_class = nil
209
- begin
210
- require 'oj'
211
- @dump_proc = Oj.method(:dump)
212
- if @prefer_oj_serializer
213
- @serializer_class = Fluent::Plugin::Serializer::Oj
214
- Elasticsearch::API.settings[:serializer] = Fluent::Plugin::Serializer::Oj
215
- end
216
- rescue LoadError
217
- @dump_proc = Yajl.method(:dump)
218
- end
219
-
220
- if @user && m = @user.match(/%{(?<user>.*)}/)
221
- @user = URI.encode_www_form_component(m["user"])
222
- end
223
- if @password && m = @password.match(/%{(?<password>.*)}/)
224
- @password = URI.encode_www_form_component(m["password"])
225
- end
226
-
227
- @transport_logger = nil
228
- if @with_transporter_log
229
- @transport_logger = log
230
- log_level = conf['@log_level'] || conf['log_level']
231
- log.warn "Consider to specify log_level with @log_level." unless log_level
232
- end
233
- # Specify @sniffer_class before calling #client.
234
- # #detect_es_major_version uses #client.
235
- @sniffer_class = nil
236
- begin
237
- @sniffer_class = Object.const_get(@sniffer_class_name) if @sniffer_class_name
238
- rescue Exception => ex
239
- raise Fluent::ConfigError, "Could not load sniffer class #{@sniffer_class_name}: #{ex}"
240
- end
241
-
242
- @last_seen_major_version =
243
- if @verify_es_version_at_startup && !Fluent::Engine.dry_run_mode
244
- retry_operate(@max_retry_get_es_version) do
245
- detect_es_major_version
246
- end
247
- else
248
- @default_elasticsearch_version
249
- end
250
- if @last_seen_major_version == 6 && @type_name != DEFAULT_TYPE_NAME_ES_7x
251
- log.info "Detected ES 6.x: ES 7.x will only accept `_doc` in type_name."
252
- end
253
- if @last_seen_major_version >= 7 && @type_name != DEFAULT_TYPE_NAME_ES_7x
254
- log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
255
- @type_name = '_doc'.freeze
256
- end
257
-
258
- if @validate_client_version && !Fluent::Engine.dry_run_mode
259
- if @last_seen_major_version != client_library_version.to_i
260
- raise Fluent::ConfigError, <<-EOC
261
- Detected ES #{@last_seen_major_version} but you use ES client #{client_library_version}.
262
- Please consider to use #{@last_seen_major_version}.x series ES client.
263
- EOC
264
- end
265
- end
266
-
267
- if @last_seen_major_version >= 6
268
- case @ssl_version
269
- when :SSLv23, :TLSv1, :TLSv1_1
270
- if @scheme == :https
271
- log.warn "Detected ES 6.x or above and enabled insecure security:
272
- You might have to specify `ssl_version TLSv1_2` in configuration."
273
- end
274
- end
275
- end
276
-
277
- if @buffer_config.flush_thread_count < 2
278
- log.warn "To prevent events traffic jam, you should specify 2 or more 'flush_thread_count'."
279
- end
280
-
281
- # Consider missing the prefix of "$." in nested key specifiers.
282
- @id_key = convert_compat_id_key(@id_key) if @id_key
283
- @parent_key = convert_compat_id_key(@parent_key) if @parent_key
284
- @routing_key = convert_compat_id_key(@routing_key) if @routing_key
285
-
286
- @routing_key_name = configure_routing_key_name
287
- @meta_config_map = create_meta_config_map
288
- @current_config = nil
289
-
290
- @ignore_exception_classes = @ignore_exceptions.map do |exception|
291
- unless Object.const_defined?(exception)
292
- log.warn "Cannot find class #{exception}. Will ignore it."
293
-
294
- nil
295
- else
296
- Object.const_get(exception)
297
- end
298
- end.compact
299
-
300
- if @bulk_message_request_threshold < 0
301
- class << self
302
- alias_method :split_request?, :split_request_size_uncheck?
303
- end
304
- else
305
- class << self
306
- alias_method :split_request?, :split_request_size_check?
307
- end
308
- end
309
- end
310
-
311
- def placeholder?(name, param)
312
- begin
313
- placeholder_validate!(name, param)
314
- true
315
- rescue Fluent::ConfigError
316
- false
317
- end
318
- end
319
-
320
- def backend_options
321
- case @http_backend
322
- when :excon
323
- { client_key: @client_key, client_cert: @client_cert, client_key_pass: @client_key_pass }
324
- when :typhoeus
325
- require 'typhoeus'
326
- { sslkey: @client_key, sslcert: @client_cert, keypasswd: @client_key_pass }
327
- end
328
- rescue LoadError => ex
329
- log.error_backtrace(ex.backtrace)
330
- raise Fluent::ConfigError, "You must install #{@http_backend} gem. Exception: #{ex}"
331
- end
332
-
333
- def detect_es_major_version
334
- @_es_info ||= client.info
335
- @_es_info["version"]["number"].to_i
336
- end
337
-
338
- def client_library_version
339
- Elasticsearch::VERSION
340
- end
341
-
342
- def configure_routing_key_name
343
- if @last_seen_major_version >= 7
344
- 'routing'
345
- else
346
- '_routing'
347
- end
348
- end
349
-
350
- def convert_compat_id_key(key)
351
- if key.include?('.') && !key.start_with?('$[')
352
- key = "$.#{key}" unless key.start_with?('$.')
353
- end
354
- key
355
- end
356
-
357
- def create_meta_config_map
358
- result = []
359
- result << [record_accessor_create(@id_key), '_id'] if @id_key
360
- result << [record_accessor_create(@parent_key), '_parent'] if @parent_key
361
- result << [record_accessor_create(@routing_key), @routing_key_name] if @routing_key
362
- result
363
- end
364
-
365
- # once fluent v0.14 is released we might be able to use
366
- # Fluent::Parser::TimeParser, but it doesn't quite do what we want - if gives
367
- # [sec,nsec] where as we want something we can call `strftime` on...
368
- def create_time_parser
369
- if @time_key_format
370
- begin
371
- # Strptime doesn't support all formats, but for those it does it's
372
- # blazingly fast.
373
- strptime = Strptime.new(@time_key_format)
374
- Proc.new { |value|
375
- value = convert_numeric_time_into_string(value, @time_key_format) if value.is_a?(Numeric)
376
- strptime.exec(value).to_datetime
377
- }
378
- rescue
379
- # Can happen if Strptime doesn't recognize the format; or
380
- # if strptime couldn't be required (because it's not installed -- it's
381
- # ruby 2 only)
382
- Proc.new { |value|
383
- value = convert_numeric_time_into_string(value, @time_key_format) if value.is_a?(Numeric)
384
- DateTime.strptime(value, @time_key_format)
385
- }
386
- end
387
- else
388
- Proc.new { |value|
389
- value = convert_numeric_time_into_string(value) if value.is_a?(Numeric)
390
- DateTime.parse(value)
391
- }
392
- end
393
- end
394
-
395
- def convert_numeric_time_into_string(numeric_time, time_key_format = "%Y-%m-%d %H:%M:%S.%N%z")
396
- numeric_time_parser = Fluent::NumericTimeParser.new(:float)
397
- Time.at(numeric_time_parser.parse(numeric_time).to_r).strftime(time_key_format)
398
- end
399
-
400
- def parse_time(value, event_time, tag)
401
- @time_parser.call(value)
402
- rescue => e
403
- router.emit_error_event(@time_parse_error_tag, Fluent::Engine.now, {'tag' => tag, 'time' => event_time, 'format' => @time_key_format, 'value' => value}, e)
404
- return Time.at(event_time).to_datetime
405
- end
406
-
407
- def client(host = nil)
408
- # check here to see if we already have a client connection for the given host
409
- connection_options = get_connection_options(host)
410
-
411
- @_es = nil unless is_existing_connection(connection_options[:hosts])
412
-
413
- @_es ||= begin
414
- @current_config = connection_options[:hosts].clone
415
- adapter_conf = lambda {|f| f.adapter @http_backend, @backend_options }
416
- local_reload_connections = @reload_connections
417
- if local_reload_connections && @reload_after > DEFAULT_RELOAD_AFTER
418
- local_reload_connections = @reload_after
419
- end
420
- headers = { 'Content-Type' => @content_type.to_s }.merge(@custom_headers)
421
- transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(connection_options.merge(
422
- options: {
423
- reload_connections: local_reload_connections,
424
- reload_on_failure: @reload_on_failure,
425
- resurrect_after: @resurrect_after,
426
- logger: @transport_logger,
427
- transport_options: {
428
- headers: headers,
429
- request: { timeout: @request_timeout },
430
- ssl: { verify: @ssl_verify, ca_file: @ca_file, version: @ssl_version }
431
- },
432
- http: {
433
- user: @user,
434
- password: @password
435
- },
436
- sniffer_class: @sniffer_class,
437
- serializer_class: @serializer_class,
438
- }), &adapter_conf)
439
- Elasticsearch::Client.new transport: transport
440
- end
441
- end
442
-
443
- def get_escaped_userinfo(host_str)
444
- if m = host_str.match(/(?<scheme>.*)%{(?<user>.*)}:%{(?<password>.*)}(?<path>@.*)/)
445
- m["scheme"] +
446
- URI.encode_www_form_component(m["user"]) +
447
- ':' +
448
- URI.encode_www_form_component(m["password"]) +
449
- m["path"]
450
- else
451
- host_str
452
- end
453
- end
454
-
455
- def get_connection_options(con_host=nil)
456
- raise "`password` must be present if `user` is present" if @user && !@password
457
-
458
- hosts = if con_host || @hosts
459
- (con_host || @hosts).split(',').map do |host_str|
460
- # Support legacy hosts format host:port,host:port,host:port...
461
- if host_str.match(%r{^[^:]+(\:\d+)?$})
462
- {
463
- host: host_str.split(':')[0],
464
- port: (host_str.split(':')[1] || @port).to_i,
465
- scheme: @scheme.to_s
466
- }
467
- else
468
- # New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
469
- uri = URI(get_escaped_userinfo(host_str))
470
- %w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
471
- hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
472
- hash
473
- end
474
- end
475
- end.compact
476
- else
477
- [{host: @host, port: @port, scheme: @scheme.to_s}]
478
- end.each do |host|
479
- host.merge!(user: @user, password: @password) if !host[:user] && @user
480
- host.merge!(path: @path) if !host[:path] && @path
481
- end
482
-
483
- {
484
- hosts: hosts
485
- }
486
- end
487
-
488
- def connection_options_description(con_host=nil)
489
- get_connection_options(con_host)[:hosts].map do |host_info|
490
- attributes = host_info.dup
491
- attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
492
- attributes.inspect
493
- end.join(', ')
494
- end
495
-
496
- # append_record_to_messages adds a record to the bulk message
497
- # payload to be submitted to Elasticsearch. Records that do
498
- # not include '_id' field are skipped when 'write_operation'
499
- # is configured for 'create' or 'update'
500
- #
501
- # returns 'true' if record was appended to the bulk message
502
- # and 'false' otherwise
503
- def append_record_to_messages(op, meta, header, record, msgs)
504
- case op
505
- when UPDATE_OP, UPSERT_OP
506
- if meta.has_key?(ID_FIELD)
507
- header[UPDATE_OP] = meta
508
- msgs << @dump_proc.call(header) << BODY_DELIMITER
509
- msgs << @dump_proc.call(update_body(record, op)) << BODY_DELIMITER
510
- return true
511
- end
512
- when CREATE_OP
513
- if meta.has_key?(ID_FIELD)
514
- header[CREATE_OP] = meta
515
- msgs << @dump_proc.call(header) << BODY_DELIMITER
516
- msgs << @dump_proc.call(record) << BODY_DELIMITER
517
- return true
518
- end
519
- when INDEX_OP
520
- header[INDEX_OP] = meta
521
- msgs << @dump_proc.call(header) << BODY_DELIMITER
522
- msgs << @dump_proc.call(record) << BODY_DELIMITER
523
- return true
524
- end
525
- return false
526
- end
527
-
528
- def update_body(record, op)
529
- update = remove_keys(record)
530
- if @suppress_doc_wrap
531
- return update
532
- end
533
- body = {"doc".freeze => update}
534
- if op == UPSERT_OP
535
- if update == record
536
- body["doc_as_upsert".freeze] = true
537
- else
538
- body[UPSERT_OP] = record
539
- end
540
- end
541
- body
542
- end
543
-
544
- def remove_keys(record)
545
- keys = record[@remove_keys_on_update_key] || @remove_keys_on_update || []
546
- record.delete(@remove_keys_on_update_key)
547
- return record unless keys.any?
548
- record = record.dup
549
- keys.each { |key| record.delete(key) }
550
- record
551
- end
552
-
553
- def flatten_record(record, prefix=[])
554
- ret = {}
555
- if record.is_a? Hash
556
- record.each { |key, value|
557
- ret.merge! flatten_record(value, prefix + [key.to_s])
558
- }
559
- elsif record.is_a? Array
560
- # Don't mess with arrays, leave them unprocessed
561
- ret.merge!({prefix.join(@flatten_hashes_separator) => record})
562
- else
563
- return {prefix.join(@flatten_hashes_separator) => record}
564
- end
565
- ret
566
- end
567
-
568
- def expand_placeholders(chunk)
569
- logstash_prefix = extract_placeholders(@logstash_prefix, chunk)
570
- index_name = extract_placeholders(@index_name, chunk)
571
- type_name = extract_placeholders(@type_name, chunk)
572
- return logstash_prefix, index_name, type_name
573
- end
574
-
575
- def multi_workers_ready?
576
- true
577
- end
578
-
579
- def write(chunk)
580
- bulk_message_count = Hash.new { |h,k| h[k] = 0 }
581
- bulk_message = Hash.new { |h,k| h[k] = '' }
582
- header = {}
583
- meta = {}
584
-
585
- tag = chunk.metadata.tag
586
- extracted_values = expand_placeholders(chunk)
587
- host = if @hosts
588
- extract_placeholders(@hosts, chunk)
589
- else
590
- extract_placeholders(@host, chunk)
591
- end
592
-
593
- chunk.msgpack_each do |time, record|
594
- next unless record.is_a? Hash
595
- begin
596
- meta, header, record = process_message(tag, meta, header, time, record, extracted_values)
597
- info = if @include_index_in_url
598
- RequestInfo.new(host, meta.delete("_index".freeze))
599
- else
600
- RequestInfo.new(host, nil)
601
- end
602
-
603
- if append_record_to_messages(@write_operation, meta, header, record, bulk_message[info])
604
- bulk_message_count[info] += 1;
605
- if split_request?(bulk_message, info)
606
- bulk_message.each do |info, msgs|
607
- send_bulk(msgs, tag, chunk, bulk_message_count[info], extracted_values, info) unless msgs.empty?
608
- msgs.clear
609
- # Clear bulk_message_count for this info.
610
- bulk_message_count[info] = 0;
611
- next
612
- end
613
- end
614
- else
615
- if @emit_error_for_missing_id
616
- raise MissingIdFieldError, "Missing '_id' field. Write operation is #{@write_operation}"
617
- else
618
- log.on_debug { log.debug("Dropping record because its missing an '_id' field and write_operation is #{@write_operation}: #{record}") }
619
- end
620
- end
621
- rescue => e
622
- router.emit_error_event(tag, time, record, e)
623
- end
624
- end
625
-
626
- bulk_message.each do |info, msgs|
627
- send_bulk(msgs, tag, chunk, bulk_message_count[info], extracted_values, info) unless msgs.empty?
628
- msgs.clear
629
- end
630
- end
631
-
632
- def split_request?(bulk_message, info)
633
- # For safety.
634
- end
635
-
636
- def split_request_size_check?(bulk_message, info)
637
- bulk_message[info].size > @bulk_message_request_threshold
638
- end
639
-
640
- def split_request_size_uncheck?(bulk_message, info)
641
- false
642
- end
643
-
644
- def process_message(tag, meta, header, time, record, extracted_values)
645
- logstash_prefix, index_name, type_name = extracted_values
646
-
647
- if @flatten_hashes
648
- record = flatten_record(record)
649
- end
650
-
651
- dt = nil
652
- if @logstash_format || @include_timestamp
653
- if record.has_key?(TIMESTAMP_FIELD)
654
- rts = record[TIMESTAMP_FIELD]
655
- dt = parse_time(rts, time, tag)
656
- elsif record.has_key?(@time_key)
657
- rts = record[@time_key]
658
- dt = parse_time(rts, time, tag)
659
- record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision) unless @time_key_exclude_timestamp
660
- else
661
- dt = Time.at(time).to_datetime
662
- record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision)
663
- end
664
- end
665
-
666
- target_index_parent, target_index_child_key = @target_index_key ? get_parent_of(record, @target_index_key) : nil
667
- if target_index_parent && target_index_parent[target_index_child_key]
668
- target_index = target_index_parent.delete(target_index_child_key)
669
- elsif @logstash_format
670
- dt = dt.new_offset(0) if @utc_index
671
- target_index = "#{logstash_prefix}#{@logstash_prefix_separator}#{dt.strftime(@logstash_dateformat)}"
672
- else
673
- target_index = index_name
674
- end
675
-
676
- # Change target_index to lower-case since Elasticsearch doesn't
677
- # allow upper-case characters in index names.
678
- target_index = target_index.downcase
679
- if @include_tag_key
680
- record[@tag_key] = tag
681
- end
682
-
683
- target_type_parent, target_type_child_key = @target_type_key ? get_parent_of(record, @target_type_key) : nil
684
- if target_type_parent && target_type_parent[target_type_child_key]
685
- target_type = target_type_parent.delete(target_type_child_key)
686
- if @last_seen_major_version == 6
687
- log.warn "Detected ES 6.x: `@type_name` will be used as the document `_type`."
688
- target_type = type_name
689
- elsif @last_seen_major_version >= 7
690
- log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
691
- target_type = '_doc'.freeze
692
- end
693
- else
694
- if @last_seen_major_version >= 7 && @type_name != DEFAULT_TYPE_NAME_ES_7x
695
- log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
696
- target_type = '_doc'.freeze
697
- else
698
- target_type = type_name
699
- end
700
- end
701
-
702
- meta.clear
703
- meta["_index".freeze] = target_index
704
- meta["_type".freeze] = target_type
705
-
706
- if @pipeline
707
- meta["pipeline".freeze] = @pipeline
708
- end
709
-
710
- @meta_config_map.each do |record_accessor, meta_key|
711
- if raw_value = record_accessor.call(record)
712
- meta[meta_key] = raw_value
713
- end
714
- end
715
-
716
- if @remove_keys
717
- @remove_keys.each { |key| record.delete(key) }
718
- end
719
-
720
- return [meta, header, record]
721
- end
722
-
723
- # returns [parent, child_key] of child described by path array in record's tree
724
- # returns [nil, child_key] if path doesnt exist in record
725
- def get_parent_of(record, path)
726
- parent_object = path[0..-2].reduce(record) { |a, e| a.is_a?(Hash) ? a[e] : nil }
727
- [parent_object, path[-1]]
728
- end
729
-
730
- # send_bulk given a specific bulk request, the original tag,
731
- # chunk, and bulk_message_count
732
- def send_bulk(data, tag, chunk, bulk_message_count, extracted_values, info)
733
- begin
734
-
735
- log.on_trace { log.trace "bulk request: #{data}" }
736
- response = client(info.host).bulk body: data, index: info.index
737
- log.on_trace { log.trace "bulk response: #{response}" }
738
-
739
- if response['errors']
740
- error = Fluent::Plugin::ElasticsearchErrorHandler.new(self)
741
- error.handle_error(response, tag, chunk, bulk_message_count, extracted_values)
742
- end
743
- rescue RetryStreamError => e
744
- emit_tag = @retry_tag ? @retry_tag : tag
745
- router.emit_stream(emit_tag, e.retry_stream)
746
- rescue => e
747
- ignore = @ignore_exception_classes.any? { |clazz| e.class <= clazz }
748
-
749
- log.warn "Exception ignored in tag #{tag}: #{e.class.name} #{e.message}" if ignore
750
-
751
- @_es = nil if @reconnect_on_error
752
- @_es_info = nil if @reconnect_on_error
753
-
754
- raise UnrecoverableRequestFailure if ignore && @exception_backup
755
-
756
- # FIXME: identify unrecoverable errors and raise UnrecoverableRequestFailure instead
757
- raise RecoverableRequestFailure, "could not push logs to Elasticsearch cluster (#{connection_options_description(info.host)}): #{e.message}" unless ignore
758
- end
759
- end
760
-
761
- def is_existing_connection(host)
762
- # check if the host provided match the current connection
763
- return false if @_es.nil?
764
- return false if @current_config.nil?
765
- return false if host.length != @current_config.length
766
-
767
- for i in 0...host.length
768
- if !host[i][:host].eql? @current_config[i][:host] || host[i][:port] != @current_config[i][:port]
769
- return false
770
- end
771
- end
772
-
773
- return true
774
- end
775
- end
776
- end
1
+ # encoding: UTF-8
2
+ require 'date'
3
+ require 'excon'
4
+ require 'elasticsearch'
5
+ require 'json'
6
+ require 'uri'
7
+ begin
8
+ require 'strptime'
9
+ rescue LoadError
10
+ end
11
+
12
+ require 'fluent/plugin/output'
13
+ require 'fluent/event'
14
+ require 'fluent/error'
15
+ require 'fluent/time'
16
+ require 'fluent/log-ext'
17
+ require_relative 'elasticsearch_constants'
18
+ require_relative 'elasticsearch_error'
19
+ require_relative 'elasticsearch_error_handler'
20
+ require_relative 'elasticsearch_index_template'
21
+ begin
22
+ require_relative 'oj_serializer'
23
+ rescue LoadError
24
+ end
25
+
26
+ module Fluent::Plugin
27
+ class ElasticsearchOutput < Output
28
+ class RecoverableRequestFailure < StandardError; end
29
+ class UnrecoverableRequestFailure < Fluent::UnrecoverableError; end
30
+
31
+ # MissingIdFieldError is raised for records that do not
32
+ # include the field for the unique record identifier
33
+ class MissingIdFieldError < StandardError; end
34
+
35
+ # RetryStreamError privides a stream to be
36
+ # put back in the pipeline for cases where a bulk request
37
+ # failed (e.g some records succeed while others failed)
38
+ class RetryStreamError < StandardError
39
+ attr_reader :retry_stream
40
+ def initialize(retry_stream)
41
+ @retry_stream = retry_stream
42
+ end
43
+ end
44
+
45
+ RequestInfo = Struct.new(:host, :index)
46
+
47
+ helpers :event_emitter, :compat_parameters, :record_accessor
48
+
49
+ Fluent::Plugin.register_output('elasticsearch', self)
50
+
51
+ DEFAULT_BUFFER_TYPE = "memory"
52
+ DEFAULT_ELASTICSEARCH_VERSION = 5 # For compatibility.
53
+ DEFAULT_TYPE_NAME_ES_7x = "_doc".freeze
54
+ DEFAULT_TYPE_NAME = "fluentd".freeze
55
+ DEFAULT_RELOAD_AFTER = -1
56
+ TARGET_BULK_BYTES = 20 * 1024 * 1024
57
+
58
+ config_param :host, :string, :default => 'localhost'
59
+ config_param :port, :integer, :default => 9200
60
+ config_param :user, :string, :default => nil
61
+ config_param :password, :string, :default => nil, :secret => true
62
+ config_param :path, :string, :default => nil
63
+ config_param :scheme, :enum, :list => [:https, :http], :default => :http
64
+ config_param :hosts, :string, :default => nil
65
+ config_param :target_index_key, :string, :default => nil
66
+ config_param :target_type_key, :string, :default => nil,
67
+ :deprecated => <<EOC
68
+ Elasticsearch 7.x or above will ignore this config. Please use fixed type_name instead.
69
+ EOC
70
+ config_param :time_key_format, :string, :default => nil
71
+ config_param :time_precision, :integer, :default => 9
72
+ config_param :include_timestamp, :bool, :default => false
73
+ config_param :logstash_format, :bool, :default => false
74
+ config_param :logstash_prefix, :string, :default => "logstash"
75
+ config_param :logstash_prefix_separator, :string, :default => '-'
76
+ config_param :logstash_dateformat, :string, :default => "%Y.%m.%d"
77
+ config_param :utc_index, :bool, :default => true
78
+ config_param :type_name, :string, :default => DEFAULT_TYPE_NAME
79
+ config_param :index_name, :string, :default => "fluentd"
80
+ config_param :id_key, :string, :default => nil
81
+ config_param :write_operation, :string, :default => "index"
82
+ config_param :parent_key, :string, :default => nil
83
+ config_param :routing_key, :string, :default => nil
84
+ config_param :request_timeout, :time, :default => 5
85
+ config_param :reload_connections, :bool, :default => true
86
+ config_param :reload_on_failure, :bool, :default => false
87
+ config_param :retry_tag, :string, :default=>nil
88
+ config_param :resurrect_after, :time, :default => 60
89
+ config_param :time_key, :string, :default => nil
90
+ config_param :time_key_exclude_timestamp, :bool, :default => false
91
+ config_param :ssl_verify , :bool, :default => true
92
+ config_param :client_key, :string, :default => nil
93
+ config_param :client_cert, :string, :default => nil
94
+ config_param :client_key_pass, :string, :default => nil
95
+ config_param :ca_file, :string, :default => nil
96
+ config_param :ssl_version, :enum, list: [:SSLv23, :TLSv1, :TLSv1_1, :TLSv1_2], :default => :TLSv1
97
+ config_param :remove_keys, :string, :default => nil
98
+ config_param :remove_keys_on_update, :string, :default => ""
99
+ config_param :remove_keys_on_update_key, :string, :default => nil
100
+ config_param :flatten_hashes, :bool, :default => false
101
+ config_param :flatten_hashes_separator, :string, :default => "_"
102
+ config_param :template_name, :string, :default => nil
103
+ config_param :template_file, :string, :default => nil
104
+ config_param :template_overwrite, :bool, :default => false
105
+ config_param :customize_template, :hash, :default => nil
106
+ config_param :rollover_index, :string, :default => false
107
+ config_param :index_date_pattern, :string, :default => "now/d"
108
+ config_param :deflector_alias, :string, :default => nil
109
+ config_param :index_prefix, :string, :default => "logstash"
110
+ config_param :application_name, :string, :default => "default"
111
+ config_param :templates, :hash, :default => nil
112
+ config_param :max_retry_putting_template, :integer, :default => 10
113
+ config_param :fail_on_putting_template_retry_exceed, :bool, :default => true
114
+ config_param :max_retry_get_es_version, :integer, :default => 15
115
+ config_param :include_tag_key, :bool, :default => false
116
+ config_param :tag_key, :string, :default => 'tag'
117
+ config_param :time_parse_error_tag, :string, :default => 'Fluent::ElasticsearchOutput::TimeParser.error'
118
+ config_param :reconnect_on_error, :bool, :default => false
119
+ config_param :pipeline, :string, :default => nil
120
+ config_param :with_transporter_log, :bool, :default => false
121
+ config_param :emit_error_for_missing_id, :bool, :default => false
122
+ config_param :sniffer_class_name, :string, :default => nil
123
+ config_param :reload_after, :integer, :default => DEFAULT_RELOAD_AFTER
124
+ config_param :content_type, :enum, list: [:"application/json", :"application/x-ndjson"], :default => :"application/json",
125
+ :deprecated => <<EOC
126
+ elasticsearch gem v6.0.2 starts to use correct Content-Type. Please upgrade elasticserach gem and stop to use this option.
127
+ see: https://github.com/elastic/elasticsearch-ruby/pull/514
128
+ EOC
129
+ config_param :include_index_in_url, :bool, :default => false
130
+ config_param :http_backend, :enum, list: [:excon, :typhoeus], :default => :excon
131
+ config_param :validate_client_version, :bool, :default => false
132
+ config_param :prefer_oj_serializer, :bool, :default => false
133
+ config_param :unrecoverable_error_types, :array, :default => ["out_of_memory_error", "es_rejected_execution_exception"]
134
+ config_param :verify_es_version_at_startup, :bool, :default => true
135
+ config_param :default_elasticsearch_version, :integer, :default => DEFAULT_ELASTICSEARCH_VERSION
136
+ config_param :log_es_400_reason, :bool, :default => false
137
+ config_param :custom_headers, :hash, :default => {}
138
+ config_param :suppress_doc_wrap, :bool, :default => false
139
+ config_param :ignore_exceptions, :array, :default => [], value_type: :string, :desc => "Ignorable exception list"
140
+ config_param :exception_backup, :bool, :default => true, :desc => "Chunk backup flag when ignore exception occured"
141
+ config_param :bulk_message_request_threshold, :size, :default => TARGET_BULK_BYTES
142
+
143
+ config_section :buffer do
144
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
145
+ config_set_default :chunk_keys, ['tag']
146
+ config_set_default :timekey_use_utc, true
147
+ end
148
+
149
+ include Fluent::ElasticsearchIndexTemplate
150
+ include Fluent::Plugin::ElasticsearchConstants
151
+
152
+ def initialize
153
+ super
154
+ end
155
+
156
+ def configure(conf)
157
+ compat_parameters_convert(conf, :buffer)
158
+
159
+ super
160
+ raise Fluent::ConfigError, "'tag' in chunk_keys is required." if not @chunk_key_tag
161
+
162
+ @time_parser = create_time_parser
163
+ @backend_options = backend_options
164
+
165
+ if @remove_keys
166
+ @remove_keys = @remove_keys.split(/\s*,\s*/)
167
+ end
168
+
169
+ if @target_index_key && @target_index_key.is_a?(String)
170
+ @target_index_key = @target_index_key.split '.'
171
+ end
172
+
173
+ if @target_type_key && @target_type_key.is_a?(String)
174
+ @target_type_key = @target_type_key.split '.'
175
+ end
176
+
177
+ if @remove_keys_on_update && @remove_keys_on_update.is_a?(String)
178
+ @remove_keys_on_update = @remove_keys_on_update.split ','
179
+ end
180
+
181
+ raise Fluent::ConfigError, "'max_retry_putting_template' must be positive number." if @max_retry_putting_template < 0
182
+
183
+ # Raise error when using host placeholders and template features at same time.
184
+ valid_host_placeholder = placeholder?(:host_placeholder, @host)
185
+ if valid_host_placeholder && (@template_name && @template_file || @templates)
186
+ raise Fluent::ConfigError, "host placeholder and template installation are exclusive features."
187
+ end
188
+
189
+ if !Fluent::Engine.dry_run_mode
190
+ if @template_name && @template_file
191
+ retry_operate(@max_retry_putting_template, @fail_on_putting_template_retry_exceed) do
192
+ if @customize_template
193
+ if @rollover_index
194
+ raise Fluent::ConfigError, "'deflector_alias' must be provided if 'rollover_index' is set true ." if not @deflector_alias
195
+ end
196
+ template_custom_install(@template_name, @template_file, @template_overwrite, @customize_template, @index_prefix, @rollover_index, @deflector_alias, @application_name, @index_date_pattern)
197
+ else
198
+ template_install(@template_name, @template_file, @template_overwrite)
199
+ end
200
+ end
201
+ elsif @templates
202
+ retry_operate(@max_retry_putting_template, @fail_on_putting_template_retry_exceed) do
203
+ templates_hash_install(@templates, @template_overwrite)
204
+ end
205
+ end
206
+ end
207
+
208
+ @serializer_class = nil
209
+ begin
210
+ require 'oj'
211
+ @dump_proc = Oj.method(:dump)
212
+ if @prefer_oj_serializer
213
+ @serializer_class = Fluent::Plugin::Serializer::Oj
214
+ Elasticsearch::API.settings[:serializer] = Fluent::Plugin::Serializer::Oj
215
+ end
216
+ rescue LoadError
217
+ @dump_proc = Yajl.method(:dump)
218
+ end
219
+
220
+ if @user && m = @user.match(/%{(?<user>.*)}/)
221
+ @user = URI.encode_www_form_component(m["user"])
222
+ end
223
+ if @password && m = @password.match(/%{(?<password>.*)}/)
224
+ @password = URI.encode_www_form_component(m["password"])
225
+ end
226
+
227
+ @transport_logger = nil
228
+ if @with_transporter_log
229
+ @transport_logger = log
230
+ log_level = conf['@log_level'] || conf['log_level']
231
+ log.warn "Consider to specify log_level with @log_level." unless log_level
232
+ end
233
+ # Specify @sniffer_class before calling #client.
234
+ # #detect_es_major_version uses #client.
235
+ @sniffer_class = nil
236
+ begin
237
+ @sniffer_class = Object.const_get(@sniffer_class_name) if @sniffer_class_name
238
+ rescue Exception => ex
239
+ raise Fluent::ConfigError, "Could not load sniffer class #{@sniffer_class_name}: #{ex}"
240
+ end
241
+
242
+ @last_seen_major_version =
243
+ if @verify_es_version_at_startup && !Fluent::Engine.dry_run_mode
244
+ retry_operate(@max_retry_get_es_version) do
245
+ detect_es_major_version
246
+ end
247
+ else
248
+ @default_elasticsearch_version
249
+ end
250
+ if @last_seen_major_version == 6 && @type_name != DEFAULT_TYPE_NAME_ES_7x
251
+ log.info "Detected ES 6.x: ES 7.x will only accept `_doc` in type_name."
252
+ end
253
+ if @last_seen_major_version >= 7 && @type_name != DEFAULT_TYPE_NAME_ES_7x
254
+ log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
255
+ @type_name = '_doc'.freeze
256
+ end
257
+
258
+ if @validate_client_version && !Fluent::Engine.dry_run_mode
259
+ if @last_seen_major_version != client_library_version.to_i
260
+ raise Fluent::ConfigError, <<-EOC
261
+ Detected ES #{@last_seen_major_version} but you use ES client #{client_library_version}.
262
+ Please consider to use #{@last_seen_major_version}.x series ES client.
263
+ EOC
264
+ end
265
+ end
266
+
267
+ if @last_seen_major_version >= 6
268
+ case @ssl_version
269
+ when :SSLv23, :TLSv1, :TLSv1_1
270
+ if @scheme == :https
271
+ log.warn "Detected ES 6.x or above and enabled insecure security:
272
+ You might have to specify `ssl_version TLSv1_2` in configuration."
273
+ end
274
+ end
275
+ end
276
+
277
+ if @buffer_config.flush_thread_count < 2
278
+ log.warn "To prevent events traffic jam, you should specify 2 or more 'flush_thread_count'."
279
+ end
280
+
281
+ # Consider missing the prefix of "$." in nested key specifiers.
282
+ @id_key = convert_compat_id_key(@id_key) if @id_key
283
+ @parent_key = convert_compat_id_key(@parent_key) if @parent_key
284
+ @routing_key = convert_compat_id_key(@routing_key) if @routing_key
285
+
286
+ @routing_key_name = configure_routing_key_name
287
+ @meta_config_map = create_meta_config_map
288
+ @current_config = nil
289
+
290
+ @ignore_exception_classes = @ignore_exceptions.map do |exception|
291
+ unless Object.const_defined?(exception)
292
+ log.warn "Cannot find class #{exception}. Will ignore it."
293
+
294
+ nil
295
+ else
296
+ Object.const_get(exception)
297
+ end
298
+ end.compact
299
+
300
+ if @bulk_message_request_threshold < 0
301
+ class << self
302
+ alias_method :split_request?, :split_request_size_uncheck?
303
+ end
304
+ else
305
+ class << self
306
+ alias_method :split_request?, :split_request_size_check?
307
+ end
308
+ end
309
+ end
310
+
311
+ def placeholder?(name, param)
312
+ begin
313
+ placeholder_validate!(name, param)
314
+ true
315
+ rescue Fluent::ConfigError
316
+ false
317
+ end
318
+ end
319
+
320
+ def backend_options
321
+ case @http_backend
322
+ when :excon
323
+ { client_key: @client_key, client_cert: @client_cert, client_key_pass: @client_key_pass }
324
+ when :typhoeus
325
+ require 'typhoeus'
326
+ { sslkey: @client_key, sslcert: @client_cert, keypasswd: @client_key_pass }
327
+ end
328
+ rescue LoadError => ex
329
+ log.error_backtrace(ex.backtrace)
330
+ raise Fluent::ConfigError, "You must install #{@http_backend} gem. Exception: #{ex}"
331
+ end
332
+
333
+ def detect_es_major_version
334
+ @_es_info ||= client.info
335
+ @_es_info["version"]["number"].to_i
336
+ end
337
+
338
+ def client_library_version
339
+ Elasticsearch::VERSION
340
+ end
341
+
342
+ def configure_routing_key_name
343
+ if @last_seen_major_version >= 7
344
+ 'routing'
345
+ else
346
+ '_routing'
347
+ end
348
+ end
349
+
350
+ def convert_compat_id_key(key)
351
+ if key.include?('.') && !key.start_with?('$[')
352
+ key = "$.#{key}" unless key.start_with?('$.')
353
+ end
354
+ key
355
+ end
356
+
357
+ def create_meta_config_map
358
+ result = []
359
+ result << [record_accessor_create(@id_key), '_id'] if @id_key
360
+ result << [record_accessor_create(@parent_key), '_parent'] if @parent_key
361
+ result << [record_accessor_create(@routing_key), @routing_key_name] if @routing_key
362
+ result
363
+ end
364
+
365
+ # once fluent v0.14 is released we might be able to use
366
+ # Fluent::Parser::TimeParser, but it doesn't quite do what we want - if gives
367
+ # [sec,nsec] where as we want something we can call `strftime` on...
368
+ def create_time_parser
369
+ if @time_key_format
370
+ begin
371
+ # Strptime doesn't support all formats, but for those it does it's
372
+ # blazingly fast.
373
+ strptime = Strptime.new(@time_key_format)
374
+ Proc.new { |value|
375
+ value = convert_numeric_time_into_string(value, @time_key_format) if value.is_a?(Numeric)
376
+ strptime.exec(value).to_datetime
377
+ }
378
+ rescue
379
+ # Can happen if Strptime doesn't recognize the format; or
380
+ # if strptime couldn't be required (because it's not installed -- it's
381
+ # ruby 2 only)
382
+ Proc.new { |value|
383
+ value = convert_numeric_time_into_string(value, @time_key_format) if value.is_a?(Numeric)
384
+ DateTime.strptime(value, @time_key_format)
385
+ }
386
+ end
387
+ else
388
+ Proc.new { |value|
389
+ value = convert_numeric_time_into_string(value) if value.is_a?(Numeric)
390
+ DateTime.parse(value)
391
+ }
392
+ end
393
+ end
394
+
395
+ def convert_numeric_time_into_string(numeric_time, time_key_format = "%Y-%m-%d %H:%M:%S.%N%z")
396
+ numeric_time_parser = Fluent::NumericTimeParser.new(:float)
397
+ Time.at(numeric_time_parser.parse(numeric_time).to_r).strftime(time_key_format)
398
+ end
399
+
400
+ def parse_time(value, event_time, tag)
401
+ @time_parser.call(value)
402
+ rescue => e
403
+ router.emit_error_event(@time_parse_error_tag, Fluent::Engine.now, {'tag' => tag, 'time' => event_time, 'format' => @time_key_format, 'value' => value}, e)
404
+ return Time.at(event_time).to_datetime
405
+ end
406
+
407
+ def client(host = nil)
408
+ # check here to see if we already have a client connection for the given host
409
+ connection_options = get_connection_options(host)
410
+
411
+ @_es = nil unless is_existing_connection(connection_options[:hosts])
412
+
413
+ @_es ||= begin
414
+ @current_config = connection_options[:hosts].clone
415
+ adapter_conf = lambda {|f| f.adapter @http_backend, @backend_options }
416
+ local_reload_connections = @reload_connections
417
+ if local_reload_connections && @reload_after > DEFAULT_RELOAD_AFTER
418
+ local_reload_connections = @reload_after
419
+ end
420
+ headers = { 'Content-Type' => @content_type.to_s }.merge(@custom_headers)
421
+ transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(connection_options.merge(
422
+ options: {
423
+ reload_connections: local_reload_connections,
424
+ reload_on_failure: @reload_on_failure,
425
+ resurrect_after: @resurrect_after,
426
+ logger: @transport_logger,
427
+ transport_options: {
428
+ headers: headers,
429
+ request: { timeout: @request_timeout },
430
+ ssl: { verify: @ssl_verify, ca_file: @ca_file, version: @ssl_version }
431
+ },
432
+ http: {
433
+ user: @user,
434
+ password: @password
435
+ },
436
+ sniffer_class: @sniffer_class,
437
+ serializer_class: @serializer_class,
438
+ }), &adapter_conf)
439
+ Elasticsearch::Client.new transport: transport
440
+ end
441
+ end
442
+
443
+ def get_escaped_userinfo(host_str)
444
+ if m = host_str.match(/(?<scheme>.*)%{(?<user>.*)}:%{(?<password>.*)}(?<path>@.*)/)
445
+ m["scheme"] +
446
+ URI.encode_www_form_component(m["user"]) +
447
+ ':' +
448
+ URI.encode_www_form_component(m["password"]) +
449
+ m["path"]
450
+ else
451
+ host_str
452
+ end
453
+ end
454
+
455
+ def get_connection_options(con_host=nil)
456
+ raise "`password` must be present if `user` is present" if @user && !@password
457
+
458
+ hosts = if con_host || @hosts
459
+ (con_host || @hosts).split(',').map do |host_str|
460
+ # Support legacy hosts format host:port,host:port,host:port...
461
+ if host_str.match(%r{^[^:]+(\:\d+)?$})
462
+ {
463
+ host: host_str.split(':')[0],
464
+ port: (host_str.split(':')[1] || @port).to_i,
465
+ scheme: @scheme.to_s
466
+ }
467
+ else
468
+ # New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
469
+ uri = URI(get_escaped_userinfo(host_str))
470
+ %w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
471
+ hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
472
+ hash
473
+ end
474
+ end
475
+ end.compact
476
+ else
477
+ [{host: @host, port: @port, scheme: @scheme.to_s}]
478
+ end.each do |host|
479
+ host.merge!(user: @user, password: @password) if !host[:user] && @user
480
+ host.merge!(path: @path) if !host[:path] && @path
481
+ end
482
+
483
+ {
484
+ hosts: hosts
485
+ }
486
+ end
487
+
488
+ def connection_options_description(con_host=nil)
489
+ get_connection_options(con_host)[:hosts].map do |host_info|
490
+ attributes = host_info.dup
491
+ attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
492
+ attributes.inspect
493
+ end.join(', ')
494
+ end
495
+
496
+ # append_record_to_messages adds a record to the bulk message
497
+ # payload to be submitted to Elasticsearch. Records that do
498
+ # not include '_id' field are skipped when 'write_operation'
499
+ # is configured for 'create' or 'update'
500
+ #
501
+ # returns 'true' if record was appended to the bulk message
502
+ # and 'false' otherwise
503
+ def append_record_to_messages(op, meta, header, record, msgs)
504
+ case op
505
+ when UPDATE_OP, UPSERT_OP
506
+ if meta.has_key?(ID_FIELD)
507
+ header[UPDATE_OP] = meta
508
+ msgs << @dump_proc.call(header) << BODY_DELIMITER
509
+ msgs << @dump_proc.call(update_body(record, op)) << BODY_DELIMITER
510
+ return true
511
+ end
512
+ when CREATE_OP
513
+ if meta.has_key?(ID_FIELD)
514
+ header[CREATE_OP] = meta
515
+ msgs << @dump_proc.call(header) << BODY_DELIMITER
516
+ msgs << @dump_proc.call(record) << BODY_DELIMITER
517
+ return true
518
+ end
519
+ when INDEX_OP
520
+ header[INDEX_OP] = meta
521
+ msgs << @dump_proc.call(header) << BODY_DELIMITER
522
+ msgs << @dump_proc.call(record) << BODY_DELIMITER
523
+ return true
524
+ end
525
+ return false
526
+ end
527
+
528
+ def update_body(record, op)
529
+ update = remove_keys(record)
530
+ if @suppress_doc_wrap
531
+ return update
532
+ end
533
+ body = {"doc".freeze => update}
534
+ if op == UPSERT_OP
535
+ if update == record
536
+ body["doc_as_upsert".freeze] = true
537
+ else
538
+ body[UPSERT_OP] = record
539
+ end
540
+ end
541
+ body
542
+ end
543
+
544
+ def remove_keys(record)
545
+ keys = record[@remove_keys_on_update_key] || @remove_keys_on_update || []
546
+ record.delete(@remove_keys_on_update_key)
547
+ return record unless keys.any?
548
+ record = record.dup
549
+ keys.each { |key| record.delete(key) }
550
+ record
551
+ end
552
+
553
+ def flatten_record(record, prefix=[])
554
+ ret = {}
555
+ if record.is_a? Hash
556
+ record.each { |key, value|
557
+ ret.merge! flatten_record(value, prefix + [key.to_s])
558
+ }
559
+ elsif record.is_a? Array
560
+ # Don't mess with arrays, leave them unprocessed
561
+ ret.merge!({prefix.join(@flatten_hashes_separator) => record})
562
+ else
563
+ return {prefix.join(@flatten_hashes_separator) => record}
564
+ end
565
+ ret
566
+ end
567
+
568
+ def expand_placeholders(chunk)
569
+ logstash_prefix = extract_placeholders(@logstash_prefix, chunk)
570
+ index_name = extract_placeholders(@index_name, chunk)
571
+ type_name = extract_placeholders(@type_name, chunk)
572
+ return logstash_prefix, index_name, type_name
573
+ end
574
+
575
+ def multi_workers_ready?
576
+ true
577
+ end
578
+
579
+ def write(chunk)
580
+ bulk_message_count = Hash.new { |h,k| h[k] = 0 }
581
+ bulk_message = Hash.new { |h,k| h[k] = '' }
582
+ header = {}
583
+ meta = {}
584
+
585
+ tag = chunk.metadata.tag
586
+ extracted_values = expand_placeholders(chunk)
587
+ host = if @hosts
588
+ extract_placeholders(@hosts, chunk)
589
+ else
590
+ extract_placeholders(@host, chunk)
591
+ end
592
+
593
+ chunk.msgpack_each do |time, record|
594
+ next unless record.is_a? Hash
595
+ begin
596
+ meta, header, record = process_message(tag, meta, header, time, record, extracted_values)
597
+ info = if @include_index_in_url
598
+ RequestInfo.new(host, meta.delete("_index".freeze))
599
+ else
600
+ RequestInfo.new(host, nil)
601
+ end
602
+
603
+ if split_request?(bulk_message, info)
604
+ bulk_message.each do |info, msgs|
605
+ send_bulk(msgs, tag, chunk, bulk_message_count[info], extracted_values, info) unless msgs.empty?
606
+ msgs.clear
607
+ # Clear bulk_message_count for this info.
608
+ bulk_message_count[info] = 0;
609
+ next
610
+ end
611
+ end
612
+
613
+ if append_record_to_messages(@write_operation, meta, header, record, bulk_message[info])
614
+ bulk_message_count[info] += 1;
615
+ else
616
+ if @emit_error_for_missing_id
617
+ raise MissingIdFieldError, "Missing '_id' field. Write operation is #{@write_operation}"
618
+ else
619
+ log.on_debug { log.debug("Dropping record because its missing an '_id' field and write_operation is #{@write_operation}: #{record}") }
620
+ end
621
+ end
622
+ rescue => e
623
+ router.emit_error_event(tag, time, record, e)
624
+ end
625
+ end
626
+
627
+ bulk_message.each do |info, msgs|
628
+ send_bulk(msgs, tag, chunk, bulk_message_count[info], extracted_values, info) unless msgs.empty?
629
+ msgs.clear
630
+ end
631
+ end
632
+
633
+ def split_request?(bulk_message, info)
634
+ # For safety.
635
+ end
636
+
637
+ def split_request_size_check?(bulk_message, info)
638
+ bulk_message[info].size > @bulk_message_request_threshold
639
+ end
640
+
641
+ def split_request_size_uncheck?(bulk_message, info)
642
+ false
643
+ end
644
+
645
+ def process_message(tag, meta, header, time, record, extracted_values)
646
+ logstash_prefix, index_name, type_name = extracted_values
647
+
648
+ if @flatten_hashes
649
+ record = flatten_record(record)
650
+ end
651
+
652
+ dt = nil
653
+ if @logstash_format || @include_timestamp
654
+ if record.has_key?(TIMESTAMP_FIELD)
655
+ rts = record[TIMESTAMP_FIELD]
656
+ dt = parse_time(rts, time, tag)
657
+ elsif record.has_key?(@time_key)
658
+ rts = record[@time_key]
659
+ dt = parse_time(rts, time, tag)
660
+ record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision) unless @time_key_exclude_timestamp
661
+ else
662
+ dt = Time.at(time).to_datetime
663
+ record[TIMESTAMP_FIELD] = dt.iso8601(@time_precision)
664
+ end
665
+ end
666
+
667
+ target_index_parent, target_index_child_key = @target_index_key ? get_parent_of(record, @target_index_key) : nil
668
+ if target_index_parent && target_index_parent[target_index_child_key]
669
+ target_index = target_index_parent.delete(target_index_child_key)
670
+ elsif @logstash_format
671
+ dt = dt.new_offset(0) if @utc_index
672
+ target_index = "#{logstash_prefix}#{@logstash_prefix_separator}#{dt.strftime(@logstash_dateformat)}"
673
+ else
674
+ target_index = index_name
675
+ end
676
+
677
+ # Change target_index to lower-case since Elasticsearch doesn't
678
+ # allow upper-case characters in index names.
679
+ target_index = target_index.downcase
680
+ if @include_tag_key
681
+ record[@tag_key] = tag
682
+ end
683
+
684
+ target_type_parent, target_type_child_key = @target_type_key ? get_parent_of(record, @target_type_key) : nil
685
+ if target_type_parent && target_type_parent[target_type_child_key]
686
+ target_type = target_type_parent.delete(target_type_child_key)
687
+ if @last_seen_major_version == 6
688
+ log.warn "Detected ES 6.x: `@type_name` will be used as the document `_type`."
689
+ target_type = type_name
690
+ elsif @last_seen_major_version >= 7
691
+ log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
692
+ target_type = '_doc'.freeze
693
+ end
694
+ else
695
+ if @last_seen_major_version >= 7 && @type_name != DEFAULT_TYPE_NAME_ES_7x
696
+ log.warn "Detected ES 7.x or above: `_doc` will be used as the document `_type`."
697
+ target_type = '_doc'.freeze
698
+ else
699
+ target_type = type_name
700
+ end
701
+ end
702
+
703
+ meta.clear
704
+ meta["_index".freeze] = target_index
705
+ meta["_type".freeze] = target_type
706
+
707
+ if @pipeline
708
+ meta["pipeline".freeze] = @pipeline
709
+ end
710
+
711
+ @meta_config_map.each do |record_accessor, meta_key|
712
+ if raw_value = record_accessor.call(record)
713
+ meta[meta_key] = raw_value
714
+ end
715
+ end
716
+
717
+ if @remove_keys
718
+ @remove_keys.each { |key| record.delete(key) }
719
+ end
720
+
721
+ return [meta, header, record]
722
+ end
723
+
724
+ # returns [parent, child_key] of child described by path array in record's tree
725
+ # returns [nil, child_key] if path doesnt exist in record
726
+ def get_parent_of(record, path)
727
+ parent_object = path[0..-2].reduce(record) { |a, e| a.is_a?(Hash) ? a[e] : nil }
728
+ [parent_object, path[-1]]
729
+ end
730
+
731
+ # send_bulk given a specific bulk request, the original tag,
732
+ # chunk, and bulk_message_count
733
+ def send_bulk(data, tag, chunk, bulk_message_count, extracted_values, info)
734
+ begin
735
+
736
+ log.on_trace { log.trace "bulk request: #{data}" }
737
+ response = client(info.host).bulk body: data, index: info.index
738
+ log.on_trace { log.trace "bulk response: #{response}" }
739
+
740
+ if response['errors']
741
+ error = Fluent::Plugin::ElasticsearchErrorHandler.new(self)
742
+ error.handle_error(response, tag, chunk, bulk_message_count, extracted_values)
743
+ end
744
+ rescue RetryStreamError => e
745
+ emit_tag = @retry_tag ? @retry_tag : tag
746
+ router.emit_stream(emit_tag, e.retry_stream)
747
+ rescue => e
748
+ ignore = @ignore_exception_classes.any? { |clazz| e.class <= clazz }
749
+
750
+ log.warn "Exception ignored in tag #{tag}: #{e.class.name} #{e.message}" if ignore
751
+
752
+ @_es = nil if @reconnect_on_error
753
+ @_es_info = nil if @reconnect_on_error
754
+
755
+ raise UnrecoverableRequestFailure if ignore && @exception_backup
756
+
757
+ # FIXME: identify unrecoverable errors and raise UnrecoverableRequestFailure instead
758
+ raise RecoverableRequestFailure, "could not push logs to Elasticsearch cluster (#{connection_options_description(info.host)}): #{e.message}" unless ignore
759
+ end
760
+ end
761
+
762
+ def is_existing_connection(host)
763
+ # check if the host provided match the current connection
764
+ return false if @_es.nil?
765
+ return false if @current_config.nil?
766
+ return false if host.length != @current_config.length
767
+
768
+ for i in 0...host.length
769
+ if !host[i][:host].eql? @current_config[i][:host] || host[i][:port] != @current_config[i][:port]
770
+ return false
771
+ end
772
+ end
773
+
774
+ return true
775
+ end
776
+ end
777
+ end