fluent-plugin-test 0.0.17
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/LICENSE.txt +201 -0
- data/README.md +1622 -0
- data/lib/fluent/log-ext.rb +64 -0
- data/lib/fluent/plugin/filter_opensearch_genid.rb +103 -0
- data/lib/fluent/plugin/in_opensearch.rb +441 -0
- data/lib/fluent/plugin/oj_serializer.rb +48 -0
- data/lib/fluent/plugin/opensearch_constants.rb +39 -0
- data/lib/fluent/plugin/opensearch_error.rb +31 -0
- data/lib/fluent/plugin/opensearch_error_handler.rb +182 -0
- data/lib/fluent/plugin/opensearch_fallback_selector.rb +36 -0
- data/lib/fluent/plugin/opensearch_index_template.rb +155 -0
- data/lib/fluent/plugin/opensearch_simple_sniffer.rb +36 -0
- data/lib/fluent/plugin/opensearch_tls.rb +96 -0
- data/lib/fluent/plugin/out_opensearch.rb +1162 -0
- data/lib/fluent/plugin/out_opensearch_data_stream.rb +231 -0
- metadata +241 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# The fluent-plugin-opensearch Contributors require contributions made to
|
4
|
+
# this file be licensed under the Apache-2.0 license or a
|
5
|
+
# compatible open source license.
|
6
|
+
#
|
7
|
+
# Modifications Copyright OpenSearch Contributors. See
|
8
|
+
# GitHub history for details.
|
9
|
+
#
|
10
|
+
# Licensed toUken Inc. under one or more contributor
|
11
|
+
# license agreements. See the NOTICE file distributed with
|
12
|
+
# this work for additional information regarding copyright
|
13
|
+
# ownership. Elasticsearch B.V. licenses this file to you under
|
14
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
15
|
+
# not use this file except in compliance with the License.
|
16
|
+
# You may obtain a copy of the License at
|
17
|
+
#
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
19
|
+
#
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
21
|
+
# software distributed under the License is distributed on an
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
23
|
+
# KIND, either express or implied. See the License for the
|
24
|
+
# specific language governing permissions and limitations
|
25
|
+
# under the License.
|
26
|
+
|
27
|
+
require 'fluent/log'
|
28
|
+
# For opensearch-ruby v1.0.0 or later which is based on elasticsearch-ruby v7.14 master tree
|
29
|
+
# logger for Elasticsearch::Loggable required the following methods:
|
30
|
+
#
|
31
|
+
# * debug?
|
32
|
+
# * info?
|
33
|
+
# * warn?
|
34
|
+
# * error?
|
35
|
+
# * fatal?
|
36
|
+
|
37
|
+
module Fluent
|
38
|
+
class Log
|
39
|
+
# OpenSearch::Loggable does not request trace? method.
|
40
|
+
# def trace?
|
41
|
+
# @level <= LEVEL_TRACE
|
42
|
+
# end
|
43
|
+
|
44
|
+
def debug?
|
45
|
+
@level <= LEVEL_DEBUG
|
46
|
+
end
|
47
|
+
|
48
|
+
def info?
|
49
|
+
@level <= LEVEL_INFO
|
50
|
+
end
|
51
|
+
|
52
|
+
def warn?
|
53
|
+
@level <= LEVEL_WARN
|
54
|
+
end
|
55
|
+
|
56
|
+
def error?
|
57
|
+
@level <= LEVEL_ERROR
|
58
|
+
end
|
59
|
+
|
60
|
+
def fatal?
|
61
|
+
@level <= LEVEL_FATAL
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# The fluent-plugin-opensearch Contributors require contributions made to
|
4
|
+
# this file be licensed under the Apache-2.0 license or a
|
5
|
+
# compatible open source license.
|
6
|
+
#
|
7
|
+
# Modifications Copyright fluent-plugin-opensearch Contributors. See
|
8
|
+
# GitHub history for details.
|
9
|
+
#
|
10
|
+
# Licensed to Uken Inc. under one or more contributor
|
11
|
+
# license agreements. See the NOTICE file distributed with
|
12
|
+
# this work for additional information regarding copyright
|
13
|
+
# ownership. Uken Inc. licenses this file to you under
|
14
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
15
|
+
# not use this file except in compliance with the License.
|
16
|
+
# You may obtain a copy of the License at
|
17
|
+
#
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
19
|
+
#
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
21
|
+
# software distributed under the License is distributed on an
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
23
|
+
# KIND, either express or implied. See the License for the
|
24
|
+
# specific language governing permissions and limitations
|
25
|
+
# under the License.
|
26
|
+
|
27
|
+
require 'securerandom'
|
28
|
+
require 'base64'
|
29
|
+
require 'fluent/plugin/filter'
|
30
|
+
|
31
|
+
module Fluent::Plugin
|
32
|
+
class OpenSearchGenidFilter < Filter
|
33
|
+
Fluent::Plugin.register_filter('opensearch_genid', self)
|
34
|
+
|
35
|
+
config_param :hash_id_key, :string, :default => '_hash'
|
36
|
+
config_param :include_tag_in_seed, :bool, :default => false
|
37
|
+
config_param :include_time_in_seed, :bool, :default => false
|
38
|
+
config_param :use_record_as_seed, :bool, :default => false
|
39
|
+
config_param :use_entire_record, :bool, :default => false
|
40
|
+
config_param :record_keys, :array, :default => []
|
41
|
+
config_param :separator, :string, :default => '_'
|
42
|
+
config_param :hash_type, :enum, list: [:md5, :sha1, :sha256, :sha512], :default => :sha1
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def configure(conf)
|
49
|
+
super
|
50
|
+
|
51
|
+
if !@use_entire_record
|
52
|
+
if @record_keys.empty? && @use_record_as_seed
|
53
|
+
raise Fluent::ConfigError, "When using record as hash seed, users must specify `record_keys`."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if @use_record_as_seed
|
58
|
+
class << self
|
59
|
+
alias_method :filter, :filter_seed_as_record
|
60
|
+
end
|
61
|
+
else
|
62
|
+
class << self
|
63
|
+
alias_method :filter, :filter_simple
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def filter(tag, time, record)
|
69
|
+
# for safety.
|
70
|
+
end
|
71
|
+
|
72
|
+
def filter_simple(tag, time, record)
|
73
|
+
record[@hash_id_key] = Base64.strict_encode64(SecureRandom.uuid)
|
74
|
+
record
|
75
|
+
end
|
76
|
+
|
77
|
+
def filter_seed_as_record(tag, time, record)
|
78
|
+
seed = ""
|
79
|
+
seed += tag + separator if @include_tag_in_seed
|
80
|
+
seed += time.to_s + separator if @include_time_in_seed
|
81
|
+
if @use_entire_record
|
82
|
+
record.each {|k,v| seed += "|#{k}|#{v}"}
|
83
|
+
else
|
84
|
+
seed += record_keys.map {|k| record[k]}.join(separator)
|
85
|
+
end
|
86
|
+
record[@hash_id_key] = Base64.strict_encode64(encode_hash(@hash_type, seed))
|
87
|
+
record
|
88
|
+
end
|
89
|
+
|
90
|
+
def encode_hash(type, seed)
|
91
|
+
case type
|
92
|
+
when :md5
|
93
|
+
Digest::MD5.digest(seed)
|
94
|
+
when :sha1
|
95
|
+
Digest::SHA1.digest(seed)
|
96
|
+
when :sha256
|
97
|
+
Digest::SHA256.digest(seed)
|
98
|
+
when :sha512
|
99
|
+
Digest::SHA512.digest(seed)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,441 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# The fluent-plugin-opensearch Contributors require contributions made to
|
4
|
+
# this file be licensed under the Apache-2.0 license or a
|
5
|
+
# compatible open source license.
|
6
|
+
#
|
7
|
+
# Modifications Copyright fluent-plugin-opensearch Contributors. See
|
8
|
+
# GitHub history for details.
|
9
|
+
#
|
10
|
+
# Licensed to Uken Inc. under one or more contributor
|
11
|
+
# license agreements. See the NOTICE file distributed with
|
12
|
+
# this work for additional information regarding copyright
|
13
|
+
# ownership. Uken Inc. licenses this file to you under
|
14
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
15
|
+
# not use this file except in compliance with the License.
|
16
|
+
# You may obtain a copy of the License at
|
17
|
+
#
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
19
|
+
#
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
21
|
+
# software distributed under the License is distributed on an
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
23
|
+
# KIND, either express or implied. See the License for the
|
24
|
+
# specific language governing permissions and limitations
|
25
|
+
# under the License.
|
26
|
+
|
27
|
+
require 'opensearch'
|
28
|
+
|
29
|
+
require 'faraday/excon'
|
30
|
+
require 'fluent/log-ext'
|
31
|
+
require 'fluent/plugin/input'
|
32
|
+
require 'fluent/plugin_helper'
|
33
|
+
require_relative 'opensearch_constants'
|
34
|
+
|
35
|
+
module Fluent::Plugin
|
36
|
+
class OpenSearchInput < Input
|
37
|
+
class UnrecoverableRequestFailure < Fluent::UnrecoverableError; end
|
38
|
+
|
39
|
+
DEFAULT_RELOAD_AFTER = -1
|
40
|
+
DEFAULT_STORAGE_TYPE = 'local'
|
41
|
+
METADATA = "@metadata".freeze
|
42
|
+
|
43
|
+
helpers :timer, :thread, :retry_state
|
44
|
+
|
45
|
+
Fluent::Plugin.register_input('opensearch', self)
|
46
|
+
|
47
|
+
config_param :tag, :string
|
48
|
+
config_param :host, :string, :default => 'localhost'
|
49
|
+
config_param :port, :integer, :default => 9200
|
50
|
+
config_param :user, :string, :default => nil
|
51
|
+
config_param :password, :string, :default => nil, :secret => true
|
52
|
+
config_param :path, :string, :default => nil
|
53
|
+
config_param :scheme, :enum, :list => [:https, :http], :default => :http
|
54
|
+
config_param :hosts, :string, :default => nil
|
55
|
+
config_param :index_name, :string, :default => "fluentd"
|
56
|
+
config_param :parse_timestamp, :bool, :default => false
|
57
|
+
config_param :timestamp_key_format, :string, :default => nil
|
58
|
+
config_param :timestamp_parse_error_tag, :string, :default => 'opensearch_plugin.input.time.error'
|
59
|
+
config_param :query, :hash, :default => {"sort" => [ "_doc" ]}
|
60
|
+
config_param :scroll, :string, :default => "1m"
|
61
|
+
config_param :size, :integer, :default => 1000
|
62
|
+
config_param :num_slices, :integer, :default => 1
|
63
|
+
config_param :interval, :size, :default => 5
|
64
|
+
config_param :repeat, :bool, :default => true
|
65
|
+
config_param :http_backend, :enum, list: [:excon, :typhoeus], :default => :excon
|
66
|
+
config_param :request_timeout, :time, :default => 5
|
67
|
+
config_param :reload_connections, :bool, :default => false
|
68
|
+
config_param :reload_on_failure, :bool, :default => false
|
69
|
+
config_param :resurrect_after, :time, :default => 60
|
70
|
+
config_param :reload_after, :integer, :default => DEFAULT_RELOAD_AFTER
|
71
|
+
config_param :ssl_verify , :bool, :default => true
|
72
|
+
config_param :client_key, :string, :default => nil
|
73
|
+
config_param :client_cert, :string, :default => nil
|
74
|
+
config_param :client_key_pass, :string, :default => nil, :secret => true
|
75
|
+
config_param :ca_file, :string, :default => nil
|
76
|
+
config_param :ssl_version, :enum, list: [:SSLv23, :TLSv1, :TLSv1_1, :TLSv1_2], :default => :TLSv1_2
|
77
|
+
config_param :with_transporter_log, :bool, :default => false
|
78
|
+
config_param :emit_error_label_event, :bool, :default => true
|
79
|
+
config_param :sniffer_class_name, :string, :default => nil
|
80
|
+
config_param :custom_headers, :hash, :default => {}
|
81
|
+
config_param :docinfo_fields, :array, :default => ['_index', '_type', '_id']
|
82
|
+
config_param :docinfo_target, :string, :default => METADATA
|
83
|
+
config_param :docinfo, :bool, :default => false
|
84
|
+
config_param :check_connection, :bool, :default => true
|
85
|
+
config_param :retry_forever, :bool, default: true, desc: 'If true, plugin will ignore retry_timeout and retry_max_times options and retry forever.'
|
86
|
+
config_param :retry_timeout, :time, default: 72 * 60 * 60, desc: 'The maximum seconds to retry'
|
87
|
+
# 72hours == 17 times with exponential backoff (not to change default behavior)
|
88
|
+
config_param :retry_max_times, :integer, default: 5, desc: 'The maximum number of times to retry'
|
89
|
+
# exponential backoff sequence will be initialized at the time of this threshold
|
90
|
+
config_param :retry_type, :enum, list: [:exponential_backoff, :periodic], default: :exponential_backoff
|
91
|
+
### Periodic -> fixed :retry_wait
|
92
|
+
### Exponential backoff: k is number of retry times
|
93
|
+
# c: constant factor, @retry_wait
|
94
|
+
# b: base factor, @retry_exponential_backoff_base
|
95
|
+
# k: times
|
96
|
+
# total retry time: c + c * b^1 + (...) + c*b^k = c*b^(k+1) - 1
|
97
|
+
config_param :retry_wait, :time, default: 5, desc: 'Seconds to wait before next retry , or constant factor of exponential backoff.'
|
98
|
+
config_param :retry_exponential_backoff_base, :float, default: 2, desc: 'The base number of exponential backoff for retries.'
|
99
|
+
config_param :retry_max_interval, :time, default: nil, desc: 'The maximum interval seconds for exponential backoff between retries while failing.'
|
100
|
+
config_param :retry_randomize, :bool, default: false, desc: 'If true, output plugin will retry after randomized interval not to do burst retries.'
|
101
|
+
|
102
|
+
include Fluent::Plugin::OpenSearchConstants
|
103
|
+
|
104
|
+
def initialize
|
105
|
+
super
|
106
|
+
end
|
107
|
+
|
108
|
+
def configure(conf)
|
109
|
+
super
|
110
|
+
|
111
|
+
@timestamp_parser = create_time_parser
|
112
|
+
@backend_options = backend_options
|
113
|
+
@retry = nil
|
114
|
+
|
115
|
+
raise Fluent::ConfigError, "`password` must be present if `user` is present" if @user && @password.nil?
|
116
|
+
|
117
|
+
if @user && m = @user.match(/%{(?<user>.*)}/)
|
118
|
+
@user = URI.encode_www_form_component(m["user"])
|
119
|
+
end
|
120
|
+
if @password && m = @password.match(/%{(?<password>.*)}/)
|
121
|
+
@password = URI.encode_www_form_component(m["password"])
|
122
|
+
end
|
123
|
+
|
124
|
+
@transport_logger = nil
|
125
|
+
if @with_transporter_log
|
126
|
+
@transport_logger = log
|
127
|
+
log_level = conf['@log_level'] || conf['log_level']
|
128
|
+
log.warn "Consider to specify log_level with @log_level." unless log_level
|
129
|
+
end
|
130
|
+
@current_config = nil
|
131
|
+
# Specify @sniffer_class before calling #client.
|
132
|
+
@sniffer_class = nil
|
133
|
+
begin
|
134
|
+
@sniffer_class = Object.const_get(@sniffer_class_name) if @sniffer_class_name
|
135
|
+
rescue Exception => ex
|
136
|
+
raise Fluent::ConfigError, "Could not load sniffer class #{@sniffer_class_name}: #{ex}"
|
137
|
+
end
|
138
|
+
|
139
|
+
@options = {
|
140
|
+
:index => @index_name,
|
141
|
+
:scroll => @scroll,
|
142
|
+
:size => @size
|
143
|
+
}
|
144
|
+
@base_query = @query
|
145
|
+
end
|
146
|
+
|
147
|
+
def backend_options
|
148
|
+
case @http_backend
|
149
|
+
when :excon
|
150
|
+
{ client_key: @client_key, client_cert: @client_cert, client_key_pass: @client_key_pass }
|
151
|
+
when :typhoeus
|
152
|
+
require 'typhoeus'
|
153
|
+
{ sslkey: @client_key, sslcert: @client_cert, keypasswd: @client_key_pass }
|
154
|
+
end
|
155
|
+
rescue LoadError => ex
|
156
|
+
log.error_backtrace(ex.backtrace)
|
157
|
+
raise Fluent::ConfigError, "You must install #{@http_backend} gem. Exception: #{ex}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def retry_state(randomize)
|
161
|
+
retry_state_create(
|
162
|
+
:input_retries, @retry_type, @retry_wait, @retry_timeout,
|
163
|
+
forever: @retry_forever, max_steps: @retry_max_times,
|
164
|
+
max_interval: @retry_max_interval, backoff_base: @retry_exponential_backoff_base,
|
165
|
+
randomize: randomize
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
def get_escaped_userinfo(host_str)
|
170
|
+
if m = host_str.match(/(?<scheme>.*)%{(?<user>.*)}:%{(?<password>.*)}(?<path>@.*)/)
|
171
|
+
m["scheme"] +
|
172
|
+
URI.encode_www_form_component(m["user"]) +
|
173
|
+
':' +
|
174
|
+
URI.encode_www_form_component(m["password"]) +
|
175
|
+
m["path"]
|
176
|
+
else
|
177
|
+
host_str
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def get_connection_options(con_host=nil)
|
182
|
+
|
183
|
+
hosts = if con_host || @hosts
|
184
|
+
(con_host || @hosts).split(',').map do |host_str|
|
185
|
+
# Support legacy hosts format host:port,host:port,host:port...
|
186
|
+
if host_str.match(%r{^[^:]+(\:\d+)?$})
|
187
|
+
{
|
188
|
+
host: host_str.split(':')[0],
|
189
|
+
port: (host_str.split(':')[1] || @port).to_i,
|
190
|
+
scheme: @scheme.to_s
|
191
|
+
}
|
192
|
+
else
|
193
|
+
# New hosts format expects URLs such as http://logs.foo.com,https://john:pass@logs2.foo.com/elastic
|
194
|
+
uri = URI(get_escaped_userinfo(host_str))
|
195
|
+
%w(user password path).inject(host: uri.host, port: uri.port, scheme: uri.scheme) do |hash, key|
|
196
|
+
hash[key.to_sym] = uri.public_send(key) unless uri.public_send(key).nil? || uri.public_send(key) == ''
|
197
|
+
hash
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end.compact
|
201
|
+
else
|
202
|
+
[{host: @host, port: @port, scheme: @scheme.to_s}]
|
203
|
+
end.each do |host|
|
204
|
+
host.merge!(user: @user, password: @password) if !host[:user] && @user
|
205
|
+
host.merge!(path: @path) if !host[:path] && @path
|
206
|
+
end
|
207
|
+
live_hosts = @check_connection ? hosts.select { |host| reachable_host?(host) } : hosts
|
208
|
+
{
|
209
|
+
hosts: live_hosts
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
def reachable_host?(host)
|
214
|
+
client = OpenSearch::Client.new(
|
215
|
+
host: ["#{host[:scheme]}://#{host[:host]}:#{host[:port]}"],
|
216
|
+
user: host[:user],
|
217
|
+
password: host[:password],
|
218
|
+
# reload_connections: @reload_connections,
|
219
|
+
request_timeout: @request_timeout,
|
220
|
+
resurrect_after: @resurrect_after,
|
221
|
+
reload_on_failure: @reload_on_failure,
|
222
|
+
transport_options: { ssl: { verify: @ssl_verify, ca_file: @ca_file, version: @ssl_version } }
|
223
|
+
)
|
224
|
+
client.ping
|
225
|
+
rescue => e
|
226
|
+
log.warn "Failed to connect to #{host[:scheme]}://#{host[:host]}:#{host[:port]}: #{e.message}"
|
227
|
+
false
|
228
|
+
end
|
229
|
+
|
230
|
+
def emit_error_label_event(&block)
|
231
|
+
# If `emit_error_label_event` is specified as false, error event emittions are not occurred.
|
232
|
+
if emit_error_label_event
|
233
|
+
block.call
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def start
|
238
|
+
super
|
239
|
+
|
240
|
+
timer_execute(:in_opensearch_timer, @interval, repeat: @repeat, &method(:run))
|
241
|
+
end
|
242
|
+
|
243
|
+
# We might be able to use
|
244
|
+
# Fluent::Parser::TimeParser, but it doesn't quite do what we want - if gives
|
245
|
+
# [sec,nsec] where as we want something we can call `strftime` on...
|
246
|
+
def create_time_parser
|
247
|
+
if @timestamp_key_format
|
248
|
+
begin
|
249
|
+
# Strptime doesn't support all formats, but for those it does it's
|
250
|
+
# blazingly fast.
|
251
|
+
strptime = Strptime.new(@timestamp_key_format)
|
252
|
+
Proc.new { |value|
|
253
|
+
value = convert_numeric_time_into_string(value, @timestamp_key_format) if value.is_a?(Numeric)
|
254
|
+
strptime.exec(value).to_time
|
255
|
+
}
|
256
|
+
rescue
|
257
|
+
# Can happen if Strptime doesn't recognize the format; or
|
258
|
+
# if strptime couldn't be required (because it's not installed -- it's
|
259
|
+
# ruby 2 only)
|
260
|
+
Proc.new { |value|
|
261
|
+
value = convert_numeric_time_into_string(value, @timestamp_key_format) if value.is_a?(Numeric)
|
262
|
+
DateTime.strptime(value, @timestamp_key_format).to_time
|
263
|
+
}
|
264
|
+
end
|
265
|
+
else
|
266
|
+
Proc.new { |value|
|
267
|
+
value = convert_numeric_time_into_string(value) if value.is_a?(Numeric)
|
268
|
+
DateTime.parse(value).to_time
|
269
|
+
}
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def convert_numeric_time_into_string(numeric_time, timestamp_key_format = "%Y-%m-%dT%H:%M:%S.%N%z")
|
274
|
+
numeric_time_parser = Fluent::NumericTimeParser.new(:float)
|
275
|
+
Time.at(numeric_time_parser.parse(numeric_time).to_r).strftime(timestamp_key_format)
|
276
|
+
end
|
277
|
+
|
278
|
+
def parse_time(value, event_time, tag)
|
279
|
+
@timestamp_parser.call(value)
|
280
|
+
rescue => e
|
281
|
+
emit_error_label_event do
|
282
|
+
router.emit_error_event(@timestamp_parse_error_tag, Fluent::Engine.now, {'tag' => tag, 'time' => event_time, 'format' => @timestamp_key_format, 'value' => value}, e)
|
283
|
+
end
|
284
|
+
return Time.at(event_time).to_time
|
285
|
+
end
|
286
|
+
|
287
|
+
def client(host = nil)
|
288
|
+
# check here to see if we already have a client connection for the given host
|
289
|
+
connection_options = get_connection_options(host)
|
290
|
+
|
291
|
+
@_os = nil unless is_existing_connection(connection_options[:hosts])
|
292
|
+
|
293
|
+
@_os ||= begin
|
294
|
+
@current_config = connection_options[:hosts].clone
|
295
|
+
adapter_conf = lambda {|f| f.adapter @http_backend, @backend_options }
|
296
|
+
local_reload_connections = @reload_connections
|
297
|
+
if local_reload_connections && @reload_after > DEFAULT_RELOAD_AFTER
|
298
|
+
local_reload_connections = @reload_after
|
299
|
+
end
|
300
|
+
|
301
|
+
headers = { 'Content-Type' => "application/json" }.merge(@custom_headers)
|
302
|
+
|
303
|
+
transport = OpenSearch::Transport::Transport::HTTP::Faraday.new(
|
304
|
+
connection_options.merge(
|
305
|
+
options: {
|
306
|
+
reload_connections: local_reload_connections,
|
307
|
+
reload_on_failure: @reload_on_failure,
|
308
|
+
resurrect_after: @resurrect_after,
|
309
|
+
logger: @transport_logger,
|
310
|
+
transport_options: {
|
311
|
+
headers: headers,
|
312
|
+
request: { timeout: @request_timeout },
|
313
|
+
ssl: { verify: @ssl_verify, ca_file: @ca_file, version: @ssl_version }
|
314
|
+
},
|
315
|
+
http: {
|
316
|
+
user: @user,
|
317
|
+
password: @password
|
318
|
+
},
|
319
|
+
sniffer_class: @sniffer_class,
|
320
|
+
}), &adapter_conf)
|
321
|
+
OpenSearch::Client.new transport: transport
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def is_existing_connection(host)
|
326
|
+
# check if the host provided match the current connection
|
327
|
+
return false if @_os.nil?
|
328
|
+
return false if @current_config.nil?
|
329
|
+
return false if host.length != @current_config.length
|
330
|
+
|
331
|
+
for i in 0...host.length
|
332
|
+
if !host[i][:host].eql? @current_config[i][:host] || host[i][:port] != @current_config[i][:port]
|
333
|
+
return false
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
return true
|
338
|
+
end
|
339
|
+
|
340
|
+
def update_retry_state(error=nil)
|
341
|
+
if error
|
342
|
+
unless @retry
|
343
|
+
@retry = retry_state(@retry_randomize)
|
344
|
+
end
|
345
|
+
@retry.step
|
346
|
+
if error.message.include?('EOFError (EOFError)')
|
347
|
+
log.error("Restart plugin because hit error #{error.message}")
|
348
|
+
start
|
349
|
+
end
|
350
|
+
#Raise error if the retry limit has been reached
|
351
|
+
raise "Hit limit for retries. retry_times: #{@retry.steps}, error: #{error.message}" if @retry.limit?
|
352
|
+
#Retry if the limit hasn't been reached
|
353
|
+
log.warn("failed to connect or search.", retry_times: @retry.steps, next_retry_time: @retry.next_time.round, error: error.message)
|
354
|
+
sleep(@retry.next_time - Time.now)
|
355
|
+
else
|
356
|
+
unless @retry.nil?
|
357
|
+
log.info("retry succeeded.")
|
358
|
+
@retry = nil
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def run
|
364
|
+
return run_slice if @num_slices <= 1
|
365
|
+
|
366
|
+
log.warn("Large slice number is specified:(#{@num_slices}). Consider reducing num_slices") if @num_slices > 8
|
367
|
+
|
368
|
+
@num_slices.times.map do |slice_id|
|
369
|
+
thread_create(:"in_opensearch_thread_#{slice_id}") do
|
370
|
+
run_slice(slice_id)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
rescue => error
|
374
|
+
update_retry_state(error)
|
375
|
+
retry
|
376
|
+
end
|
377
|
+
|
378
|
+
def run_slice(slice_id=nil)
|
379
|
+
slice_query = @base_query
|
380
|
+
slice_query = slice_query.merge('slice' => { 'id' => slice_id, 'max' => @num_slices}) unless slice_id.nil?
|
381
|
+
result = client.search(@options.merge(:body => Yajl.dump(slice_query) ))
|
382
|
+
es = Fluent::MultiEventStream.new
|
383
|
+
|
384
|
+
result["hits"]["hits"].each {|hit| process_events(hit, es)}
|
385
|
+
has_hits = result['hits']['hits'].any?
|
386
|
+
scroll_id = result['_scroll_id']
|
387
|
+
|
388
|
+
while has_hits && scroll_id
|
389
|
+
result = process_next_scroll_request(es, scroll_id)
|
390
|
+
has_hits = result['has_hits']
|
391
|
+
scroll_id = result['_scroll_id']
|
392
|
+
end
|
393
|
+
|
394
|
+
router.emit_stream(@tag, es)
|
395
|
+
clear_scroll(scroll_id)
|
396
|
+
update_retry_state
|
397
|
+
end
|
398
|
+
|
399
|
+
def clear_scroll(scroll_id)
|
400
|
+
client.clear_scroll(scroll_id: scroll_id) if scroll_id
|
401
|
+
rescue => e
|
402
|
+
# ignore & log any clear_scroll errors
|
403
|
+
log.warn("Ignoring clear_scroll exception", message: e.message, exception: e.class)
|
404
|
+
end
|
405
|
+
|
406
|
+
def process_scroll_request(scroll_id)
|
407
|
+
client.scroll(:body => { :scroll_id => scroll_id }, :scroll => @scroll)
|
408
|
+
end
|
409
|
+
|
410
|
+
def process_next_scroll_request(es, scroll_id)
|
411
|
+
result = process_scroll_request(scroll_id)
|
412
|
+
result['hits']['hits'].each { |hit| process_events(hit, es) }
|
413
|
+
{'has_hits' => result['hits']['hits'].any?, '_scroll_id' => result['_scroll_id']}
|
414
|
+
end
|
415
|
+
|
416
|
+
def process_events(hit, es)
|
417
|
+
event = hit["_source"]
|
418
|
+
time = Fluent::Engine.now
|
419
|
+
if @parse_timestamp
|
420
|
+
if event.has_key?(TIMESTAMP_FIELD)
|
421
|
+
rts = event[TIMESTAMP_FIELD]
|
422
|
+
time = parse_time(rts, time, @tag)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
if @docinfo
|
426
|
+
docinfo_target = event[@docinfo_target] || {}
|
427
|
+
|
428
|
+
unless docinfo_target.is_a?(Hash)
|
429
|
+
raise UnrecoverableError, "incompatible type for the docinfo_target=#{@docinfo_target} field in the `_source` document, expected a hash got:", :type => docinfo_target.class, :event => event
|
430
|
+
end
|
431
|
+
|
432
|
+
@docinfo_fields.each do |field|
|
433
|
+
docinfo_target[field] = hit[field]
|
434
|
+
end
|
435
|
+
|
436
|
+
event[@docinfo_target] = docinfo_target
|
437
|
+
end
|
438
|
+
es.add(time, event)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# The fluent-plugin-opensearch Contributors require contributions made to
|
4
|
+
# this file be licensed under the Apache-2.0 license or a
|
5
|
+
# compatible open source license.
|
6
|
+
#
|
7
|
+
# Modifications Copyright fluent-plugin-opensearch Contributors. See
|
8
|
+
# GitHub history for details.
|
9
|
+
#
|
10
|
+
# Licensed to Uken Inc. under one or more contributor
|
11
|
+
# license agreements. See the NOTICE file distributed with
|
12
|
+
# this work for additional information regarding copyright
|
13
|
+
# ownership. Uken Inc. licenses this file to you under
|
14
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
15
|
+
# not use this file except in compliance with the License.
|
16
|
+
# You may obtain a copy of the License at
|
17
|
+
#
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
19
|
+
#
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
21
|
+
# software distributed under the License is distributed on an
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
23
|
+
# KIND, either express or implied. See the License for the
|
24
|
+
# specific language governing permissions and limitations
|
25
|
+
# under the License.
|
26
|
+
|
27
|
+
require 'oj'
|
28
|
+
|
29
|
+
module Fluent::Plugin
|
30
|
+
module Serializer
|
31
|
+
|
32
|
+
class Oj
|
33
|
+
include OpenSearch::Transport::Transport::Serializer::Base
|
34
|
+
|
35
|
+
# De-serialize a Hash from JSON string
|
36
|
+
#
|
37
|
+
def load(string, options={})
|
38
|
+
::Oj.load(string, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Serialize a Hash to JSON string
|
42
|
+
#
|
43
|
+
def dump(object, options={})
|
44
|
+
::Oj.dump(object, options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
#
|
3
|
+
# The fluent-plugin-opensearch Contributors require contributions made to
|
4
|
+
# this file be licensed under the Apache-2.0 license or a
|
5
|
+
# compatible open source license.
|
6
|
+
#
|
7
|
+
# Modifications Copyright fluent-plugin-opensearch Contributors. See
|
8
|
+
# GitHub history for details.
|
9
|
+
#
|
10
|
+
# Licensed to Uken Inc. under one or more contributor
|
11
|
+
# license agreements. See the NOTICE file distributed with
|
12
|
+
# this work for additional information regarding copyright
|
13
|
+
# ownership. Uken Inc. licenses this file to you under
|
14
|
+
# the Apache License, Version 2.0 (the "License"); you may
|
15
|
+
# not use this file except in compliance with the License.
|
16
|
+
# You may obtain a copy of the License at
|
17
|
+
#
|
18
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
19
|
+
#
|
20
|
+
# Unless required by applicable law or agreed to in writing,
|
21
|
+
# software distributed under the License is distributed on an
|
22
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
23
|
+
# KIND, either express or implied. See the License for the
|
24
|
+
# specific language governing permissions and limitations
|
25
|
+
# under the License.
|
26
|
+
|
27
|
+
module Fluent
|
28
|
+
module Plugin
|
29
|
+
module OpenSearchConstants
|
30
|
+
BODY_DELIMITER = "\n".freeze
|
31
|
+
UPDATE_OP = "update".freeze
|
32
|
+
UPSERT_OP = "upsert".freeze
|
33
|
+
CREATE_OP = "create".freeze
|
34
|
+
INDEX_OP = "index".freeze
|
35
|
+
ID_FIELD = "_id".freeze
|
36
|
+
TIMESTAMP_FIELD = "@timestamp".freeze
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|