logstash-filter-elasticsearch 3.17.1 → 3.19.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,13 +2,23 @@
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'
6
8
  require "logstash/plugin_mixins/normalize_config_support"
9
+ require 'logstash/plugin_mixins/validator_support/field_reference_validation_adapter'
7
10
  require "monitor"
8
11
 
9
12
  require_relative "elasticsearch/client"
10
13
 
11
14
  class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
15
+
16
+ require 'logstash/filters/elasticsearch/dsl_executor'
17
+ require 'logstash/filters/elasticsearch/esql_executor'
18
+
19
+ include LogStash::PluginMixins::ECSCompatibilitySupport
20
+ include LogStash::PluginMixins::ECSCompatibilitySupport::TargetCheck
21
+
12
22
  config_name "elasticsearch"
13
23
 
14
24
  # List of elasticsearch hosts to use for querying.
@@ -18,8 +28,13 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
18
28
  # Field substitution (e.g. `index-name-%{date_field}`) is available
19
29
  config :index, :validate => :string, :default => ""
20
30
 
21
- # Elasticsearch query string. Read the Elasticsearch query string documentation.
22
- # for more info at: https://www.elastic.co/guide/en/elasticsearch/reference/master/query-dsl-query-string-query.html#query-string-syntax
31
+ # A type of Elasticsearch query, provided by @query.
32
+ config :query_type, :validate => %w[esql dsl], :default => "dsl"
33
+
34
+ # Elasticsearch query string. This can be in DSL or ES|QL query shape defined by @query_type.
35
+ # Read the Elasticsearch query string documentation.
36
+ # DSL: https://www.elastic.co/guide/en/elasticsearch/reference/master/query-dsl-query-string-query.html#query-string-syntax
37
+ # ES|QL: https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html
23
38
  config :query, :validate => :string
24
39
 
25
40
  # File path to elasticsearch query in DSL format. Read the Elasticsearch query documentation
@@ -131,12 +146,25 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
131
146
  # Tags the event on failure to look up geo information. This can be used in later analysis.
132
147
  config :tag_on_failure, :validate => :array, :default => ["_elasticsearch_lookup_failure"]
133
148
 
149
+ # If set, the result set will be nested under the target field
150
+ config :target, :validate => :field_reference
151
+
134
152
  # How many times to retry on failure?
135
153
  config :retry_on_failure, :validate => :number, :default => 0
136
154
 
137
155
  # What status codes to retry on?
138
156
  config :retry_on_status, :validate => :number, :list => true, :default => [500, 502, 503, 504]
139
157
 
158
+ # named placeholders in ES|QL query
159
+ # example,
160
+ # if the query is "FROM my-index | WHERE some_type = ?type AND depth > ?min_depth"
161
+ # named placeholders can be applied as the following in query_params:
162
+ # query_params => [
163
+ # {"type" => "%{[type]}"}
164
+ # {"min_depth" => "%{[depth]}"}
165
+ # ]
166
+ config :query_params, :validate => :array, :default => []
167
+
140
168
  # config :ca_trusted_fingerprint, :validate => :sha_256_hex
141
169
  include LogStash::PluginMixins::CATrustedFingerprintSupport
142
170
 
@@ -145,6 +173,9 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
145
173
  include MonitorMixin
146
174
  attr_reader :shared_client
147
175
 
176
+ LS_ESQL_SUPPORT_VERSION = "8.17.4" # the version started using elasticsearch-ruby v8
177
+ ES_ESQL_SUPPORT_VERSION = "8.11.0"
178
+
148
179
  ##
149
180
  # @override to handle proxy => '' as if none was set
150
181
  # @param value [Array<Object>]
@@ -162,17 +193,22 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
162
193
  return super(value, :uri)
163
194
  end
164
195
 
196
+ attr_reader :query_dsl
197
+
165
198
  def register
166
- #Load query if it exists
167
- if @query_template
168
- if File.zero?(@query_template)
169
- raise "template is empty"
170
- end
171
- file = File.open(@query_template, 'r')
172
- @query_dsl = file.read
199
+ case @query_type
200
+ when "esql"
201
+ invalid_params_with_esql = original_params.keys & %w(index query_template sort fields docinfo_fields aggregation_fields enable_sort result_size)
202
+ raise LogStash::ConfigurationError, "Configured #{invalid_params_with_esql} params cannot be used with ES|QL query" if invalid_params_with_esql.any?
203
+
204
+ validate_ls_version_for_esql_support!
205
+ validate_esql_query_and_params!
206
+ @esql_executor ||= LogStash::Filters::Elasticsearch::EsqlExecutor.new(self, @logger)
207
+ else # dsl
208
+ validate_dsl_query_settings!
209
+ @esql_executor ||= LogStash::Filters::Elasticsearch::DslExecutor.new(self, @logger)
173
210
  end
