fluent-plugin-es-mohit 1.9.3
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 +7 -0
- data/.coveralls.yml +2 -0
- data/.editorconfig +9 -0
- data/.gitignore +18 -0
- data/.travis.yml +9 -0
- data/Gemfile +8 -0
- data/History.md +112 -0
- data/ISSUE_TEMPLATE.md +11 -0
- data/LICENSE.txt +22 -0
- data/PULL_REQUEST_TEMPLATE.md +10 -0
- data/README.md +520 -0
- data/Rakefile +11 -0
- data/fluent-plugin-elasticsearch.gemspec +30 -0
- data/lib/fluent/plugin/elasticsearch_index_template.rb +37 -0
- data/lib/fluent/plugin/out_elasticsearch.rb +371 -0
- data/lib/fluent/plugin/out_elasticsearch_dynamic.rb +269 -0
- data/test/helper.rb +24 -0
- data/test/plugin/test_out_elasticsearch.rb +1138 -0
- data/test/plugin/test_out_elasticsearch_dynamic.rb +647 -0
- data/test/plugin/test_template.json +23 -0
- metadata +165 -0
@@ -0,0 +1,371 @@
|
|
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/output'
|
13
|
+
require_relative 'elasticsearch_index_template'
|
14
|
+
|
15
|
+
class Fluent::ElasticsearchOutput < Fluent::ObjectBufferedOutput
|
16
|
+
class ConnectionFailure < StandardError; end
|
17
|
+
|
18
|
+
Fluent::Plugin.register_output('elasticsearch', self)
|
19
|
+
|
20
|
+
config_param :host, :string, :default => 'localhost'
|
21
|
+
config_param :port, :integer, :default => 9200
|
22
|
+
config_param :user, :string, :default => nil
|
23
|
+
config_param :password, :string, :default => nil, :secret => true
|
24
|
+
config_param :path, :string, :default => nil
|
25
|
+
config_param :scheme, :string, :default => 'http'
|
26
|
+
config_param :hosts, :string, :default => nil
|
27
|
+
config_param :target_index_key, :string, :default => nil
|
28
|
+
config_param :target_type_key, :string, :default => nil
|
29
|
+
config_param :time_key_format, :string, :default => nil
|
30
|
+
config_param :logstash_format, :bool, :default => false
|
31
|
+
config_param :logstash_prefix, :string, :default => "logstash"
|
32
|
+
config_param :logstash_dateformat, :string, :default => "%Y.%m.%d"
|
33
|
+
config_param :utc_index, :bool, :default => true
|
34
|
+
config_param :type_name, :string, :default => "fluentd"
|
35
|
+
config_param :index_name, :string, :default => "fluentd"
|
36
|
+
config_param :id_key, :string, :default => nil
|
37
|
+
config_param :write_operation, :string, :default => "index"
|
38
|
+
config_param :parent_key, :string, :default => nil
|
39
|
+
config_param :routing_key, :string, :default => nil
|
40
|
+
config_param :request_timeout, :time, :default => 5
|
41
|
+
config_param :reload_connections, :bool, :default => true
|
42
|
+
config_param :reload_on_failure, :bool, :default => false
|
43
|
+
config_param :resurrect_after, :time, :default => 60
|
44
|
+
config_param :time_key, :string, :default => nil
|
45
|
+
config_param :time_key_exclude_timestamp, :bool, :default => false
|
46
|
+
config_param :ssl_verify , :bool, :default => true
|
47
|
+
config_param :client_key, :string, :default => nil
|
48
|
+
config_param :client_cert, :string, :default => nil
|
49
|
+
config_param :client_key_pass, :string, :default => nil
|
50
|
+
config_param :ca_file, :string, :default => nil
|
51
|
+
config_param :remove_keys, :string, :default => nil
|
52
|
+
config_param :remove_keys_on_update, :string, :default => ""
|
53
|
+
config_param :remove_keys_on_update_key, :string, :default => nil
|
54
|
+
config_param :flatten_hashes, :bool, :default => false
|
55
|
+
config_param :flatten_hashes_separator, :string, :default => "_"
|
56
|
+
config_param :template_name, :string, :default => nil
|
57
|
+
config_param :template_file, :string, :default => nil
|
58
|
+
config_param :templates, :hash, :default => nil
|
59
|
+
config_param :include_tag_key, :bool, :default => false
|
60
|
+
config_param :tag_key, :string, :default => 'tag'
|
61
|
+
config_param :time_parse_error_tag, :string, :default => 'Fluent::ElasticsearchOutput::TimeParser.error'
|
62
|
+
config_param :reconnect_on_error, :bool, :default => false
|
63
|
+
config_param :pipeline, :string, :default => nil
|
64
|
+
|
65
|
+
include Fluent::ElasticsearchIndexTemplate
|
66
|
+
|
67
|
+
def initialize
|
68
|
+
super
|
69
|
+
end
|
70
|
+
|
71
|
+
def configure(conf)
|
72
|
+
super
|
73
|
+
@time_parser = create_time_parser
|
74
|
+
|
75
|
+
if @remove_keys
|
76
|
+
@remove_keys = @remove_keys.split(/\s*,\s*/)
|
77
|
+
end
|
78
|
+
|
79
|
+
if @target_index_key && @target_index_key.is_a?(String)
|
80
|
+
@target_index_key = @target_index_key.split '.'
|
81
|
+
end
|
82
|
+
|
83
|
+
if @target_type_key && @target_type_key.is_a?(String)
|
84
|
+
@target_type_key = @target_type_key.split '.'
|
85
|
+
end
|
86
|
+
|
87
|
+
if @remove_keys_on_update && @remove_keys_on_update.is_a?(String)
|
88
|
+
@remove_keys_on_update = @remove_keys_on_update.split ','
|
89
|
+
end
|
90
|
+
|
91
|
+
if @template_name && @template_file
|
92
|
+
template_install(@template_name, @template_file)
|
93
|
+
elsif @templates
|
94
|
+
templates_hash_install (@templates)
|
95
|
+
end
|
96
|
+
|
97
|
+
@meta_config_map = create_meta_config_map
|
98
|
+
|
99
|
+
begin
|
100
|
+
require 'oj'
|
101
|
+
@dump_proc = Oj.method(:dump)
|
102
|
+
rescue LoadError
|
103
|
+
@dump_proc = Yajl.method(:dump)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def create_meta_config_map
|
108
|
+
result = []
|
109
|
+
result << [@id_key, '_id'] if @id_key
|
110
|
+
result << [@parent_key, '_parent'] if @parent_key
|
111
|
+
result << [@routing_key, '_routing'] if @routing_key
|
112
|
+
result
|
113
|
+
end
|
114
|
+
|
115
|
+
# once fluent v0.14 is released we might be able to use
|
116
|
+
# Fluent::Parser::TimeParser, but it doesn't quite do what we want - if gives
|
117
|
+
# [sec,nsec] where as we want something we can call `strftime` on...
|
118
|
+
def create_time_parser
|
119
|
+
if @time_key_format
|
120
|
+
begin
|
121
|
+
# Strptime doesn't support all formats, but for those it does it's
|
122
|
+
# blazingly fast.
|
123
|
+
strptime = Strptime.new(@time_key_format)
|
124
|
+
Proc.new { |value| strptime.exec(value).to_datetime }
|
125
|
+
rescue
|
126
|
+
# Can happen if Strptime doesn't recognize the format; or
|
127
|
+
# if strptime couldn't be required (because it's not installed -- it's
|
128
|
+
# ruby 2 only)
|
129
|
+
Proc.new { |value| DateTime.strptime(value, @time_key_format) }
|
130
|
+
end
|
131
|
+
else
|
132
|
+
Proc.new { |value| DateTime.parse(value) }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def parse_time(value, event_time, tag)
|
137
|
+
@time_parser.call(value)
|
138
|
+
rescue => e
|
139
|
+
router.emit_error_event(@time_parse_error_tag, Fluent::Engine.now, {'tag' => tag, 'time' => event_time, 'format' => @time_key_format, 'value' => value}, e)
|
140
|
+
return Time.at(event_time).to_datetime
|
141
|
+
end
|
142
|
+
|
143
|
+
def client
|
144
|
+
@_es ||= begin
|
145
|
+
excon_options = { client_key: @client_key, client_cert: @client_cert, client_key_pass: @client_key_pass }
|
146
|
+
adapter_conf = lambda {|f| f.adapter :excon, excon_options }
|
147
|
+
transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(get_connection_options.merge(
|
148
|
+
options: {
|
149
|
+
reload_connections: @reload_connections,
|
150
|
+
reload_on_failure: @reload_on_failure,
|
151
|
+
resurrect_after: @resurrect_after,
|
152
|
+
retry_on_failure: 5,
|
153
|
+
transport_options: {
|
154
|
+
request: { timeout: @request_timeout },
|
155
|
+
ssl: { verify: @ssl_verify, ca_file: @ca_file }
|
156
|
+
}
|
157
|
+
}), &adapter_conf)
|
158
|
+
es = Elasticsearch::Client.new transport: transport
|
159
|
+
|
160
|
+
begin
|
161
|
+
raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description})!" unless es.ping
|
162
|
+
rescue *es.transport.host_unreachable_exceptions => e
|
163
|
+
raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description})! #{e.message}"
|
164
|
+
end
|
165
|
+
|
166
|
+
log.info "Connection opened to Elasticsearch cluster => #{connection_options_description}"
|
167
|
+
es
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def get_connection_options
|
172
|
+
raise "`password` must be present if `user` is present" if @user && !@password
|
173
|
+
|
174
|
+
hosts = if @hosts
|
175
|
+
@hosts.split(',').map do |host_str|
|
176
|
+
# Support legacy hosts format host:port,host:port,host:port...
|
177
|
+
if host_str.match(%r{^[^:]+(\:\d+)?$})
|
178
|
+
{
|
179
|
+
host: host_str.split(':')[0],
|
180
|
+
port: (host_str.split(':')[1] || @port).to_i,
|
181
|
+
scheme: @scheme
|
182
|
+
}
|
183
|
+
else
|
184
|
+
# New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
|
185
|
+
uri = URI(host_str)
|
186
|
+
%w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
|
187
|
+
hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
|
188
|
+
hash
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end.compact
|
192
|
+
else
|
193
|
+
[{host: @host, port: @port, scheme: @scheme}]
|
194
|
+
end.each do |host|
|
195
|
+
host.merge!(user: @user, password: @password) if !host[:user] && @user
|
196
|
+
host.merge!(path: @path) if !host[:path] && @path
|
197
|
+
end
|
198
|
+
|
199
|
+
{
|
200
|
+
hosts: hosts
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
204
|
+
def connection_options_description
|
205
|
+
get_connection_options[:hosts].map do |host_info|
|
206
|
+
attributes = host_info.dup
|
207
|
+
attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
|
208
|
+
attributes.inspect
|
209
|
+
end.join(', ')
|
210
|
+
end
|
211
|
+
|
212
|
+
BODY_DELIMITER = "\n".freeze
|
213
|
+
UPDATE_OP = "update".freeze
|
214
|
+
UPSERT_OP = "upsert".freeze
|
215
|
+
CREATE_OP = "create".freeze
|
216
|
+
INDEX_OP = "index".freeze
|
217
|
+
ID_FIELD = "_id".freeze
|
218
|
+
TIMESTAMP_FIELD = "@timestamp".freeze
|
219
|
+
|
220
|
+
def append_record_to_messages(op, meta, header, record, msgs)
|
221
|
+
case op
|
222
|
+
when UPDATE_OP, UPSERT_OP
|
223
|
+
if meta.has_key?(ID_FIELD)
|
224
|
+
header[UPDATE_OP] = meta
|
225
|
+
msgs << @dump_proc.call(header) << BODY_DELIMITER
|
226
|
+
msgs << @dump_proc.call(update_body(record, op)) << BODY_DELIMITER
|
227
|
+
end
|
228
|
+
when CREATE_OP
|
229
|
+
if meta.has_key?(ID_FIELD)
|
230
|
+
header[CREATE_OP] = meta
|
231
|
+
msgs << @dump_proc.call(header) << BODY_DELIMITER
|
232
|
+
msgs << @dump_proc.call(record) << BODY_DELIMITER
|
233
|
+
end
|
234
|
+
when INDEX_OP
|
235
|
+
header[INDEX_OP] = meta
|
236
|
+
msgs << @dump_proc.call(header) << BODY_DELIMITER
|
237
|
+
msgs << @dump_proc.call(record) << BODY_DELIMITER
|
238
|
+
end
|
239
|
+
if @pipeline
|
240
|
+
msgs << "\"pipeline\" : \"" << @pipeline << "\"" << BODY_DELIMITER
|
241
|
+
end
|
242
|
+
|
243
|
+
def update_body(record, op)
|
244
|
+
update = remove_keys(record)
|
245
|
+
body = {"doc".freeze => update}
|
246
|
+
if op == UPSERT_OP
|
247
|
+
if update == record
|
248
|
+
body["doc_as_upsert".freeze] = true
|
249
|
+
else
|
250
|
+
body[UPSERT_OP] = record
|
251
|
+
end
|
252
|
+
end
|
253
|
+
body
|
254
|
+
end
|
255
|
+
|
256
|
+
def remove_keys(record)
|
257
|
+
keys = record[@remove_keys_on_update_key] || @remove_keys_on_update || []
|
258
|
+
record.delete(@remove_keys_on_update_key)
|
259
|
+
return record unless keys.any?
|
260
|
+
record = record.dup
|
261
|
+
keys.each { |key| record.delete(key) }
|
262
|
+
record
|
263
|
+
end
|
264
|
+
|
265
|
+
def flatten_record(record, prefix=[])
|
266
|
+
ret = {}
|
267
|
+
if record.is_a? Hash
|
268
|
+
record.each { |key, value|
|
269
|
+
ret.merge! flatten_record(value, prefix + [key.to_s])
|
270
|
+
}
|
271
|
+
elsif record.is_a? Array
|
272
|
+
# Don't mess with arrays, leave them unprocessed
|
273
|
+
ret.merge!({prefix.join(@flatten_hashes_separator) => record})
|
274
|
+
else
|
275
|
+
return {prefix.join(@flatten_hashes_separator) => record}
|
276
|
+
end
|
277
|
+
ret
|
278
|
+
end
|
279
|
+
|
280
|
+
def write_objects(tag, chunk)
|
281
|
+
bulk_message = ''
|
282
|
+
header = {}
|
283
|
+
meta = {}
|
284
|
+
|
285
|
+
chunk.msgpack_each do |time, record|
|
286
|
+
next unless record.is_a? Hash
|
287
|
+
|
288
|
+
if @flatten_hashes
|
289
|
+
record = flatten_record(record)
|
290
|
+
end
|
291
|
+
|
292
|
+
target_index_parent, target_index_child_key = @target_index_key ? get_parent_of(record, @target_index_key) : nil
|
293
|
+
if target_index_parent && target_index_parent[target_index_child_key]
|
294
|
+
target_index = target_index_parent.delete(target_index_child_key)
|
295
|
+
elsif @logstash_format
|
296
|
+
if record.has_key?(TIMESTAMP_FIELD)
|
297
|
+
rts = record[TIMESTAMP_FIELD]
|
298
|
+
dt = parse_time(rts, time, tag)
|
299
|
+
elsif record.has_key?(@time_key)
|
300
|
+
rts = record[@time_key]
|
301
|
+
dt = parse_time(rts, time, tag)
|
302
|
+
record[TIMESTAMP_FIELD] = rts unless @time_key_exclude_timestamp
|
303
|
+
else
|
304
|
+
dt = Time.at(time).to_datetime
|
305
|
+
record[TIMESTAMP_FIELD] = dt.to_s
|
306
|
+
end
|
307
|
+
dt = dt.new_offset(0) if @utc_index
|
308
|
+
target_index = "#{@logstash_prefix}-#{dt.strftime(@logstash_dateformat)}"
|
309
|
+
else
|
310
|
+
target_index = @index_name
|
311
|
+
end
|
312
|
+
|
313
|
+
# Change target_index to lower-case since Elasticsearch doesn't
|
314
|
+
# allow upper-case characters in index names.
|
315
|
+
target_index = target_index.downcase
|
316
|
+
if @include_tag_key
|
317
|
+
record[@tag_key] = tag
|
318
|
+
end
|
319
|
+
|
320
|
+
target_type_parent, target_type_child_key = @target_type_key ? get_parent_of(record, @target_type_key) : nil
|
321
|
+
if target_type_parent && target_type_parent[target_type_child_key]
|
322
|
+
target_type = target_type_parent.delete(target_type_child_key)
|
323
|
+
else
|
324
|
+
target_type = @type_name
|
325
|
+
end
|
326
|
+
|
327
|
+
meta.clear
|
328
|
+
meta["_index".freeze] = target_index
|
329
|
+
meta["_type".freeze] = target_type
|
330
|
+
|
331
|
+
@meta_config_map.each do |record_key, meta_key|
|
332
|
+
meta[meta_key] = record[record_key] if record[record_key]
|
333
|
+
end
|
334
|
+
|
335
|
+
if @remove_keys
|
336
|
+
@remove_keys.each { |key| record.delete(key) }
|
337
|
+
end
|
338
|
+
|
339
|
+
append_record_to_messages(@write_operation, meta, header, record, bulk_message)
|
340
|
+
end
|
341
|
+
|
342
|
+
send_bulk(bulk_message) unless bulk_message.empty?
|
343
|
+
bulk_message.clear
|
344
|
+
end
|
345
|
+
|
346
|
+
# returns [parent, child_key] of child described by path array in record's tree
|
347
|
+
# returns [nil, child_key] if path doesnt exist in record
|
348
|
+
def get_parent_of(record, path)
|
349
|
+
parent_object = path[0..-2].reduce(record) { |a, e| a.is_a?(Hash) ? a[e] : nil }
|
350
|
+
[parent_object, path[-1]]
|
351
|
+
end
|
352
|
+
|
353
|
+
def send_bulk(data)
|
354
|
+
retries = 0
|
355
|
+
begin
|
356
|
+
client.bulk body: data
|
357
|
+
rescue *client.transport.host_unreachable_exceptions => e
|
358
|
+
if retries < 2
|
359
|
+
retries += 1
|
360
|
+
@_es = nil
|
361
|
+
log.warn "Could not push logs to Elasticsearch, resetting connection and trying again. #{e.message}"
|
362
|
+
sleep 2**retries
|
363
|
+
retry
|
364
|
+
end
|
365
|
+
raise ConnectionFailure, "Could not push logs to Elasticsearch after #{retries} retries. #{e.message}"
|
366
|
+
rescue Exception
|
367
|
+
@_es = nil if @reconnect_on_error
|
368
|
+
raise
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative 'out_elasticsearch'
|
3
|
+
|
4
|
+
class Fluent::ElasticsearchOutputDynamic < Fluent::ElasticsearchOutput
|
5
|
+
|
6
|
+
Fluent::Plugin.register_output('elasticsearch_dynamic', self)
|
7
|
+
|
8
|
+
config_param :delimiter, :string, :default => "."
|
9
|
+
|
10
|
+
# params overloaded as strings
|
11
|
+
config_param :port, :string, :default => "9200"
|
12
|
+
config_param :logstash_format, :string, :default => "false"
|
13
|
+
config_param :utc_index, :string, :default => "true"
|
14
|
+
config_param :time_key_exclude_timestamp, :bool, :default => false
|
15
|
+
config_param :reload_connections, :string, :default => "true"
|
16
|
+
config_param :reload_on_failure, :string, :default => "false"
|
17
|
+
config_param :resurrect_after, :string, :default => "60"
|
18
|
+
config_param :ssl_verify, :string, :default => "true"
|
19
|
+
config_param :reconnect_on_error, :bool, :default => false
|
20
|
+
config_param :pipeline, :string, :default => nil
|
21
|
+
|
22
|
+
def configure(conf)
|
23
|
+
super
|
24
|
+
|
25
|
+
# evaluate all configurations here
|
26
|
+
@dynamic_params ||= []
|
27
|
+
@dynamic_params += self.instance_variables.select { |var| is_valid_expand_param_type(var) }
|
28
|
+
@dynamic_config = Hash.new
|
29
|
+
@dynamic_params.each { |var|
|
30
|
+
value = expand_param(self.instance_variable_get(var), nil, nil, nil)
|
31
|
+
var = var[1..-1]
|
32
|
+
@dynamic_config[var] = value
|
33
|
+
}
|
34
|
+
# end eval all configs
|
35
|
+
@current_config = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_meta_config_map
|
39
|
+
{'id_key' => '_id', 'parent_key' => '_parent', 'routing_key' => '_routing'}
|
40
|
+
end
|
41
|
+
|
42
|
+
def client(host)
|
43
|
+
|
44
|
+
# check here to see if we already have a client connection for the given host
|
45
|
+
connection_options = get_connection_options(host)
|
46
|
+
|
47
|
+
@_es = nil unless is_existing_connection(connection_options[:hosts])
|
48
|
+
|
49
|
+
@_es ||= begin
|
50
|
+
@current_config = connection_options[:hosts].clone
|
51
|
+
excon_options = { client_key: @dynamic_config['client_key'], client_cert: @dynamic_config['client_cert'], client_key_pass: @dynamic_config['client_key_pass'] }
|
52
|
+
adapter_conf = lambda {|f| f.adapter :excon, excon_options }
|
53
|
+
transport = Elasticsearch::Transport::Transport::HTTP::Faraday.new(connection_options.merge(
|
54
|
+
options: {
|
55
|
+
reload_connections: Fluent::Config.bool_value(@dynamic_config['reload_connections']),
|
56
|
+
reload_on_failure: Fluent::Config.bool_value(@dynamic_config['reload_on_failure']),
|
57
|
+
resurrect_after: @dynamic_config['resurrect_after'].to_i,
|
58
|
+
retry_on_failure: 5,
|
59
|
+
transport_options: {
|
60
|
+
request: { timeout: @dynamic_config['request_timeout'] },
|
61
|
+
ssl: { verify: @dynamic_config['ssl_verify'], ca_file: @dynamic_config['ca_file'] }
|
62
|
+
}
|
63
|
+
}), &adapter_conf)
|
64
|
+
es = Elasticsearch::Client.new transport: transport
|
65
|
+
|
66
|
+
begin
|
67
|
+
raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description(host)})!" unless es.ping
|
68
|
+
rescue *es.transport.host_unreachable_exceptions => e
|
69
|
+
raise ConnectionFailure, "Can not reach Elasticsearch cluster (#{connection_options_description(host)})! #{e.message}"
|
70
|
+
end
|
71
|
+
|
72
|
+
log.info "Connection opened to Elasticsearch cluster => #{connection_options_description(host)}"
|
73
|
+
es
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_connection_options(con_host)
|
78
|
+
raise "`password` must be present if `user` is present" if @dynamic_config['user'] && !@dynamic_config['password']
|
79
|
+
|
80
|
+
hosts = if con_host || @dynamic_config['hosts']
|
81
|
+
(con_host || @dynamic_config['hosts']).split(',').map do |host_str|
|
82
|
+
# Support legacy hosts format host:port,host:port,host:port...
|
83
|
+
if host_str.match(%r{^[^:]+(\:\d+)?$})
|
84
|
+
{
|
85
|
+
host: host_str.split(':')[0],
|
86
|
+
port: (host_str.split(':')[1] || @dynamic_config['port'] || @port).to_i,
|
87
|
+
scheme: @dynamic_config['scheme']
|
88
|
+
}
|
89
|
+
else
|
90
|
+
# New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
|
91
|
+
uri = URI(host_str)
|
92
|
+
%w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
|
93
|
+
hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
|
94
|
+
hash
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end.compact
|
98
|
+
else
|
99
|
+
[{host: @dynamic_config['host'], port: @dynamic_config['port'].to_i, scheme: @dynamic_config['scheme']}]
|
100
|
+
end.each do |host|
|
101
|
+
host.merge!(user: @dynamic_config['user'], password: @dynamic_config['password']) if !host[:user] && @dynamic_config['user']
|
102
|
+
host.merge!(path: @dynamic_config['path']) if !host[:path] && @dynamic_config['path']
|
103
|
+
end
|
104
|
+
|
105
|
+
{
|
106
|
+
hosts: hosts
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def connection_options_description(host)
|
111
|
+
get_connection_options(host)[:hosts].map do |host_info|
|
112
|
+
attributes = host_info.dup
|
113
|
+
attributes[:password] = 'obfuscated' if attributes.has_key?(:password)
|
114
|
+
attributes.inspect
|
115
|
+
end.join(', ')
|
116
|
+
end
|
117
|
+
|
118
|
+
def write_objects(tag, chunk)
|
119
|
+
bulk_message = Hash.new { |h,k| h[k] = '' }
|
120
|
+
dynamic_conf = @dynamic_config.clone
|
121
|
+
|
122
|
+
headers = {
|
123
|
+
UPDATE_OP => {},
|
124
|
+
UPSERT_OP => {},
|
125
|
+
CREATE_OP => {},
|
126
|
+
INDEX_OP => {}
|
127
|
+
}
|
128
|
+
|
129
|
+
chunk.msgpack_each do |time, record|
|
130
|
+
next unless record.is_a? Hash
|
131
|
+
|
132
|
+
# evaluate all configurations here
|
133
|
+
@dynamic_params.each { |var|
|
134
|
+
k = var[1..-1]
|
135
|
+
v = self.instance_variable_get(var)
|
136
|
+
# check here to determine if we should evaluate
|
137
|
+
if dynamic_conf[k] != v
|
138
|
+
value = expand_param(v, tag, time, record)
|
139
|
+
dynamic_conf[k] = value
|
140
|
+
end
|
141
|
+
}
|
142
|
+
# end eval all configs
|
143
|
+
|
144
|
+
if eval_or_val(dynamic_conf['logstash_format'])
|
145
|
+
if record.has_key?("@timestamp")
|
146
|
+
time = Time.parse record["@timestamp"]
|
147
|
+
elsif record.has_key?(dynamic_conf['time_key'])
|
148
|
+
time = Time.parse record[dynamic_conf['time_key']]
|
149
|
+
record['@timestamp'] = record[dynamic_conf['time_key']] unless time_key_exclude_timestamp
|
150
|
+
else
|
151
|
+
record.merge!({"@timestamp" => Time.at(time).to_datetime.to_s})
|
152
|
+
end
|
153
|
+
|
154
|
+
if eval_or_val(dynamic_conf['utc_index'])
|
155
|
+
target_index = "#{dynamic_conf['logstash_prefix']}-#{Time.at(time).getutc.strftime("#{dynamic_conf['logstash_dateformat']}")}"
|
156
|
+
else
|
157
|
+
target_index = "#{dynamic_conf['logstash_prefix']}-#{Time.at(time).strftime("#{dynamic_conf['logstash_dateformat']}")}"
|
158
|
+
end
|
159
|
+
else
|
160
|
+
target_index = dynamic_conf['index_name']
|
161
|
+
end
|
162
|
+
|
163
|
+
# Change target_index to lower-case since Elasticsearch doesn't
|
164
|
+
# allow upper-case characters in index names.
|
165
|
+
target_index = target_index.downcase
|
166
|
+
|
167
|
+
if @include_tag_key
|
168
|
+
record.merge!(dynamic_conf['tag_key'] => tag)
|
169
|
+
end
|
170
|
+
|
171
|
+
meta = {"_index" => target_index, "_type" => dynamic_conf['type_name']}
|
172
|
+
|
173
|
+
@meta_config_map.each_pair do |config_name, meta_key|
|
174
|
+
if dynamic_conf[config_name] && record[dynamic_conf[config_name]]
|
175
|
+
meta[meta_key] = record[dynamic_conf[config_name]]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
if dynamic_conf['hosts']
|
180
|
+
host = dynamic_conf['hosts']
|
181
|
+
else
|
182
|
+
host = "#{dynamic_conf['host']}:#{dynamic_conf['port']}"
|
183
|
+
end
|
184
|
+
|
185
|
+
if @remove_keys
|
186
|
+
@remove_keys.each { |key| record.delete(key) }
|
187
|
+
end
|
188
|
+
|
189
|
+
write_op = dynamic_conf["write_operation"]
|
190
|
+
append_record_to_messages(write_op, meta, headers[write_op], record, bulk_message[host])
|
191
|
+
end
|
192
|
+
|
193
|
+
bulk_message.each do |hKey, msgs|
|
194
|
+
send_bulk(msgs, hKey) unless msgs.empty?
|
195
|
+
msgs.clear
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def send_bulk(data, host)
|
200
|
+
retries = 0
|
201
|
+
begin
|
202
|
+
client(host).bulk body: data
|
203
|
+
rescue *client(host).transport.host_unreachable_exceptions => e
|
204
|
+
if retries < 2
|
205
|
+
retries += 1
|
206
|
+
@_es = nil
|
207
|
+
log.warn "Could not push logs to Elasticsearch, resetting connection and trying again. #{e.message}"
|
208
|
+
sleep 2**retries
|
209
|
+
retry
|
210
|
+
end
|
211
|
+
raise ConnectionFailure, "Could not push logs to Elasticsearch after #{retries} retries. #{e.message}"
|
212
|
+
rescue Exception
|
213
|
+
@_es = nil if @reconnect_on_error
|
214
|
+
raise
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def eval_or_val(var)
|
219
|
+
return var unless var.is_a?(String)
|
220
|
+
eval(var)
|
221
|
+
end
|
222
|
+
|
223
|
+
def expand_param(param, tag, time, record)
|
224
|
+
# check for '${ ... }'
|
225
|
+
# yes => `eval`
|
226
|
+
# no => return param
|
227
|
+
return param if (param =~ /\${.+}/).nil?
|
228
|
+
|
229
|
+
# check for 'tag_parts[]'
|
230
|
+
# separated by a delimiter (default '.')
|
231
|
+
tag_parts = tag.split(@delimiter) unless (param =~ /tag_parts\[.+\]/).nil? || tag.nil?
|
232
|
+
|
233
|
+
# pull out section between ${} then eval
|
234
|
+
inner = param.clone
|
235
|
+
while inner.match(/\${.+}/)
|
236
|
+
to_eval = inner.match(/\${(.+?)}/){$1}
|
237
|
+
|
238
|
+
if !(to_eval =~ /record\[.+\]/).nil? && record.nil?
|
239
|
+
return to_eval
|
240
|
+
elsif !(to_eval =~/tag_parts\[.+\]/).nil? && tag_parts.nil?
|
241
|
+
return to_eval
|
242
|
+
elsif !(to_eval =~/time/).nil? && time.nil?
|
243
|
+
return to_eval
|
244
|
+
else
|
245
|
+
inner.sub!(/\${.+?}/, eval( to_eval ))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
inner
|
249
|
+
end
|
250
|
+
|
251
|
+
def is_valid_expand_param_type(param)
|
252
|
+
return false if [:@buffer_type].include?(param)
|
253
|
+
return self.instance_variable_get(param).is_a?(String)
|
254
|
+
end
|
255
|
+
|
256
|
+
def is_existing_connection(host)
|
257
|
+
# check if the host provided match the current connection
|
258
|
+
return false if @_es.nil?
|
259
|
+
return false if host.length != @current_config.length
|
260
|
+
|
261
|
+
for i in 0...host.length
|
262
|
+
if !host[i][:host].eql? @current_config[i][:host] || host[i][:port] != @current_config[i][:port]
|
263
|
+
return false
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
return true
|
268
|
+
end
|
269
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start do
|
3
|
+
add_filter do |src|
|
4
|
+
!(src.filename =~ /^#{SimpleCov.root}\/lib/)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'coveralls'
|
9
|
+
Coveralls.wear!
|
10
|
+
|
11
|
+
# needs to be after simplecov but before test/unit, because fluentd sets default
|
12
|
+
# encoding to ASCII-8BIT, but coverall might load git data which could contain a
|
13
|
+
# UTF-8 character
|
14
|
+
at_exit do
|
15
|
+
Encoding.default_internal = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_internal)
|
16
|
+
Encoding.default_external = 'UTF-8' if defined?(Encoding) && Encoding.respond_to?(:default_external)
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'test/unit'
|
20
|
+
require 'fluent/test'
|
21
|
+
require 'minitest/pride'
|
22
|
+
|
23
|
+
require 'webmock/test_unit'
|
24
|
+
WebMock.disable_net_connect!
|