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.
- 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 +114 -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,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
|
-
#
|
21
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
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
|
-
|
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.
|
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'
|