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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/docs/index.asciidoc +181 -9
- data/lib/logstash/filters/elasticsearch/client.rb +8 -0
- data/lib/logstash/filters/elasticsearch/dsl_executor.rb +140 -0
- data/lib/logstash/filters/elasticsearch/esql_executor.rb +178 -0
- data/lib/logstash/filters/elasticsearch.rb +115 -114
- data/logstash-filter-elasticsearch.gemspec +3 -1
- data/spec/filters/elasticsearch_dsl_spec.rb +372 -0
- data/spec/filters/elasticsearch_esql_spec.rb +211 -0
- data/spec/filters/elasticsearch_spec.rb +140 -310
- data/spec/filters/integration/elasticsearch_esql_spec.rb +167 -0
- data/spec/filters/integration/elasticsearch_spec.rb +9 -2
- metadata +38 -2
@@ -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
|
-
#
|
22
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
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
|
-
|
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.
|
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'
|