logstash-filter-elasticsearch 4.1.1 → 4.3.0

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.
@@ -2,12 +2,22 @@
2
2
  require "logstash/filters/base"
3
3
  require "logstash/namespace"
4
4
  require "logstash/json"
5
+ require 'logstash/plugin_mixins/ecs_compatibility_support'
6
+ require 'logstash/plugin_mixins/ecs_compatibility_support/target_check'
5
7
  require 'logstash/plugin_mixins/ca_trusted_fingerprint_support'
8
+ require 'logstash/plugin_mixins/validator_support/field_reference_validation_adapter'
6
9
  require "monitor"
7
10
 
8
11
  require_relative "elasticsearch/client"
9
12
 
10
13
  class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
14
+
15
+ require 'logstash/filters/elasticsearch/dsl_executor'
16
+ require 'logstash/filters/elasticsearch/esql_executor'
17
+
18
+ include LogStash::PluginMixins::ECSCompatibilitySupport
19
+ include LogStash::PluginMixins::ECSCompatibilitySupport::TargetCheck
20
+
11
21
  config_name "elasticsearch"
12
22
 
13
23
  # List of elasticsearch hosts to use for querying.
@@ -17,8 +27,13 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
17
27
  # Field substitution (e.g. `index-name-%{date_field}`) is available
18
28
  config :index, :validate => :string, :default => ""
19
29
 
20
- # Elasticsearch query string. Read the Elasticsearch query string documentation.
21
- # for more info at: https://www.elastic.co/guide/en/elasticsearch/reference/master/query-dsl-query-string-query.html#query-string-syntax
30
+ # A type of Elasticsearch query, provided by @query.
31
+ config :query_type, :validate => %w[esql dsl], :default => "dsl"
32
+
33
+ # Elasticsearch query string. This can be in DSL or ES|QL query shape defined by @query_type.
34
+ # Read the Elasticsearch query string documentation.
35
+ # DSL: https://www.elastic.co/guide/en/elasticsearch/reference/master/query-dsl-query-string-query.html#query-string-syntax
36
+ # ES|QL: https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html
22
37
  config :query, :validate => :string
23
38
 
24
39
  # File path to elasticsearch query in DSL format. Read the Elasticsearch query documentation
@@ -118,12 +133,24 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
118
133
  # Tags the event on failure to look up geo information. This can be used in later analysis.
119
134
  config :tag_on_failure, :validate => :array, :default => ["_elasticsearch_lookup_failure"]
120
135
 
136
+ # If set, the result set will be nested under the target field
137
+ config :target, :validate => :field_reference
138
+
121
139
  # How many times to retry on failure?
122
140
  config :retry_on_failure, :validate => :number, :default => 0
123
141
 
124
142
  # What status codes to retry on?
125
143
  config :retry_on_status, :validate => :number, :list => true, :default => [500, 502, 503, 504]
126
144
 
145
+ # named placeholders in ES|QL query
146
+ # example,
147
+ # if the query is "FROM my-index | WHERE some_type = ?type AND depth > ?min_depth"
148
+ # named placeholders can be applied as the following in query_params:
149
+ # query_params => [
150
+ # {"type" => "%{[type]}"}
151
+ # {"min_depth" => "%{[depth]}"}
152
+ # ]
153
+ config :query_params, :validate => :array, :default => []
127
154
 
128
155
  config :ssl, :obsolete => "Set 'ssl_enabled' instead."
129
156
  config :ca_file, :obsolete => "Set 'ssl_certificate_authorities' instead."
@@ -136,6 +163,9 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
136
163
  include MonitorMixin
137
164
  attr_reader :shared_client
138
165
 
166
+ LS_ESQL_SUPPORT_VERSION = "8.17.4" # the version started using elasticsearch-ruby v8
167
+ ES_ESQL_SUPPORT_VERSION = "8.11.0"
168
+
139
169
  ##
140
170
  # @override to handle proxy => '' as if none was set
141
171
  # @param value [Array<Object>]
@@ -153,17 +183,22 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
153
183
  return super(value, :uri)
154
184
  end
155
185
 
186
+ attr_reader :query_dsl
187
+
156
188
  def register
157
- #Load query if it exists
158
- if @query_template
159
- if File.zero?(@query_template)
160
- raise "template is empty"
161
- end
162
- file = File.open(@query_template, 'r')
163
- @query_dsl = file.read
189
+ case @query_type
190
+ when "esql"
191
+ invalid_params_with_esql = original_params.keys & %w(index query_template sort fields docinfo_fields aggregation_fields enable_sort result_size)
192
+ raise LogStash::ConfigurationError, "Configured #{invalid_params_with_esql} params cannot be used with ES|QL query" if invalid_params_with_esql.any?
193
+
194
+ validate_ls_version_for_esql_support!
195
+ validate_esql_query_and_params!
196
+ @esql_executor ||= LogStash::Filters::Elasticsearch::EsqlExecutor.new(self, @logger)
197
+ else # dsl
198
+ validate_dsl_query_settings!
199
+ @esql_executor ||= LogStash::Filters::Elasticsearch::DslExecutor.new(self, @logger)
164
200
  end