174
211
 
175
- validate_query_settings
176
212
  fill_hosts_from_cloud_id
177
213
  setup_ssl_params!
178
214
  validate_authentication
@@ -181,6 +217,7 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
181
217
  @hosts = Array(@hosts).map { |host| host.to_s } # potential SafeURI#to_s
182
218
 
183
219
  test_connection!
220
+ validate_es_for_esql_support! if @query_type == "esql"
184
221
  setup_serverless
185
222
  if get_client.es_transport_client_type == "elasticsearch_transport"
186
223
  require_relative "elasticsearch/patches/_elasticsearch_transport_http_manticore"
@@ -188,69 +225,15 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
188
225
  end # def register
189
226
 
190
227
  def filter(event)
191
- matched = false
192
- begin
193
- params = { :index => event.sprintf(@index) }
194
-
195
- if @query_dsl
196
- query = LogStash::Json.load(event.sprintf(@query_dsl))
197
- params[:body] = query
198
- else
199
- query = event.sprintf(@query)
200
- params[:q] = query
201
- params[:size] = result_size
202
- params[:sort] = @sort if @enable_sort
203
- end
204
-
205
- @logger.debug("Querying elasticsearch for lookup", :params => params)
206
-
207
- results = get_client.search(params)
208
- raise "Elasticsearch query error: #{results["_shards"]["failures"]}" if results["_shards"].include? "failures"
209
-
210
- event.set("[@metadata][total_hits]", extract_total_from_hits(results['hits']))
211
-
212
- resultsHits = results["hits"]["hits"]
213
- if !resultsHits.nil? && !resultsHits.empty?
214
- matched = true
215
- @fields.each do |old_key, new_key|
216
- old_key_path = extract_path(old_key)
217
- set = resultsHits.map do |doc|
218
- extract_value(doc["_source"], old_key_path)
219
- end
220
- event.set(new_key, set.count > 1 ? set : set.first)
221
- end
222
- @docinfo_fields.each do |old_key, new_key|
223
- old_key_path = extract_path(old_key)
224
- set = resultsHits.map do |doc|
225
- extract_value(doc, old_key_path)
226
- end
227
- event.set(new_key, set.count > 1 ? set : set.first)
228
- end
229
- end
230
-
231
- resultsAggs = results["aggregations"]
232
- if !resultsAggs.nil? && !resultsAggs.empty?
233
- matched = true
234
- @aggregation_fields.each do |agg_name, ls_field|
235
- event.set(ls_field, resultsAggs[agg_name])
236
- end
237
- end
238
-
239
- rescue => e
240
- if @logger.trace?
241
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :query => query, :event => event.to_hash, :error => e.message, :backtrace => e.backtrace)
242
- elsif @logger.debug?
243
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message, :backtrace => e.backtrace)
244
- else
245
- @logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message)
246
- end
247
- @tag_on_failure.each{|tag| event.tag(tag)}
248
- else
249
- filter_matched(event) if matched
250
- end
228
+ @esql_executor.process(get_client, event)
251
229
  end # def filter
252
230
 
253
- # public only to be reuse in testing
231
+ def decorate(event)
232
+ # this Elasticsearch class has access to `filter_matched`
233
+ filter_matched(event)
234
+ end
235
+
236
+ # public only to be reused in testing
254
237
  def prepare_user_agent
255
238
  os_name = java.lang.System.getProperty('os.name')
256
239
  os_version = java.lang.System.getProperty('os.version')
@@ -361,53 +344,10 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
361
344
  end
362
345
  end
363
346
 
364
- # get an array of path elements from a path reference
365
- def extract_path(path_reference)
366
- return [path_reference] unless path_reference.start_with?('[') && path_reference.end_with?(']')
367
-
368
- path_reference[1...-1].split('][')
369
- end
370
-
371
- # given a Hash and an array of path fragments, returns the value at the path
372
- # @param source [Hash{String=>Object}]
373
- # @param path [Array{String}]
374
- # @return [Object]
375
- def extract_value(source, path)
376
- path.reduce(source) do |memo, old_key_fragment|
377
- break unless memo.include?(old_key_fragment)
378
- memo[old_key_fragment]
379
- end
380
- end
381
-
382
- # Given a "hits" object from an Elasticsearch response, return the total number of hits in
383
- # the result set.
384
- # @param hits [Hash{String=>Object}]
385
- # @return [Integer]
386
- def extract_total_from_hits(hits)
387
- total = hits['total']
388
-
389
- # Elasticsearch 7.x produces an object containing `value` and `relation` in order
390
- # to enable unambiguous reporting when the total is only a lower bound; if we get
391
- # an object back, return its `value`.
392
- return total['value'] if total.kind_of?(Hash)
393
-
394
- total
395
- end
396
-
397
347
  def hosts_default?(hosts)
