logstash-output-elasticsearch-test 11.16.0-x86_64-linux
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/CHANGELOG.md +649 -0
- data/CONTRIBUTORS +34 -0
- data/Gemfile +16 -0
- data/LICENSE +202 -0
- data/NOTICE.TXT +5 -0
- data/README.md +106 -0
- data/docs/index.asciidoc +1369 -0
- data/lib/logstash/outputs/elasticsearch/data_stream_support.rb +282 -0
- data/lib/logstash/outputs/elasticsearch/default-ilm-policy.json +14 -0
- data/lib/logstash/outputs/elasticsearch/http_client/manticore_adapter.rb +155 -0
- data/lib/logstash/outputs/elasticsearch/http_client/pool.rb +534 -0
- data/lib/logstash/outputs/elasticsearch/http_client.rb +497 -0
- data/lib/logstash/outputs/elasticsearch/http_client_builder.rb +201 -0
- data/lib/logstash/outputs/elasticsearch/ilm.rb +92 -0
- data/lib/logstash/outputs/elasticsearch/license_checker.rb +52 -0
- data/lib/logstash/outputs/elasticsearch/template_manager.rb +131 -0
- data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-6x.json +45 -0
- data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-7x.json +44 -0
- data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-8x.json +50 -0
- data/lib/logstash/outputs/elasticsearch.rb +699 -0
- data/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb +237 -0
- data/lib/logstash/plugin_mixins/elasticsearch/common.rb +409 -0
- data/lib/logstash/plugin_mixins/elasticsearch/noop_license_checker.rb +9 -0
- data/logstash-output-elasticsearch.gemspec +40 -0
- data/spec/es_spec_helper.rb +225 -0
- data/spec/fixtures/_nodes/6x.json +81 -0
- data/spec/fixtures/_nodes/7x.json +92 -0
- data/spec/fixtures/htpasswd +2 -0
- data/spec/fixtures/license_check/active.json +16 -0
- data/spec/fixtures/license_check/inactive.json +5 -0
- data/spec/fixtures/nginx_reverse_proxy.conf +22 -0
- data/spec/fixtures/scripts/painless/scripted_update.painless +2 -0
- data/spec/fixtures/scripts/painless/scripted_update_nested.painless +1 -0
- data/spec/fixtures/scripts/painless/scripted_upsert.painless +1 -0
- data/spec/fixtures/template-with-policy-es6x.json +48 -0
- data/spec/fixtures/template-with-policy-es7x.json +45 -0
- data/spec/fixtures/template-with-policy-es8x.json +50 -0
- data/spec/fixtures/test_certs/ca.crt +29 -0
- data/spec/fixtures/test_certs/ca.der.sha256 +1 -0
- data/spec/fixtures/test_certs/ca.key +51 -0
- data/spec/fixtures/test_certs/renew.sh +13 -0
- data/spec/fixtures/test_certs/test.crt +30 -0
- data/spec/fixtures/test_certs/test.der.sha256 +1 -0
- data/spec/fixtures/test_certs/test.key +51 -0
- data/spec/fixtures/test_certs/test.p12 +0 -0
- data/spec/fixtures/test_certs/test_invalid.crt +36 -0
- data/spec/fixtures/test_certs/test_invalid.key +51 -0
- data/spec/fixtures/test_certs/test_invalid.p12 +0 -0
- data/spec/fixtures/test_certs/test_self_signed.crt +32 -0
- data/spec/fixtures/test_certs/test_self_signed.key +54 -0
- data/spec/fixtures/test_certs/test_self_signed.p12 +0 -0
- data/spec/integration/outputs/compressed_indexing_spec.rb +70 -0
- data/spec/integration/outputs/create_spec.rb +67 -0
- data/spec/integration/outputs/data_stream_spec.rb +68 -0
- data/spec/integration/outputs/delete_spec.rb +63 -0
- data/spec/integration/outputs/ilm_spec.rb +534 -0
- data/spec/integration/outputs/index_spec.rb +421 -0
- data/spec/integration/outputs/index_version_spec.rb +98 -0
- data/spec/integration/outputs/ingest_pipeline_spec.rb +75 -0
- data/spec/integration/outputs/metrics_spec.rb +66 -0
- data/spec/integration/outputs/no_es_on_startup_spec.rb +78 -0
- data/spec/integration/outputs/painless_update_spec.rb +99 -0
- data/spec/integration/outputs/parent_spec.rb +94 -0
- data/spec/integration/outputs/retry_spec.rb +182 -0
- data/spec/integration/outputs/routing_spec.rb +61 -0
- data/spec/integration/outputs/sniffer_spec.rb +94 -0
- data/spec/integration/outputs/templates_spec.rb +133 -0
- data/spec/integration/outputs/unsupported_actions_spec.rb +75 -0
- data/spec/integration/outputs/update_spec.rb +114 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/elasticsearch/api/actions/delete_ilm_policy.rb +19 -0
- data/spec/support/elasticsearch/api/actions/get_alias.rb +18 -0
- data/spec/support/elasticsearch/api/actions/get_ilm_policy.rb +18 -0
- data/spec/support/elasticsearch/api/actions/put_alias.rb +24 -0
- data/spec/support/elasticsearch/api/actions/put_ilm_policy.rb +25 -0
- data/spec/unit/http_client_builder_spec.rb +185 -0
- data/spec/unit/outputs/elasticsearch/data_stream_support_spec.rb +612 -0
- data/spec/unit/outputs/elasticsearch/http_client/manticore_adapter_spec.rb +151 -0
- data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +501 -0
- data/spec/unit/outputs/elasticsearch/http_client_spec.rb +339 -0
- data/spec/unit/outputs/elasticsearch/template_manager_spec.rb +189 -0
- data/spec/unit/outputs/elasticsearch_proxy_spec.rb +103 -0
- data/spec/unit/outputs/elasticsearch_spec.rb +1573 -0
- data/spec/unit/outputs/elasticsearch_ssl_spec.rb +197 -0
- data/spec/unit/outputs/error_whitelist_spec.rb +56 -0
- data/spec/unit/outputs/license_check_spec.rb +57 -0
- metadata +423 -0
@@ -0,0 +1,497 @@
|
|
1
|
+
require "logstash/outputs/elasticsearch"
|
2
|
+
require 'logstash/outputs/elasticsearch/http_client/pool'
|
3
|
+
require 'logstash/outputs/elasticsearch/http_client/manticore_adapter'
|
4
|
+
require 'cgi'
|
5
|
+
require 'zlib'
|
6
|
+
require 'stringio'
|
7
|
+
require 'java'
|
8
|
+
|
9
|
+
module LogStash; module Outputs; class ElasticSearch;
|
10
|
+
# This is a constant instead of a config option because
|
11
|
+
# there really isn't a good reason to configure it.
|
12
|
+
#
|
13
|
+
# The criteria used are:
|
14
|
+
# 1. We need a number that's less than 100MiB because ES
|
15
|
+
# won't accept bulks larger than that.
|
16
|
+
# 2. It must be large enough to amortize the connection constant
|
17
|
+
# across multiple requests.
|
18
|
+
# 3. It must be small enough that even if multiple threads hit this size
|
19
|
+
# we won't use a lot of heap.
|
20
|
+
#
|
21
|
+
# We wound up agreeing that a number greater than 10 MiB and less than 100MiB
|
22
|
+
# made sense. We picked one on the lowish side to not use too much heap.
|
23
|
+
TARGET_BULK_BYTES = 20 * 1024 * 1024 # 20MiB
|
24
|
+
|
25
|
+
class HttpClient
|
26
|
+
attr_reader :client, :options, :logger, :pool, :action_count, :recv_count
|
27
|
+
# This is here in case we use DEFAULT_OPTIONS in the future
|
28
|
+
# DEFAULT_OPTIONS = {
|
29
|
+
# :setting => value
|
30
|
+
# }
|
31
|
+
|
32
|
+
#
|
33
|
+
# The `options` is a hash where the following symbol keys have meaning:
|
34
|
+
#
|
35
|
+
# * `:hosts` - array of String. Set a list of hosts to use for communication.
|
36
|
+
# * `:port` - number. set the port to use to communicate with Elasticsearch
|
37
|
+
# * `:user` - String. The user to use for authentication.
|
38
|
+
# * `:password` - String. The password to use for authentication.
|
39
|
+
# * `:timeout` - Float. A duration value, in seconds, after which a socket
|
40
|
+
# operation or request will be aborted if not yet successfull
|
41
|
+
# * `:client_settings` - a hash; see below for keys.
|
42
|
+
#
|
43
|
+
# The `client_settings` key is a has that can contain other settings:
|
44
|
+
#
|
45
|
+
# * `:ssl` - Boolean. Enable or disable SSL/TLS.
|
46
|
+
# * `:proxy` - String. Choose a HTTP HTTProxy to use.
|
47
|
+
# * `:path` - String. The leading path for prefixing Elasticsearch
|
48
|
+
# * `:headers` - Hash. Pairs of headers and their values
|
49
|
+
# requests. This is sometimes used if you are proxying Elasticsearch access
|
50
|
+
# through a special http path, such as using mod_rewrite.
|
51
|
+
def initialize(options={})
|
52
|
+
@logger = options[:logger]
|
53
|
+
@metric = options[:metric]
|
54
|
+
@bulk_request_metrics = @metric.namespace(:bulk_requests)
|
55
|
+
@bulk_response_metrics = @bulk_request_metrics.namespace(:responses)
|
56
|
+
|
57
|
+
# Again, in case we use DEFAULT_OPTIONS in the future, uncomment this.
|
58
|
+
# @options = DEFAULT_OPTIONS.merge(options)
|
59
|
+
@options = options
|
60
|
+
|
61
|
+
@url_template = build_url_template
|
62
|
+
|
63
|
+
@pool = build_pool(@options)
|
64
|
+
# mutex to prevent requests and sniffing to access the
|
65
|
+
# connection pool at the same time
|
66
|
+
@bulk_path = @options[:bulk_path]
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_url_template
|
70
|
+
{
|
71
|
+
:scheme => self.scheme,
|
72
|
+
:user => self.user,
|
73
|
+
:password => self.password,
|
74
|
+
:host => "URLTEMPLATE",
|
75
|
+
:port => self.port,
|
76
|
+
:path => self.path
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def template_install(template_endpoint, name, template, force=false)
|
81
|
+
if template_exists?(template_endpoint, name) && !force
|
82
|
+
@logger.debug("Found existing Elasticsearch template, skipping template management", name: name)
|
83
|
+
return
|
84
|
+
end
|
85
|
+
template_put(template_endpoint, name, template)
|
86
|
+
end
|
87
|
+
|
88
|
+
def last_es_version
|
89
|
+
@pool.last_es_version
|
90
|
+
end
|
91
|
+
|
92
|
+
def maximum_seen_major_version
|
93
|
+
@pool.maximum_seen_major_version
|
94
|
+
end
|
95
|
+
|
96
|
+
def serverless?
|
97
|
+
@pool.serverless?
|
98
|
+
end
|
99
|
+
|
100
|
+
def alive_urls_count
|
101
|
+
@pool.alive_urls_count
|
102
|
+
end
|
103
|
+
|
104
|
+
def bulk(actions)
|
105
|
+
@action_count ||= 0
|
106
|
+
@action_count += actions.size
|
107
|
+
|
108
|
+
return if actions.empty?
|
109
|
+
|
110
|
+
bulk_actions = actions.collect do |action, args, source|
|
111
|
+
args, source = update_action_builder(args, source) if action == 'update'
|
112
|
+
|
113
|
+
if source && action != 'delete'
|
114
|
+
next [ { action => args }, source ]
|
115
|
+
else
|
116
|
+
next { action => args }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
body_stream = StringIO.new
|
121
|
+
if http_compression
|
122
|
+
body_stream.set_encoding "BINARY"
|
123
|
+
stream_writer = gzip_writer(body_stream)
|
124
|
+
else
|
125
|
+
stream_writer = body_stream
|
126
|
+
end
|
127
|
+
|
128
|
+
bulk_responses = []
|
129
|
+
batch_actions = []
|
130
|
+
bulk_actions.each_with_index do |action, index|
|
131
|
+
as_json = action.is_a?(Array) ?
|
132
|
+
action.map {|line| LogStash::Json.dump(line)}.join("\n") :
|
133
|
+
LogStash::Json.dump(action)
|
134
|
+
as_json << "\n"
|
135
|
+
if (stream_writer.pos + as_json.bytesize) > TARGET_BULK_BYTES && stream_writer.pos > 0
|
136
|
+
stream_writer.flush # ensure writer has sync'd buffers before reporting sizes
|
137
|
+
logger.debug("Sending partial bulk request for batch with one or more actions remaining.",
|
138
|
+
:action_count => batch_actions.size,
|
139
|
+
:payload_size => stream_writer.pos,
|
140
|
+
:content_length => body_stream.size,
|
141
|
+
:batch_offset => (index + 1 - batch_actions.size))
|
142
|
+
bulk_responses << bulk_send(body_stream, batch_actions)
|
143
|
+
body_stream.truncate(0) && body_stream.seek(0)
|
144
|
+
stream_writer = gzip_writer(body_stream) if http_compression
|
145
|
+
batch_actions.clear
|
146
|
+
end
|
147
|
+
stream_writer.write(as_json)
|
148
|
+
batch_actions << action
|
149
|
+
end
|
150
|
+
|
151
|
+
stream_writer.close if http_compression
|
152
|
+
|
153
|
+
logger.debug("Sending final bulk request for batch.",
|
154
|
+
:action_count => batch_actions.size,
|
155
|
+
:payload_size => stream_writer.pos,
|
156
|
+
:content_length => body_stream.size,
|
157
|
+
:batch_offset => (actions.size - batch_actions.size))
|
158
|
+
bulk_responses << bulk_send(body_stream, batch_actions) if body_stream.size > 0
|
159
|
+
|
160
|
+
body_stream.close if !http_compression
|
161
|
+
join_bulk_responses(bulk_responses)
|
162
|
+
end
|
163
|
+
|
164
|
+
def gzip_writer(io)
|
165
|
+
fail(ArgumentError, "Cannot create gzip writer on IO with unread bytes") unless io.eof?
|
166
|
+
fail(ArgumentError, "Cannot create gzip writer on non-empty IO") unless io.pos == 0
|
167
|
+
|
168
|
+
Zlib::GzipWriter.new(io, Zlib::DEFAULT_COMPRESSION, Zlib::DEFAULT_STRATEGY)
|
169
|
+
end
|
170
|
+
|
171
|
+
def join_bulk_responses(bulk_responses)
|
172
|
+
{
|
173
|
+
"errors" => bulk_responses.any? {|r| r["errors"] == true},
|
174
|
+
"items" => bulk_responses.reduce([]) {|m,r| m.concat(r.fetch("items", []))}
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def bulk_send(body_stream, batch_actions)
|
179
|
+
params = http_compression ? {:headers => {"Content-Encoding" => "gzip"}} : {}
|
180
|
+
response = @pool.post(@bulk_path, params, body_stream.string)
|
181
|
+
|
182
|
+
@bulk_response_metrics.increment(response.code.to_s)
|
183
|
+
|
184
|
+
case response.code
|
185
|
+
when 200 # OK
|
186
|
+
LogStash::Json.load(response.body)
|
187
|
+
when 413 # Payload Too Large
|
188
|
+
logger.warn("Bulk request rejected: `413 Payload Too Large`", :action_count => batch_actions.size, :content_length => body_stream.size)
|
189
|
+
emulate_batch_error_response(batch_actions, response.code, 'payload_too_large')
|
190
|
+
else
|
191
|
+
url = ::LogStash::Util::SafeURI.new(response.final_url)
|
192
|
+
raise ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(
|
193
|
+
response.code, url, body_stream.to_s, response.body
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def emulate_batch_error_response(actions, http_code, reason)
|
199
|
+
{
|
200
|
+
"errors" => true,
|
201
|
+
"items" => actions.map do |action|
|
202
|
+
action = action.first if action.is_a?(Array)
|
203
|
+
request_action, request_parameters = action.first
|
204
|
+
{
|
205
|
+
request_action => {"status" => http_code, "error" => { "type" => reason }}
|
206
|
+
}
|
207
|
+
end
|
208
|
+
}
|
209
|
+
end
|
210
|
+
|
211
|
+
def get(path)
|
212
|
+
response = @pool.get(path, nil)
|
213
|
+
LogStash::Json.load(response.body)
|
214
|
+
end
|
215
|
+
|
216
|
+
def post(path, params = {}, body_string)
|
217
|
+
response = @pool.post(path, params, body_string)
|
218
|
+
LogStash::Json.load(response.body)
|
219
|
+
end
|
220
|
+
|
221
|
+
def close
|
222
|
+
@pool.close
|
223
|
+
end
|
224
|
+
|
225
|
+
def calculate_property(uris, property, default, sniff_check)
|
226
|
+
values = uris.map(&property).uniq
|
227
|
+
|
228
|
+
if sniff_check && values.size > 1
|
229
|
+
raise LogStash::ConfigurationError, "Cannot have multiple values for #{property} in hosts when sniffing is enabled!"
|
230
|
+
end
|
231
|
+
|
232
|
+
uri_value = values.first
|
233
|
+
|
234
|
+
default = nil if default.is_a?(String) && default.empty? # Blanks are as good as nil
|
235
|
+
uri_value = nil if uri_value.is_a?(String) && uri_value.empty?
|
236
|
+
|
237
|
+
if default && uri_value && (default != uri_value)
|
238
|
+
raise LogStash::ConfigurationError, "Explicit value for '#{property}' was declared, but it is different in one of the URLs given! Please make sure your URLs are inline with explicit values. The URLs have the property set to '#{uri_value}', but it was also set to '#{default}' explicitly"
|
239
|
+
end
|
240
|
+
|
241
|
+
uri_value || default
|
242
|
+
end
|
243
|
+
|
244
|
+
def sniffing
|
245
|
+
@options[:sniffing]
|
246
|
+
end
|
247
|
+
|
248
|
+
def user
|
249
|
+
calculate_property(uris, :user, @options[:user], sniffing)
|
250
|
+
end
|
251
|
+
|
252
|
+
def password
|
253
|
+
calculate_property(uris, :password, @options[:password], sniffing)
|
254
|
+
end
|
255
|
+
|
256
|
+
def path
|
257
|
+
calculated = calculate_property(uris, :path, client_settings[:path], sniffing)
|
258
|
+
calculated = "/#{calculated}" if calculated && !calculated.start_with?("/")
|
259
|
+
calculated
|
260
|
+
end
|
261
|
+
|
262
|
+
def scheme
|
263
|
+
explicit_scheme = if ssl_options && ssl_options.has_key?(:enabled)
|
264
|
+
ssl_options[:enabled] ? 'https' : 'http'
|
265
|
+
else
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
calculated_scheme = calculate_property(uris, :scheme, explicit_scheme, sniffing)
|
270
|
+
|
271
|
+
if calculated_scheme && calculated_scheme !~ /https?/
|
272
|
+
raise LogStash::ConfigurationError, "Bad scheme '#{calculated_scheme}' found should be one of http/https"
|
273
|
+
end
|
274
|
+
|
275
|
+
if calculated_scheme && explicit_scheme && calculated_scheme != explicit_scheme
|
276
|
+
raise LogStash::ConfigurationError, "SSL option was explicitly set to #{ssl_options[:enabled]} but a URL was also declared with a scheme of '#{explicit_scheme}'. Please reconcile this"
|
277
|
+
end
|
278
|
+
|
279
|
+
calculated_scheme # May be nil if explicit_scheme is nil!
|
280
|
+
end
|
281
|
+
|
282
|
+
def port
|
283
|
+
# We don't set the 'default' here because the default is what the user
|
284
|
+
# indicated, so we use an || outside of calculate_property. This lets people
|
285
|
+
# Enter things like foo:123, bar and wind up with foo:123, bar:9200
|
286
|
+
calculate_property(uris, :port, nil, sniffing) || 9200
|
287
|
+
end
|
288
|
+
|
289
|
+
def uris
|
290
|
+
@options[:hosts]
|
291
|
+
end
|
292
|
+
|
293
|
+
def client_settings
|
294
|
+
@_client_settings ||= @options[:client_settings] || {}
|
295
|
+
end
|
296
|
+
|
297
|
+
def ssl_options
|
298
|
+
@_ssl_options ||= client_settings.fetch(:ssl, {})
|
299
|
+
end
|
300
|
+
|
301
|
+
def http_compression
|
302
|
+
client_settings.fetch(:http_compression, false)
|
303
|
+
end
|
304
|
+
|
305
|
+
def build_adapter(options)
|
306
|
+
timeout = options[:timeout] || 0
|
307
|
+
|
308
|
+
adapter_options = {
|
309
|
+
:socket_timeout => timeout,
|
310
|
+
:request_timeout => timeout,
|
311
|
+
}
|
312
|
+
|
313
|
+
adapter_options[:user_agent] = prepare_user_agent
|
314
|
+
|
315
|
+
adapter_options[:proxy] = client_settings[:proxy] if client_settings[:proxy]
|
316
|
+
|
317
|
+
adapter_options[:check_connection_timeout] = client_settings[:check_connection_timeout] if client_settings[:check_connection_timeout]
|
318
|
+
|
319
|
+
# Having this explicitly set to nil is an error
|
320
|
+
if client_settings[:pool_max]
|
321
|
+
adapter_options[:pool_max] = client_settings[:pool_max]
|
322
|
+
end
|
323
|
+
|
324
|
+
# Having this explicitly set to nil is an error
|
325
|
+
if client_settings[:pool_max_per_route]
|
326
|
+
adapter_options[:pool_max_per_route] = client_settings[:pool_max_per_route]
|
327
|
+
end
|
328
|
+
|
329
|
+
adapter_options[:ssl] = ssl_options if self.scheme == 'https'
|
330
|
+
|
331
|
+
adapter_options[:headers] = client_settings[:headers] if client_settings[:headers]
|
332
|
+
|
333
|
+
::LogStash::Outputs::ElasticSearch::HttpClient::ManticoreAdapter.new(@logger, adapter_options)
|
334
|
+
end
|
335
|
+
|
336
|
+
def prepare_user_agent
|
337
|
+
os_name = java.lang.System.getProperty('os.name')
|
338
|
+
os_version = java.lang.System.getProperty('os.version')
|
339
|
+
os_arch = java.lang.System.getProperty('os.arch')
|
340
|
+
jvm_vendor = java.lang.System.getProperty('java.vendor')
|
341
|
+
jvm_version = java.lang.System.getProperty('java.version')
|
342
|
+
|
343
|
+
plugin_version = Gem.loaded_specs['logstash-output-elasticsearch'].version
|
344
|
+
# example: Logstash/7.14.1 (OS=Linux-5.4.0-84-generic-amd64; JVM=AdoptOpenJDK-11.0.11) logstash-output-elasticsearch/11.0.1
|
345
|
+
"Logstash/#{LOGSTASH_VERSION} (OS=#{os_name}-#{os_version}-#{os_arch}; JVM=#{jvm_vendor}-#{jvm_version}) logstash-output-elasticsearch/#{plugin_version}"
|
346
|
+
end
|
347
|
+
|
348
|
+
def build_pool(options)
|
349
|
+
adapter = build_adapter(options)
|
350
|
+
|
351
|
+
pool_options = {
|
352
|
+
:license_checker => options[:license_checker],
|
353
|
+
:sniffing => sniffing,
|
354
|
+
:sniffer_delay => options[:sniffer_delay],
|
355
|
+
:sniffing_path => options[:sniffing_path],
|
356
|
+
:healthcheck_path => options[:healthcheck_path],
|
357
|
+
:resurrect_delay => options[:resurrect_delay],
|
358
|
+
:url_normalizer => self.method(:host_to_url),
|
359
|
+
:metric => options[:metric]
|
360
|
+
}
|
361
|
+
pool_options[:scheme] = self.scheme if self.scheme
|
362
|
+
|
363
|
+
pool_class = ::LogStash::Outputs::ElasticSearch::HttpClient::Pool
|
364
|
+
full_urls = @options[:hosts].map {|h| host_to_url(h) }
|
365
|
+
pool = pool_class.new(@logger, adapter, full_urls, pool_options)
|
366
|
+
pool.start
|
367
|
+
pool
|
368
|
+
end
|
369
|
+
|
370
|
+
def host_to_url(h)
|
371
|
+
# Never override the calculated scheme
|
372
|
+
raw_scheme = @url_template[:scheme] || 'http'
|
373
|
+
|
374
|
+
raw_user = h.user || @url_template[:user]
|
375
|
+
raw_password = h.password || @url_template[:password]
|
376
|
+
postfixed_userinfo = raw_user && raw_password ? "#{raw_user}:#{raw_password}@" : nil
|
377
|
+
|
378
|
+
raw_host = h.host # Always replace this!
|
379
|
+
raw_port = h.port || @url_template[:port]
|
380
|
+
|
381
|
+
raw_path = !h.path.nil? && !h.path.empty? && h.path != "/" ? h.path : @url_template[:path]
|
382
|
+
prefixed_raw_path = raw_path && !raw_path.empty? ? raw_path : "/"
|
383
|
+
|
384
|
+
parameters = client_settings[:parameters]
|
385
|
+
raw_query = if parameters && !parameters.empty?
|
386
|
+
combined = h.query ?
|
387
|
+
Hash[URI::decode_www_form(h.query)].merge(parameters) :
|
388
|
+
parameters
|
389
|
+
query_str = combined.flat_map {|k,v|
|
390
|
+
values = Array(v)
|
391
|
+
values.map {|av| "#{k}=#{av}"}
|
392
|
+
}.join("&")
|
393
|
+
query_str
|
394
|
+
else
|
395
|
+
h.query
|
396
|
+
end
|
397
|
+
prefixed_raw_query = raw_query && !raw_query.empty? ? "?#{raw_query}" : nil
|
398
|
+
|
399
|
+
raw_url = "#{raw_scheme}://#{postfixed_userinfo}#{raw_host}:#{raw_port}#{prefixed_raw_path}#{prefixed_raw_query}"
|
400
|
+
|
401
|
+
::LogStash::Util::SafeURI.new(raw_url)
|
402
|
+
end
|
403
|
+
|
404
|
+
def exists?(path, use_get=false)
|
405
|
+
response = use_get ? @pool.get(path) : @pool.head(path)
|
406
|
+
response.code >= 200 && response.code <= 299
|
407
|
+
end
|
408
|
+
|
409
|
+
def template_exists?(template_endpoint, name)
|
410
|
+
exists?("/#{template_endpoint}/#{name}")
|
411
|
+
end
|
412
|
+
|
413
|
+
def template_put(template_endpoint, name, template)
|
414
|
+
path = "#{template_endpoint}/#{name}"
|
415
|
+
logger.info("Installing Elasticsearch template", name: name)
|
416
|
+
@pool.put(path, nil, LogStash::Json.dump(template))
|
417
|
+
end
|
418
|
+
|
419
|
+
# ILM methods
|
420
|
+
|
421
|
+
# check whether rollover alias already exists
|
422
|
+
def rollover_alias_exists?(name)
|
423
|
+
exists?(name)
|
424
|
+
end
|
425
|
+
|
426
|
+
# Create a new rollover alias
|
427
|
+
def rollover_alias_put(alias_name, alias_definition)
|
428
|
+
begin
|
429
|
+
@pool.put(CGI::escape(alias_name), nil, LogStash::Json.dump(alias_definition))
|
430
|
+
logger.info("Created rollover alias", name: alias_name)
|
431
|
+
# If the rollover alias already exists, ignore the error that comes back from Elasticsearch
|
432
|
+
rescue ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError => e
|
433
|
+
if e.response_code == 400
|
434
|
+
logger.info("Rollover alias already exists, skipping", name: alias_name)
|
435
|
+
return
|
436
|
+
end
|
437
|
+
raise e
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def get_xpack_info
|
442
|
+
get("/_xpack")
|
443
|
+
end
|
444
|
+
|
445
|
+
def get_ilm_endpoint
|
446
|
+
@pool.get("/_ilm/policy")
|
447
|
+
end
|
448
|
+
|
449
|
+
def ilm_policy_exists?(name)
|
450
|
+
exists?("/_ilm/policy/#{name}", true)
|
451
|
+
end
|
452
|
+
|
453
|
+
def ilm_policy_put(name, policy)
|
454
|
+
path = "_ilm/policy/#{name}"
|
455
|
+
logger.info("Installing ILM policy #{policy}", name: name)
|
456
|
+
@pool.put(path, nil, LogStash::Json.dump(policy))
|
457
|
+
end
|
458
|
+
|
459
|
+
|
460
|
+
# Build a bulk item for an elasticsearch update action
|
461
|
+
def update_action_builder(args, source)
|
462
|
+
args = args.clone()
|
463
|
+
if args[:_script]
|
464
|
+
# Use the event as a hash from your script with variable name defined
|
465
|
+
# by script_var_name (default: "event")
|
466
|
+
# Ex: event["@timestamp"]
|
467
|
+
source_orig = source
|
468
|
+
source = { 'script' => {'params' => { @options[:script_var_name] => source_orig }} }
|
469
|
+
if @options[:scripted_upsert]
|
470
|
+
source['scripted_upsert'] = true
|
471
|
+
source['upsert'] = {}
|
472
|
+
elsif @options[:doc_as_upsert]
|
473
|
+
source['upsert'] = source_orig
|
474
|
+
else
|
475
|
+
source['upsert'] = args.delete(:_upsert) if args[:_upsert]
|
476
|
+
end
|
477
|
+
case @options[:script_type]
|
478
|
+
when 'indexed'
|
479
|
+
source['script']['id'] = args.delete(:_script)
|
480
|
+
when 'file'
|
481
|
+
source['script']['file'] = args.delete(:_script)
|
482
|
+
when 'inline'
|
483
|
+
source['script']['inline'] = args.delete(:_script)
|
484
|
+
end
|
485
|
+
source['script']['lang'] = @options[:script_lang] if @options[:script_lang] != ''
|
486
|
+
else
|
487
|
+
source = { 'doc' => source }
|
488
|
+
if @options[:doc_as_upsert]
|
489
|
+
source['doc_as_upsert'] = true
|
490
|
+
else
|
491
|
+
source['upsert'] = args.delete(:_upsert) if args[:_upsert]
|
492
|
+
end
|
493
|
+
end
|
494
|
+
[args, source]
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end end end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
module LogStash; module Outputs; class ElasticSearch;
|
5
|
+
module HttpClientBuilder
|
6
|
+
def self.build(logger, hosts, params)
|
7
|
+
client_settings = {
|
8
|
+
:pool_max => params["pool_max"],
|
9
|
+
:pool_max_per_route => params["pool_max_per_route"],
|
10
|
+
:check_connection_timeout => params["validate_after_inactivity"],
|
11
|
+
:http_compression => params["http_compression"],
|
12
|
+
:headers => params["custom_headers"] || {}
|
13
|
+
}
|
14
|
+
|
15
|
+
client_settings[:proxy] = params["proxy"] if params["proxy"]
|
16
|
+
|
17
|
+
common_options = {
|
18
|
+
:license_checker => params["license_checker"],
|
19
|
+
:client_settings => client_settings,
|
20
|
+
:metric => params["metric"],
|
21
|
+
:resurrect_delay => params["resurrect_delay"]
|
22
|
+
}
|
23
|
+
|
24
|
+
if params["sniffing"]
|
25
|
+
common_options[:sniffing] = true
|
26
|
+
common_options[:sniffer_delay] = params["sniffing_delay"]
|
27
|
+
end
|
28
|
+
|
29
|
+
common_options[:timeout] = params["timeout"] if params["timeout"]
|
30
|
+
|
31
|
+
if params["path"]
|
32
|
+
client_settings[:path] = dedup_slashes("/#{params["path"]}/")
|
33
|
+
end
|
34
|
+
|
35
|
+
common_options[:bulk_path] = if params["bulk_path"]
|
36
|
+
dedup_slashes("/#{params["bulk_path"]}")
|
37
|
+
else
|
38
|
+
dedup_slashes("/#{params["path"]}/_bulk")
|
39
|
+
end
|
40
|
+
|
41
|
+
common_options[:sniffing_path] = if params["sniffing_path"]
|
42
|
+
dedup_slashes("/#{params["sniffing_path"]}")
|
43
|
+
else
|
44
|
+
dedup_slashes("/#{params["path"]}/_nodes/http")
|
45
|
+
end
|
46
|
+
|
47
|
+
common_options[:healthcheck_path] = if params["healthcheck_path"]
|
48
|
+
dedup_slashes("/#{params["healthcheck_path"]}")
|
49
|
+
else
|
50
|
+
dedup_slashes("/#{params["path"]}")
|
51
|
+
end
|
52
|
+
|
53
|
+
if params["parameters"]
|
54
|
+
client_settings[:parameters] = params["parameters"]
|
55
|
+
end
|
56
|
+
|
57
|
+
logger.debug? && logger.debug("Normalizing http path", :path => params["path"], :normalized => client_settings[:path])
|
58
|
+
|
59
|
+
client_settings.merge! setup_ssl(logger, params)
|
60
|
+
common_options.merge! setup_basic_auth(logger, params)
|
61
|
+
client_settings[:headers].merge! setup_api_key(logger, params)
|
62
|
+
|
63
|
+
external_version_types = ["external", "external_gt", "external_gte"]
|
64
|
+
# External Version validation
|
65
|
+
raise(
|
66
|
+
LogStash::ConfigurationError,
|
67
|
+
"External versioning requires the presence of a version number."
|
68
|
+
) if external_version_types.include?(params.fetch('version_type', '')) and params.fetch("version", nil) == nil
|
69
|
+
|
70
|
+
|
71
|
+
# Create API setup
|
72
|
+
raise(
|
73
|
+
LogStash::ConfigurationError,
|
74
|
+
"External versioning is not supported by the create action."
|
75
|
+
) if params['action'] == 'create' and external_version_types.include?(params.fetch('version_type', ''))
|
76
|
+
|
77
|
+
# Update API setup
|
78
|
+
raise( LogStash::ConfigurationError,
|
79
|
+
"doc_as_upsert and scripted_upsert are mutually exclusive."
|
80
|
+
) if params["doc_as_upsert"] and params["scripted_upsert"]
|
81
|
+
|
82
|
+
raise(
|
83
|
+
LogStash::ConfigurationError,
|
84
|
+
"Specifying action => 'update' needs a document_id."
|
85
|
+
) if params['action'] == 'update' and params.fetch('document_id', '') == ''
|
86
|
+
|
87
|
+
raise(
|
88
|
+
LogStash::ConfigurationError,
|
89
|
+
"External versioning is not supported by the update action. See https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html."
|
90
|
+
) if params['action'] == 'update' and external_version_types.include?(params.fetch('version_type', ''))
|
91
|
+
|
92
|
+
# Update API setup
|
93
|
+
update_options = {
|
94
|
+
:doc_as_upsert => params["doc_as_upsert"],
|
95
|
+
:script_var_name => params["script_var_name"],
|
96
|
+
:script_type => params["script_type"],
|
97
|
+
:script_lang => params["script_lang"],
|
98
|
+
:scripted_upsert => params["scripted_upsert"]
|
99
|
+
}
|
100
|
+
common_options.merge! update_options if params["action"] == 'update'
|
101
|
+
|
102
|
+
create_http_client(common_options.merge(:hosts => hosts, :logger => logger))
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.create_http_client(options)
|
106
|
+
LogStash::Outputs::ElasticSearch::HttpClient.new(options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.setup_ssl(logger, params)
|
110
|
+
params["ssl_enabled"] = true if params["hosts"].any? {|h| h.scheme == "https" }
|
111
|
+
return {} if params["ssl_enabled"].nil?
|
112
|
+
|
113
|
+
return {:ssl => {:enabled => false}} if params["ssl_enabled"] == false
|
114
|
+
|
115
|
+
ssl_certificate_authorities, ssl_truststore_path, ssl_certificate, ssl_keystore_path = params.values_at('ssl_certificate_authorities', 'ssl_truststore_path', 'ssl_certificate', 'ssl_keystore_path')
|
116
|
+
|
117
|
+
if ssl_certificate_authorities && ssl_truststore_path
|
118
|
+
raise LogStash::ConfigurationError, 'Use either "ssl_certificate_authorities/cacert" or "ssl_truststore_path/truststore" when configuring the CA certificate'
|
119
|
+
end
|
120
|
+
|
121
|
+
if ssl_certificate && ssl_keystore_path
|
122
|
+
raise LogStash::ConfigurationError, 'Use either "ssl_certificate" or "ssl_keystore_path/keystore" when configuring client certificates'
|
123
|
+
end
|
124
|
+
|
125
|
+
ssl_options = {:enabled => true}
|
126
|
+
|
127
|
+
if ssl_certificate_authorities&.any?
|
128
|
+
raise LogStash::ConfigurationError, 'Multiple values on "ssl_certificate_authorities" are not supported by this plugin' if ssl_certificate_authorities.size > 1
|
129
|
+
ssl_options[:ca_file] = ssl_certificate_authorities.first
|
130
|
+
end
|
131
|
+
|
132
|
+
setup_ssl_store(ssl_options, 'truststore', params)
|
133
|
+
setup_ssl_store(ssl_options, 'keystore', params)
|
134
|
+
|
135
|
+
ssl_key = params["ssl_key"]
|
136
|
+
if ssl_certificate
|
137
|
+
raise LogStash::ConfigurationError, 'Using an "ssl_certificate" requires an "ssl_key"' unless ssl_key
|
138
|
+
ssl_options[:client_cert] = ssl_certificate
|
139
|
+
ssl_options[:client_key] = ssl_key
|
140
|
+
elsif !ssl_key.nil?
|
141
|
+
raise LogStash::ConfigurationError, 'An "ssl_certificate" is required when using an "ssl_key"'
|
142
|
+
end
|
143
|
+
|
144
|
+
ssl_verification_mode = params["ssl_verification_mode"]
|
145
|
+
unless ssl_verification_mode.nil?
|
146
|
+
case ssl_verification_mode
|
147
|
+
when 'none'
|
148
|
+
logger.warn "You have enabled encryption but DISABLED certificate verification, " +
|
149
|
+
"to make sure your data is secure set `ssl_verification_mode => full`"
|
150
|
+
ssl_options[:verify] = :disable
|
151
|
+
else
|
152
|
+
# Manticore's :default maps to Apache HTTP Client's DefaultHostnameVerifier,
|
153
|
+
# which is the modern STRICT verifier that replaces the deprecated StrictHostnameVerifier
|
154
|
+
ssl_options[:verify] = :default
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
ssl_options[:cipher_suites] = params["ssl_cipher_suites"] if params.include?("ssl_cipher_suites")
|
159
|
+
ssl_options[:trust_strategy] = params["ssl_trust_strategy"] if params.include?("ssl_trust_strategy")
|
160
|
+
|
161
|
+
protocols = params['ssl_supported_protocols']
|
162
|
+
ssl_options[:protocols] = protocols if protocols && protocols.any?
|
163
|
+
|
164
|
+
{ ssl: ssl_options }
|
165
|
+
end
|
166
|
+
|
167
|
+
# @param kind is a string [truststore|keystore]
|
168
|
+
def self.setup_ssl_store(ssl_options, kind, params)
|
169
|
+
store_path = params["ssl_#{kind}_path"]
|
170
|
+
if store_path
|
171
|
+
ssl_options[kind.to_sym] = store_path
|
172
|
+
ssl_options["#{kind}_type".to_sym] = params["ssl_#{kind}_type"] if params.include?("ssl_#{kind}_type")
|
173
|
+
ssl_options["#{kind}_password".to_sym] = params["ssl_#{kind}_password"].value if params.include?("ssl_#{kind}_password")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.setup_basic_auth(logger, params)
|
178
|
+
user, password = params["user"], params["password"]
|
179
|
+
|
180
|
+
return {} unless user && password && password.value
|
181
|
+
|
182
|
+
{
|
183
|
+
:user => CGI.escape(user),
|
184
|
+
:password => CGI.escape(password.value)
|
185
|
+
}
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.setup_api_key(logger, params)
|
189
|
+
api_key = params["api_key"]
|
190
|
+
|
191
|
+
return {} unless (api_key && api_key.value)
|
192
|
+
|
193
|
+
{ "Authorization" => "ApiKey " + Base64.strict_encode64(api_key.value) }
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
def self.dedup_slashes(url)
|
198
|
+
url.gsub(/\/+/, "/")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end; end; end
|