165
201
 
166
- validate_query_settings
167
202
  fill_hosts_from_cloud_id
168
203
  setup_ssl_params!
169
204
  validate_authentication
@@ -172,6 +207,7 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
172
207
  @hosts = Array(@hosts).map { |host| host.to_s } # potential SafeURI#to_s
173
208
 
174
209
  test_connection!
210
+ validate_es_for_esql_support! if @query_type == "esql"
175
211
  setup_serverless
176
212
  if get_client.es_transport_client_type == "elasticsearch_transport"
177
213
  require_relative "elasticsearch/patches/_elasticsearch_transport_http_manticore"
@@ -179,69 +215,15 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
179
215
  end # def register
180
216
 
181
217
  def filter(event)
182
- matched = false
183
- begin
184
- params = { :index => event.sprintf(@index) }
185
-
186
- if @query_dsl
187
- query = LogStash::Json.load(event.sprintf(@query_dsl))
188
- params[:body] = query
189
- else
190
- query = event.sprintf(@query)
191
- params[:q] = query
192
- params[:size] = result_size
193
- params[:sort] = @sort if @enable_sort
194
- end
195
-
196
- @logger.debug("Querying elasticsearch for lookup", :params => params)
197
-
198
- results = get_client.search(params)
199
- raise "Elasticsearch query error: #{results["_shards"]["failures"]}" if results["_shards"].include? "failures"
200
-
201
- event.set("[@metadata][total_hits]", extract_total_from_hits(results['hits']))
202
-
203
- resultsHits = results["hits"]["hits"]
204
- if !resultsHits.nil? && !resultsHits.empty?
205
- matched = true
206
- @fields.each do |old_key, new_key|
207
- old_key_path = extract_path(old_key)
208
- set = resultsHits.map do |doc|
209
- extract_value(doc["_source"], old_key_path)
210
- end
211
- event.set(new_key, set.count > 1 ? set : set.first)
212
- end
213
- @docinfo_fields.each do |old_key, new_key|
214
- old_key_path = extract_path(old_key)
215
- set = resultsHits.map do |doc|
216
- extract_value(doc, old_key_path)
217
- end
218
- event.set(new_key, set.count > 1 ? set : set.first)
219
- end
220
- end
221
-
222
- resultsAggs = results["aggregations"]
223
- if !resultsAggs.nil? && !resultsAggs.empty?
224
- matched = true
225
- @aggregation_fields.each do |agg_name, ls_field|
226
- event.set(ls_field, resultsAggs[agg_name])
227
- end
228
- end
229
-
230
- rescue => e
231
- if @logger.trace?
232
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :query => query, :event => event.to_hash, :error => e.message, :backtrace => e.backtrace)
233
- elsif @logger.debug?
234
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message, :backtrace => e.backtrace)
235
- else
236
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message)
237
- end
238
- @tag_on_failure.each{|tag| event.tag(tag)}
239
- else
240
- filter_matched(event) if matched
241
- end
218
+ @esql_executor.process(get_client, event)
242
219
  end # def filter
243
220
 
244
- # public only to be reuse in testing
221
+ def decorate(event)
222
+ # this Elasticsearch class has access to `filter_matched`
223
+ filter_matched(event)
224
+ end
225
+
226
+ # public only to be reused in testing
245
227
  def prepare_user_agent
246
228
  os_name = java.lang.System.getProperty('os.name')
247
229
  os_version = java.lang.System.getProperty('os.version')
@@ -352,53 +334,10 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
352
334
  end
353
335
  end
354
336
 
355
- # get an array of path elements from a path reference
356
- def extract_path(path_reference)
357
- return [path_reference] unless path_reference.start_with?('[') && path_reference.end_with?(']')
358
-
359
- path_reference[1...-1].split('][')
360
- end
361
-
362
- # given a Hash and an array of path fragments, returns the value at the path
363
- # @param source [Hash{String=>Object}]
364
- # @param path [Array{String}]
365
- # @return [Object]
366
- def extract_value(source, path)
367
- path.reduce(source) do |memo, old_key_fragment|
368
- break unless memo.include?(old_key_fragment)
369
- memo[old_key_fragment]
370
- end
371
- end
372
-
373
- # Given a "hits" object from an Elasticsearch response, return the total number of hits in
374
- # the result set.
375
- # @param hits [Hash{String=>Object}]
376
- # @return [Integer]
377
- def extract_total_from_hits(hits)
378
- total = hits['total']
379
-
380
- # Elasticsearch 7.x produces an object containing `value` and `relation` in order
381
- # to enable unambiguous reporting when the total is only a lower bound; if we get
382
- # an object back, return its `value`.
383
- return total['value'] if total.kind_of?(Hash)
384
-
385
- total
386
- end
387
-
388
337
  def hosts_default?(hosts)
