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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +649 -0
  3. data/CONTRIBUTORS +34 -0
  4. data/Gemfile +16 -0
  5. data/LICENSE +202 -0
  6. data/NOTICE.TXT +5 -0
  7. data/README.md +106 -0
  8. data/docs/index.asciidoc +1369 -0
  9. data/lib/logstash/outputs/elasticsearch/data_stream_support.rb +282 -0
  10. data/lib/logstash/outputs/elasticsearch/default-ilm-policy.json +14 -0
  11. data/lib/logstash/outputs/elasticsearch/http_client/manticore_adapter.rb +155 -0
  12. data/lib/logstash/outputs/elasticsearch/http_client/pool.rb +534 -0
  13. data/lib/logstash/outputs/elasticsearch/http_client.rb +497 -0
  14. data/lib/logstash/outputs/elasticsearch/http_client_builder.rb +201 -0
  15. data/lib/logstash/outputs/elasticsearch/ilm.rb +92 -0
  16. data/lib/logstash/outputs/elasticsearch/license_checker.rb +52 -0
  17. data/lib/logstash/outputs/elasticsearch/template_manager.rb +131 -0
  18. data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-6x.json +45 -0
  19. data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-7x.json +44 -0
  20. data/lib/logstash/outputs/elasticsearch/templates/ecs-disabled/elasticsearch-8x.json +50 -0
  21. data/lib/logstash/outputs/elasticsearch.rb +699 -0
  22. data/lib/logstash/plugin_mixins/elasticsearch/api_configs.rb +237 -0
  23. data/lib/logstash/plugin_mixins/elasticsearch/common.rb +409 -0
  24. data/lib/logstash/plugin_mixins/elasticsearch/noop_license_checker.rb +9 -0
  25. data/logstash-output-elasticsearch.gemspec +40 -0
  26. data/spec/es_spec_helper.rb +225 -0
  27. data/spec/fixtures/_nodes/6x.json +81 -0
  28. data/spec/fixtures/_nodes/7x.json +92 -0
  29. data/spec/fixtures/htpasswd +2 -0
  30. data/spec/fixtures/license_check/active.json +16 -0
  31. data/spec/fixtures/license_check/inactive.json +5 -0
  32. data/spec/fixtures/nginx_reverse_proxy.conf +22 -0
  33. data/spec/fixtures/scripts/painless/scripted_update.painless +2 -0
  34. data/spec/fixtures/scripts/painless/scripted_update_nested.painless +1 -0
  35. data/spec/fixtures/scripts/painless/scripted_upsert.painless +1 -0
  36. data/spec/fixtures/template-with-policy-es6x.json +48 -0
  37. data/spec/fixtures/template-with-policy-es7x.json +45 -0
  38. data/spec/fixtures/template-with-policy-es8x.json +50 -0
  39. data/spec/fixtures/test_certs/ca.crt +29 -0
  40. data/spec/fixtures/test_certs/ca.der.sha256 +1 -0
  41. data/spec/fixtures/test_certs/ca.key +51 -0
  42. data/spec/fixtures/test_certs/renew.sh +13 -0
  43. data/spec/fixtures/test_certs/test.crt +30 -0
  44. data/spec/fixtures/test_certs/test.der.sha256 +1 -0
  45. data/spec/fixtures/test_certs/test.key +51 -0
  46. data/spec/fixtures/test_certs/test.p12 +0 -0
  47. data/spec/fixtures/test_certs/test_invalid.crt +36 -0
  48. data/spec/fixtures/test_certs/test_invalid.key +51 -0
  49. data/spec/fixtures/test_certs/test_invalid.p12 +0 -0
  50. data/spec/fixtures/test_certs/test_self_signed.crt +32 -0
  51. data/spec/fixtures/test_certs/test_self_signed.key +54 -0
  52. data/spec/fixtures/test_certs/test_self_signed.p12 +0 -0
  53. data/spec/integration/outputs/compressed_indexing_spec.rb +70 -0
  54. data/spec/integration/outputs/create_spec.rb +67 -0
  55. data/spec/integration/outputs/data_stream_spec.rb +68 -0
  56. data/spec/integration/outputs/delete_spec.rb +63 -0
  57. data/spec/integration/outputs/ilm_spec.rb +534 -0
  58. data/spec/integration/outputs/index_spec.rb +421 -0
  59. data/spec/integration/outputs/index_version_spec.rb +98 -0
  60. data/spec/integration/outputs/ingest_pipeline_spec.rb +75 -0
  61. data/spec/integration/outputs/metrics_spec.rb +66 -0
  62. data/spec/integration/outputs/no_es_on_startup_spec.rb +78 -0
  63. data/spec/integration/outputs/painless_update_spec.rb +99 -0
  64. data/spec/integration/outputs/parent_spec.rb +94 -0
  65. data/spec/integration/outputs/retry_spec.rb +182 -0
  66. data/spec/integration/outputs/routing_spec.rb +61 -0
  67. data/spec/integration/outputs/sniffer_spec.rb +94 -0
  68. data/spec/integration/outputs/templates_spec.rb +133 -0
  69. data/spec/integration/outputs/unsupported_actions_spec.rb +75 -0
  70. data/spec/integration/outputs/update_spec.rb +114 -0
  71. data/spec/spec_helper.rb +10 -0
  72. data/spec/support/elasticsearch/api/actions/delete_ilm_policy.rb +19 -0
  73. data/spec/support/elasticsearch/api/actions/get_alias.rb +18 -0
  74. data/spec/support/elasticsearch/api/actions/get_ilm_policy.rb +18 -0
  75. data/spec/support/elasticsearch/api/actions/put_alias.rb +24 -0
  76. data/spec/support/elasticsearch/api/actions/put_ilm_policy.rb +25 -0
  77. data/spec/unit/http_client_builder_spec.rb +185 -0
  78. data/spec/unit/outputs/elasticsearch/data_stream_support_spec.rb +612 -0
  79. data/spec/unit/outputs/elasticsearch/http_client/manticore_adapter_spec.rb +151 -0
  80. data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +501 -0
  81. data/spec/unit/outputs/elasticsearch/http_client_spec.rb +339 -0
  82. data/spec/unit/outputs/elasticsearch/template_manager_spec.rb +189 -0
  83. data/spec/unit/outputs/elasticsearch_proxy_spec.rb +103 -0
  84. data/spec/unit/outputs/elasticsearch_spec.rb +1573 -0
  85. data/spec/unit/outputs/elasticsearch_ssl_spec.rb +197 -0
  86. data/spec/unit/outputs/error_whitelist_spec.rb +56 -0
  87. data/spec/unit/outputs/license_check_spec.rb +57 -0
  88. 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