398
348
  hosts.is_a?(Array) && hosts.size == 1 && !original_params.key?('hosts')
399
349
  end
400
350
 
401
- def validate_query_settings
402
- unless @query || @query_template
403
- raise LogStash::ConfigurationError, "Both `query` and `query_template` are empty. Require either `query` or `query_template`."
404
- end
405
-
406
- if @query && @query_template
407
- raise LogStash::ConfigurationError, "Both `query` and `query_template` are set. Use either `query` or `query_template`."
408
- end
409
- end
410
-
411
351
  def validate_authentication
412
352
  authn_options = 0
413
353
  authn_options += 1 if @cloud_auth
@@ -536,4 +476,65 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
536
476
  hosts.all? { |host| host && host.to_s.start_with?("https") }
537
477
  end
538
478
 
479
+ def validate_dsl_query_settings!
480
+ #Load query if it exists
481
+ if @query_template
482
+ if File.zero?(@query_template)
483
+ raise "template is empty"
484
+ end
485
+ file = File.open(@query_template, 'r')
486
+ @query_dsl = file.read
487
+ end
488
+
489
+ validate_query_settings
490
+ end
491
+
492
+ def validate_query_settings
493
+ unless @query || @query_template
494
+ raise LogStash::ConfigurationError, "Both `query` and `query_template` are empty. Require either `query` or `query_template`."
495
+ end
496
+
497
+ if @query && @query_template
498
+ raise LogStash::ConfigurationError, "Both `query` and `query_template` are set. Use either `query` or `query_template`."
499
+ end
500
+
501
+ if original_params.keys.include?("query_params")
502
+ raise LogStash::ConfigurationError, "`query_params` is not allowed when `query_type => 'dsl'`."
503
+ end
504
+ end
505
+
506
+ def validate_ls_version_for_esql_support!
507
+ if Gem::Version.create(LOGSTASH_VERSION) < Gem::Version.create(LS_ESQL_SUPPORT_VERSION)
508
+ fail("Current version of Logstash does not include Elasticsearch client which supports ES|QL. Please upgrade Logstash to at least #{LS_ESQL_SUPPORT_VERSION}")
509
+ end
510
+ end
511
+
512
+ def validate_esql_query_and_params!
513
+ # If Array, validate that query_params needs to contain only single-entry hashes, convert it to a Hash
514
+ if @query_params.kind_of?(Array)
515
+ illegal_entries = @query_params.reject {|e| e.kind_of?(Hash) && e.size == 1 }
516
+ raise LogStash::ConfigurationError, "`query_params` must contain only single-entry hashes. Illegal placeholders: #{illegal_entries}" if illegal_entries.any?
517
+
518
+ @query_params = @query_params.reduce({}, :merge)
519
+ end
520
+
521
+ illegal_keys = @query_params.keys.reject {|k| k[/^[a-z_][a-z0-9_]*$/] }
522
+ if illegal_keys.any?
523
+ message = "Illegal #{illegal_keys} placeholder names in `query_params`. A valid parameter name starts with a letter and contains letters, digits and underscores only;"
524
+ raise LogStash::ConfigurationError, message
525
+ end
526
+
527
+ placeholders = @query.scan(/(?<=[?])[a-z_][a-z0-9_]*/i)
528
+ placeholders.each do |placeholder|
529
+ raise LogStash::ConfigurationError, "Placeholder #{placeholder} not found in query" unless @query_params.include?(placeholder)
530
+ end
531
+ end
532
+
533
+ def validate_es_for_esql_support!
534
+ # make sure connected ES supports ES|QL (8.11+)
535
+ @es_version ||= get_client.es_version
536
+ es_supports_esql = Gem::Version.create(@es_version) >= Gem::Version.create(ES_ESQL_SUPPORT_VERSION)
537
+ 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
538
+ end
539
+
539
540
  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 = '3.17.1'
4
+ s.version = '3.19.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,8 +23,10 @@ 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'
27
28
  s.add_runtime_dependency 'logstash-mixin-normalize_config_support', '~>1.0'
29
+ s.add_runtime_dependency 'logstash-mixin-validator_support', '~> 1.0'
28
30
  s.add_development_dependency 'cabin', ['~> 0.6']
29
31
  s.add_development_dependency 'webrick'
30
32
  s.add_development_dependency 'logstash-devutils'