389
338
  hosts.is_a?(Array) && hosts.size == 1 && !original_params.key?('hosts')
390
339
  end
391
340
 
392
- def validate_query_settings
393
- unless @query || @query_template
394
- raise LogStash::ConfigurationError, "Both `query` and `query_template` are empty. Require either `query` or `query_template`."
395
- end
396
-
397
- if @query && @query_template
398
- raise LogStash::ConfigurationError, "Both `query` and `query_template` are set. Use either `query` or `query_template`."
399
- end
400
- end
401
-
402
341
  def validate_authentication
403
342
  authn_options = 0
404
343
  authn_options += 1 if @cloud_auth
@@ -490,4 +429,65 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
490
429
  params['ssl_enabled'] = @ssl_enabled ||= Array(@hosts).all? { |host| host && host.to_s.start_with?("https") }
491
430
  end
492
431
 
432
+ def validate_dsl_query_settings!
433
+ #Load query if it exists
434
+ if @query_template
435
+ if File.zero?(@query_template)
436
+ raise "template is empty"
437
+ end
438
+ file = File.open(@query_template, 'r')
439
+ @query_dsl = file.read
440
+ end
441
+
442
+ validate_query_settings
443
+ end
444
+
445
+ def validate_query_settings
446
+ unless @query || @query_template
447
+ raise LogStash::ConfigurationError, "Both `query` and `query_template` are empty. Require either `query` or `query_template`."
448
+ end
449
+
450
+ if @query && @query_template
451
+ raise LogStash::ConfigurationError, "Both `query` and `query_template` are set. Use either `query` or `query_template`."
452
+ end
453
+
454
+ if original_params.keys.include?("query_params")
455
+ raise LogStash::ConfigurationError, "`query_params` is not allowed when `query_type => 'dsl'`."
456
+ end
457
+ end
458
+
459
+ def validate_ls_version_for_esql_support!
460
+ if Gem::Version.create(LOGSTASH_VERSION) < Gem::Version.create(LS_ESQL_SUPPORT_VERSION)
461
+ fail("Current version of Logstash does not include Elasticsearch client which supports ES|QL. Please upgrade Logstash to at least #{LS_ESQL_SUPPORT_VERSION}")
462
+ end
463
+ end
464
+
465
+ def validate_esql_query_and_params!
466
+ # If Array, validate that query_params needs to contain only single-entry hashes, convert it to a Hash
467
+ if @query_params.kind_of?(Array)
468
+ illegal_entries = @query_params.reject {|e| e.kind_of?(Hash) && e.size == 1 }
469
+ raise LogStash::ConfigurationError, "`query_params` must contain only single-entry hashes. Illegal placeholders: #{illegal_entries}" if illegal_entries.any?
470
+
471
+ @query_params = @query_params.reduce({}, :merge)
472
+ end
473
+
474
+ illegal_keys = @query_params.keys.reject {|k| k[/^[a-z_][a-z0-9_]*$/] }
475
+ if illegal_keys.any?
476
+ message = "Illegal #{illegal_keys} placeholder names in `query_params`. A valid parameter name starts with a letter and contains letters, digits and underscores only;"
477
+ raise LogStash::ConfigurationError, message
478
+ end
479
+
480
+ placeholders = @query.scan(/(?<=[?])[a-z_][a-z0-9_]*/i)
481
+ placeholders.each do |placeholder|
482
+ raise LogStash::ConfigurationError, "Placeholder #{placeholder} not found in query" unless @query_params.include?(placeholder)
483
+ end
484
+ end
485
+
486
+ def validate_es_for_esql_support!
487
+ # make sure connected ES supports ES|QL (8.11+)
488
+ @es_version ||= get_client.es_version
489
+ es_supports_esql = Gem::Version.create(@es_version) >= Gem::Version.create(ES_ESQL_SUPPORT_VERSION)
490
+ fail("Connected Elasticsearch #{@es_version} version does not supports ES|QL. ES|QL feature requires at least Elasticsearch #{ES_ESQL_SUPPORT_VERSION} version.") unless es_supports_esql
491
+ end
492
+
493
493
  end #class LogStash::Filters::Elasticsearch
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-filter-elasticsearch'
4
- s.version = '4.1.1'
4
+ s.version = '4.3.0'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Copies fields from previous log events in Elasticsearch to current events "
7
7
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -23,7 +23,9 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
24
  s.add_runtime_dependency 'elasticsearch', ">= 7.14.9", '< 9'
25
25
  s.add_runtime_dependency 'manticore', ">= 0.7.1"
26
+ s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~> 1.3'
26
27
  s.add_runtime_dependency 'logstash-mixin-ca_trusted_fingerprint_support', '~> 1.0'
28
+ s.add_runtime_dependency 'logstash-mixin-validator_support', '~> 1.0'
27
29
  s.add_development_dependency 'cabin', ['~> 0.6']
28
30
  s.add_development_dependency 'webrick'
29
31
  s.add_development_dependency 'logstash-devutils'