logstash-filter-elasticsearch 4.2.0 → 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 +3 -0
- data/docs/index.asciidoc +132 -6
- 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 +105 -129
- data/logstash-filter-elasticsearch.gemspec +1 -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 +129 -326
- data/spec/filters/integration/elasticsearch_esql_spec.rb +167 -0
- metadata +10 -2
@@ -12,6 +12,9 @@ require_relative "elasticsearch/client"
|
|
12
12
|
|
13
13
|
class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
14
14
|
|
15
|
+
require 'logstash/filters/elasticsearch/dsl_executor'
|
16
|
+
require 'logstash/filters/elasticsearch/esql_executor'
|
17
|
+
|
15
18
|
include LogStash::PluginMixins::ECSCompatibilitySupport
|
16
19
|
include LogStash::PluginMixins::ECSCompatibilitySupport::TargetCheck
|
17
20
|
|
@@ -24,8 +27,13 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
24
27
|
# Field substitution (e.g. `index-name-%{date_field}`) is available
|
25
28
|
config :index, :validate => :string, :default => ""
|
26
29
|
|
27
|
-
#
|
28
|
-
|
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
|
29
37
|
config :query, :validate => :string
|
30
38
|
|
31
39
|
# File path to elasticsearch query in DSL format. Read the Elasticsearch query documentation
|
@@ -125,7 +133,7 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
125
133
|
# Tags the event on failure to look up geo information. This can be used in later analysis.
|
126
134
|
config :tag_on_failure, :validate => :array, :default => ["_elasticsearch_lookup_failure"]
|
127
135
|
|
128
|
-
# If set, the
|
136
|
+
# If set, the result set will be nested under the target field
|
129
137
|
config :target, :validate => :field_reference
|
130
138
|
|
131
139
|
# How many times to retry on failure?
|
@@ -134,6 +142,15 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
134
142
|
# What status codes to retry on?
|
135
143
|
config :retry_on_status, :validate => :number, :list => true, :default => [500, 502, 503, 504]
|
136
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 => []
|
137
154
|
|
138
155
|
config :ssl, :obsolete => "Set 'ssl_enabled' instead."
|
139
156
|
config :ca_file, :obsolete => "Set 'ssl_certificate_authorities' instead."
|
@@ -146,6 +163,9 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
146
163
|
include MonitorMixin
|
147
164
|
attr_reader :shared_client
|
148
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
|
+
|
149
169
|
##
|
150
170
|
# @override to handle proxy => '' as if none was set
|
151
171
|
# @param value [Array<Object>]
|
@@ -163,17 +183,22 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
163
183
|
return super(value, :uri)
|
164
184
|
end
|
165
185
|
|
186
|
+
attr_reader :query_dsl
|
187
|
+
|
166
188
|
def register
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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)
|
174
200
|
end
|
175
201
|
|
176
|
-
validate_query_settings
|
177
202
|
fill_hosts_from_cloud_id
|
178
203
|
setup_ssl_params!
|
179
204
|
validate_authentication
|
@@ -182,6 +207,7 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
182
207
|
@hosts = Array(@hosts).map { |host| host.to_s } # potential SafeURI#to_s
|
183
208
|
|
184
209
|
test_connection!
|
210
|
+
validate_es_for_esql_support! if @query_type == "esql"
|
185
211
|
setup_serverless
|
186
212
|
if get_client.es_transport_client_type == "elasticsearch_transport"
|
187
213
|
require_relative "elasticsearch/patches/_elasticsearch_transport_http_manticore"
|
@@ -189,71 +215,15 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
189
215
|
end # def register
|
190
216
|
|
191
217
|
def filter(event)
|
192
|
-
|
193
|
-
begin
|
194
|
-
params = { :index => event.sprintf(@index) }
|
195
|
-
|
196
|
-
if @query_dsl
|
197
|
-
query = LogStash::Json.load(event.sprintf(@query_dsl))
|
198
|
-
params[:body] = query
|
199
|
-
else
|
200
|
-
query = event.sprintf(@query)
|
201
|
-
params[:q] = query
|
202
|
-
params[:size] = result_size
|
203
|
-
params[:sort] = @sort if @enable_sort
|
204
|
-
end
|
205
|
-
|
206
|
-
@logger.debug("Querying elasticsearch for lookup", :params => params)
|
207
|
-
|
208
|
-
results = get_client.search(params)
|
209
|
-
raise "Elasticsearch query error: #{results["_shards"]["failures"]}" if results["_shards"].include? "failures"
|
210
|
-
|
211
|
-
event.set("[@metadata][total_hits]", extract_total_from_hits(results['hits']))
|
212
|
-
|
213
|
-
resultsHits = results["hits"]["hits"]
|
214
|
-
if !resultsHits.nil? && !resultsHits.empty?
|
215
|
-
matched = true
|
216
|
-
@fields.each do |old_key, new_key|
|
217
|
-
old_key_path = extract_path(old_key)
|
218
|
-
extracted_hit_values = resultsHits.map do |doc|
|
219
|
-
extract_value(doc["_source"], old_key_path)
|
220
|
-
end
|
221
|
-
value_to_set = extracted_hit_values.count > 1 ? extracted_hit_values : extracted_hit_values.first
|
222
|
-
set_to_event_target(event, new_key, value_to_set)
|
223
|
-
end
|
224
|
-
@docinfo_fields.each do |old_key, new_key|
|
225
|
-
old_key_path = extract_path(old_key)
|
226
|
-
extracted_docs_info = resultsHits.map do |doc|
|
227
|
-
extract_value(doc, old_key_path)
|
228
|
-
end
|
229
|
-
value_to_set = extracted_docs_info.count > 1 ? extracted_docs_info : extracted_docs_info.first
|
230
|
-
set_to_event_target(event, new_key, value_to_set)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
resultsAggs = results["aggregations"]
|
235
|
-
if !resultsAggs.nil? && !resultsAggs.empty?
|
236
|
-
matched = true
|
237
|
-
@aggregation_fields.each do |agg_name, ls_field|
|
238
|
-
set_to_event_target(event, ls_field, resultsAggs[agg_name])
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
rescue => e
|
243
|
-
if @logger.trace?
|
244
|
-
@logger.warn("Failed to query elasticsearch for previous event", :index => @index, :query => query, :event => event.to_hash, :error => e.message, :backtrace => e.backtrace)
|
245
|
-
elsif @logger.debug?
|
246
|
-
@logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message, :backtrace => e.backtrace)
|
247
|
-
else
|
248
|
-
@logger.warn("Failed to query elasticsearch for previous event", :index => @index, :error => e.message)
|
249
|
-
end
|
250
|
-
@tag_on_failure.each{|tag| event.tag(tag)}
|
251
|
-
else
|
252
|
-
filter_matched(event) if matched
|
253
|
-
end
|
218
|
+
@esql_executor.process(get_client, event)
|
254
219
|
end # def filter
|
255
220
|
|
256
|
-
|
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
|
257
227
|
def prepare_user_agent
|
258
228
|
os_name = java.lang.System.getProperty('os.name')
|
259
229
|
os_version = java.lang.System.getProperty('os.version')
|
@@ -268,18 +238,6 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
268
238
|
|
269
239
|
private
|
270
240
|
|
271
|
-
# if @target is defined, creates a nested structure to inject result into target field
|
272
|
-
# if not defined, directly sets to the top-level event field
|
273
|
-
# @param event [LogStash::Event]
|
274
|
-
# @param new_key [String] name of the field to set
|
275
|
-
# @param value_to_set [Array] values to set
|
276
|
-
# @return [void]
|
277
|
-
def set_to_event_target(event, new_key, value_to_set)
|
278
|
-
key_to_set = target ? "[#{target}][#{new_key}]" : new_key
|
279
|
-
|
280
|
-
event.set(key_to_set, value_to_set)
|
281
|
-
end
|
282
|
-
|
283
241
|
def client_options
|
284
242
|
@client_options ||= {
|
285
243
|
:user => @user,
|
@@ -376,53 +334,10 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
376
334
|
end
|
377
335
|
end
|
378
336
|
|
379
|
-
# get an array of path elements from a path reference
|
380
|
-
def extract_path(path_reference)
|
381
|
-
return [path_reference] unless path_reference.start_with?('[') && path_reference.end_with?(']')
|
382
|
-
|
383
|
-
path_reference[1...-1].split('][')
|
384
|
-
end
|
385
|
-
|
386
|
-
# given a Hash and an array of path fragments, returns the value at the path
|
387
|
-
# @param source [Hash{String=>Object}]
|
388
|
-
# @param path [Array{String}]
|
389
|
-
# @return [Object]
|
390
|
-
def extract_value(source, path)
|
391
|
-
path.reduce(source) do |memo, old_key_fragment|
|
392
|
-
break unless memo.include?(old_key_fragment)
|
393
|
-
memo[old_key_fragment]
|
394
|
-
end
|
395
|
-
end
|
396
|
-
|
397
|
-
# Given a "hits" object from an Elasticsearch response, return the total number of hits in
|
398
|
-
# the result set.
|
399
|
-
# @param hits [Hash{String=>Object}]
|
400
|
-
# @return [Integer]
|
401
|
-
def extract_total_from_hits(hits)
|
402
|
-
total = hits['total']
|
403
|
-
|
404
|
-
# Elasticsearch 7.x produces an object containing `value` and `relation` in order
|
405
|
-
# to enable unambiguous reporting when the total is only a lower bound; if we get
|
406
|
-
# an object back, return its `value`.
|
407
|
-
return total['value'] if total.kind_of?(Hash)
|
408
|
-
|
409
|
-
total
|
410
|
-
end
|
411
|
-
|
412
337
|
def hosts_default?(hosts)
|
413
338
|
hosts.is_a?(Array) && hosts.size == 1 && !original_params.key?('hosts')
|
414
339
|
end
|
415
340
|
|
416
|
-
def validate_query_settings
|
417
|
-
unless @query || @query_template
|
418
|
-
raise LogStash::ConfigurationError, "Both `query` and `query_template` are empty. Require either `query` or `query_template`."
|
419
|
-
end
|
420
|
-
|
421
|
-
if @query && @query_template
|
422
|
-
raise LogStash::ConfigurationError, "Both `query` and `query_template` are set. Use either `query` or `query_template`."
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
341
|
def validate_authentication
|
427
342
|
authn_options = 0
|
428
343
|
authn_options += 1 if @cloud_auth
|
@@ -514,4 +429,65 @@ class LogStash::Filters::Elasticsearch < LogStash::Filters::Base
|
|
514
429
|
params['ssl_enabled'] = @ssl_enabled ||= Array(@hosts).all? { |host| host && host.to_s.start_with?("https") }
|
515
430
|
end
|
516
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
|
+
|
517
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"
|
@@ -0,0 +1,372 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/devutils/rspec/spec_helper"
|
3
|
+
require "logstash/filters/elasticsearch"
|
4
|
+
|
5
|
+
describe LogStash::Filters::Elasticsearch::DslExecutor do
|
6
|
+
let(:client) { instance_double(LogStash::Filters::ElasticsearchClient) }
|
7
|
+
let(:logger) { double("logger") }
|
8
|
+
let(:plugin) { LogStash::Filters::Elasticsearch.new(plugin_config) }
|
9
|
+
let(:plugin_config) do
|
10
|
+
{
|
11
|
+
"index" => "test_index",
|
12
|
+
"query" => "test_query",
|
13
|
+
"fields" => { "field1" => "field1_mapped" },
|
14
|
+
"result_size" => 10,
|
15
|
+
"docinfo_fields" => { "_id" => "doc_id" },
|
16
|
+
"tag_on_failure" => ["_failure"],
|
17
|
+
"enable_sort" => true,
|
18
|
+
"sort" => "@timestamp:desc",
|
19
|
+
"aggregation_fields" => { "agg1" => "agg1_mapped" }
|
20
|
+
}
|
21
|
+
end
|
22
|
+
let(:dsl_executor) { described_class.new(plugin, logger) }
|
23
|
+
let(:event) { LogStash::Event.new({}) }
|
24
|
+
|
25
|
+
describe "#initialize" do
|
26
|
+
it "initializes instance variables correctly" do
|
27
|
+
expect(dsl_executor.instance_variable_get(:@index)).to eq("test_index")
|
28
|
+
expect(dsl_executor.instance_variable_get(:@query)).to eq("test_query")
|
29
|
+
expect(dsl_executor.instance_variable_get(:@query_dsl)).to eq(nil)
|
30
|
+
expect(dsl_executor.instance_variable_get(:@fields)).to eq({ "field1" => "field1_mapped" })
|
31
|
+
expect(dsl_executor.instance_variable_get(:@result_size)).to eq(10)
|
32
|
+
expect(dsl_executor.instance_variable_get(:@docinfo_fields)).to eq({ "_id" => "doc_id" })
|
33
|
+
expect(dsl_executor.instance_variable_get(:@tag_on_failure)).to eq(["_failure"])
|
34
|
+
expect(dsl_executor.instance_variable_get(:@enable_sort)).to eq(true)
|
35
|
+
expect(dsl_executor.instance_variable_get(:@sort)).to eq("@timestamp:desc")
|
36
|
+
expect(dsl_executor.instance_variable_get(:@aggregation_fields)).to eq({ "agg1" => "agg1_mapped" })
|
37
|
+
expect(dsl_executor.instance_variable_get(:@logger)).to eq(logger)
|
38
|
+
expect(dsl_executor.instance_variable_get(:@event_decorator)).not_to be_nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "data fetch" do
|
43
|
+
let(:plugin_config) do
|
44
|
+
{
|
45
|
+
"hosts" => ["localhost:9200"],
|
46
|
+
"query" => "response: 404",
|
47
|
+
"fields" => { "response" => "code" },
|
48
|
+
"docinfo_fields" => { "_index" => "es_index" },
|
49
|
+
"aggregation_fields" => { "bytes_avg" => "bytes_avg_ls_field" }
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:response) do
|
54
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_x_1.json")))
|
55
|
+
end
|
56
|
+
|
57
|
+
let(:client) { double(:client) }
|
58
|
+
|
59
|
+
before(:each) do
|
60
|
+
allow(LogStash::Filters::ElasticsearchClient).to receive(:new).and_return(client)
|
61
|
+
if defined?(Elastic::Transport)
|
62
|
+
allow(client).to receive(:es_transport_client_type).and_return('elastic_transport')
|
63
|
+
else
|
64
|
+
allow(client).to receive(:es_transport_client_type).and_return('elasticsearch_transport')
|
65
|
+
end
|
66
|
+
allow(client).to receive(:search).and_return(response)
|
67
|
+
allow(plugin).to receive(:test_connection!)
|
68
|
+
allow(plugin).to receive(:setup_serverless)
|
69
|
+
plugin.register
|
70
|
+
end
|
71
|
+
|
72
|
+
after(:each) do
|
73
|
+
Thread.current[:filter_elasticsearch_client] = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should enhance the current event with new data" do
|
77
|
+
plugin.filter(event)
|
78
|
+
expect(event.get("code")).to eq(404)
|
79
|
+
expect(event.get("es_index")).to eq("logstash-2014.08.26")
|
80
|
+
expect(event.get("bytes_avg_ls_field")["value"]).to eq(294)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should receive all necessary params to perform the search" do
|
84
|
+
expect(client).to receive(:search).with({:q=>"response: 404", :size=>1, :index=>"", :sort=>"@timestamp:desc"})
|
85
|
+
plugin.filter(event)
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when asking to hit specific index" do
|
89
|
+
|
90
|
+
let(:plugin_config) do
|
91
|
+
{
|
92
|
+
"index" => "foo*",
|
93
|
+
"hosts" => ["localhost:9200"],
|
94
|
+
"query" => "response: 404",
|
95
|
+
"fields" => { "response" => "code" }
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should receive all necessary params to perform the search" do
|
100
|
+
expect(client).to receive(:search).with({:q=>"response: 404", :size=>1, :index=>"foo*", :sort=>"@timestamp:desc"})
|
101
|
+
plugin.filter(event)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when asking for more than one result" do
|
106
|
+
|
107
|
+
let(:plugin_config) do
|
108
|
+
{
|
109
|
+
"hosts" => ["localhost:9200"],
|
110
|
+
"query" => "response: 404",
|
111
|
+
"fields" => { "response" => "code" },
|
112
|
+
"result_size" => 10
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
let(:response) do
|
117
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_x_10.json")))
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should enhance the current event with new data" do
|
121
|
+
plugin.filter(event)
|
122
|
+
expect(event.get("code")).to eq([404]*10)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'when Elasticsearch 7.x gives us a totals object instead of an integer' do
|
127
|
+
let(:plugin_config) do
|
128
|
+
{
|
129
|
+
"hosts" => ["localhost:9200"],
|
130
|
+
"query" => "response: 404",
|
131
|
+
"fields" => { "response" => "code" },
|
132
|
+
"result_size" => 10
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
let(:response) do
|
137
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "elasticsearch_7.x_hits_total_as_object.json")))
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should enhance the current event with new data" do
|
141
|
+
plugin.filter(event)
|
142
|
+
expect(event.get("[@metadata][total_hits]")).to eq(13476)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "if something wrong happen during connection" do
|
147
|
+
|
148
|
+
before(:each) do
|
149
|
+
allow(LogStash::Filters::ElasticsearchClient).to receive(:new).and_return(client)
|
150
|
+
allow(client).to receive(:search).and_raise("connection exception")
|
151
|
+
plugin.register
|
152
|
+
end
|
153
|
+
|
154
|
+
it "tag the event as something happened, but still deliver it" do
|
155
|
+
expect(plugin.logger).to receive(:warn)
|
156
|
+
plugin.filter(event)
|
157
|
+
expect(event.to_hash["tags"]).to include("_elasticsearch_lookup_failure")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Tagging test for positive results
|
162
|
+
context "Tagging should occur if query returns results" do
|
163
|
+
let(:plugin_config) do
|
164
|
+
{
|
165
|
+
"index" => "foo*",
|
166
|
+
"hosts" => ["localhost:9200"],
|
167
|
+
"query" => "response: 404",
|
168
|
+
"add_tag" => ["tagged"]
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
let(:response) do
|
173
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_x_10.json")))
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should tag the current event if results returned" do
|
177
|
+
plugin.filter(event)
|
178
|
+
expect(event.to_hash["tags"]).to include("tagged")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context "an aggregation search with size 0 that matches" do
|
183
|
+
let(:plugin_config) do
|
184
|
+
{
|
185
|
+
"index" => "foo*",
|
186
|
+
"hosts" => ["localhost:9200"],
|
187
|
+
"query" => "response: 404",
|
188
|
+
"add_tag" => ["tagged"],
|
189
|
+
"result_size" => 0,
|
190
|
+
"aggregation_fields" => { "bytes_avg" => "bytes_avg_ls_field" }
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
let(:response) do
|
195
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_size0_agg.json")))
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should tag the current event" do
|
199
|
+
plugin.filter(event)
|
200
|
+
expect(event.get("tags")).to include("tagged")
|
201
|
+
expect(event.get("bytes_avg_ls_field")["value"]).to eq(294)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Tagging test for negative results
|
206
|
+
context "Tagging should not occur if query has no results" do
|
207
|
+
let(:plugin_config) do
|
208
|
+
{
|
209
|
+
"index" => "foo*",
|
210
|
+
"hosts" => ["localhost:9200"],
|
211
|
+
"query" => "response: 404",
|
212
|
+
"add_tag" => ["tagged"]
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
let(:response) do
|
217
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_error.json")))
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should not tag the current event" do
|
221
|
+
plugin.filter(event)
|
222
|
+
expect(event.to_hash["tags"]).to_not include("tagged")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
context "testing a simple query template" do
|
226
|
+
let(:plugin_config) do
|
227
|
+
{
|
228
|
+
"hosts" => ["localhost:9200"],
|
229
|
+
"query_template" => File.join(File.dirname(__FILE__), "fixtures", "query_template.json"),
|
230
|
+
"fields" => { "response" => "code" },
|
231
|
+
"result_size" => 1
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
let(:response) do
|
236
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_x_1.json")))
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should enhance the current event with new data" do
|
240
|
+
plugin.filter(event)
|
241
|
+
expect(event.get("code")).to eq(404)
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
context "testing a simple index substitution" do
|
247
|
+
let(:event) {
|
248
|
+
LogStash::Event.new(
|
249
|
+
{
|
250
|
+
"subst_field" => "subst_value"
|
251
|
+
}
|
252
|
+
)
|
253
|
+
}
|
254
|
+
let(:plugin_config) do
|
255
|
+
{
|
256
|
+
"index" => "foo_%{subst_field}*",
|
257
|
+
"hosts" => ["localhost:9200"],
|
258
|
+
"query" => "response: 404",
|
259
|
+
"fields" => { "response" => "code" }
|
260
|
+
}
|
261
|
+
end
|
262
|
+
|
263
|
+
it "should receive substituted index name" do
|
264
|
+
expect(client).to receive(:search).with({:q => "response: 404", :size => 1, :index => "foo_subst_value*", :sort => "@timestamp:desc"})
|
265
|
+
plugin.filter(event)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
context "if query result errored but no exception is thrown" do
|
270
|
+
let(:response) do
|
271
|
+
LogStash::Json.load(File.read(File.join(File.dirname(__FILE__), "fixtures", "request_error.json")))
|
272
|
+
end
|
273
|
+
|
274
|
+
before(:each) do
|
275
|
+
allow(LogStash::Filters::ElasticsearchClient).to receive(:new).and_return(client)
|
276
|
+
allow(client).to receive(:search).and_return(response)
|
277
|
+
plugin.register
|
278
|
+
end
|
279
|
+
|
280
|
+
it "tag the event as something happened, but still deliver it" do
|
281
|
+
expect(plugin.logger).to receive(:warn)
|
282
|
+
plugin.filter(event)
|
283
|
+
expect(event.to_hash["tags"]).to include("_elasticsearch_lookup_failure")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
context 'with client-level retries' do
|
288
|
+
let(:plugin_config) do
|
289
|
+
super().merge(
|
290
|
+
"retry_on_failure" => 3,
|
291
|
+
"retry_on_status" => [500]
|
292
|
+
)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
context "with custom headers" do
|
297
|
+
let(:plugin_config) do
|
298
|
+
{
|
299
|
+
"query" => "*",
|
300
|
+
"custom_headers" => { "Custom-Header-1" => "Custom Value 1", "Custom-Header-2" => "Custom Value 2" }
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
let(:plugin) { LogStash::Filters::Elasticsearch.new(plugin_config) }
|
305
|
+
let(:client_double) { double("client") }
|
306
|
+
let(:transport_double) { double("transport", options: { transport_options: { headers: plugin_config["custom_headers"] } }) }
|
307
|
+
|
308
|
+
before do
|
309
|
+
allow(plugin).to receive(:get_client).and_return(client_double)
|
310
|
+
if defined?(Elastic::Transport)
|
311
|
+
allow(client_double).to receive(:es_transport_client_type).and_return('elastic_transport')
|
312
|
+
else
|
313
|
+
allow(client_double).to receive(:es_transport_client_type).and_return('elasticsearch_transport')
|
314
|
+
end
|
315
|
+
allow(client_double).to receive(:client).and_return(transport_double)
|
316
|
+
end
|
317
|
+
|
318
|
+
it "sets custom headers" do
|
319
|
+
plugin.register
|
320
|
+
client = plugin.send(:get_client).client
|
321
|
+
expect(client.options[:transport_options][:headers]).to match(hash_including(plugin_config["custom_headers"]))
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
context "if query is on nested field" do
|
326
|
+
let(:plugin_config) do
|
327
|
+
{
|
328
|
+
"hosts" => ["localhost:9200"],
|
329
|
+
"query" => "response: 404",
|
330
|
+
"fields" => [ ["[geoip][ip]", "ip_address"] ]
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
it "should enhance the current event with new data" do
|
335
|
+
plugin.filter(event)
|
336
|
+
expect(event.get("ip_address")).to eq("66.249.73.185")
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
describe "#set_to_event_target" do
|
343
|
+
it 'is ready to set to `target`' do
|
344
|
+
expect(dsl_executor.apply_target("path")).to eq("path")
|
345
|
+
end
|
346
|
+
|
347
|
+
context "when `@target` is nil, default behavior" do
|
348
|
+
it "sets the value directly to the top-level event field" do
|
349
|
+
dsl_executor.send(:set_to_event_target, event, "new_field", %w[value1 value2])
|
350
|
+
expect(event.get("new_field")).to eq(%w[value1 value2])
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
context "when @target is defined" do
|
355
|
+
let(:plugin_config) {
|
356
|
+
super().merge({ "target" => "nested" })
|
357
|
+
}
|
358
|
+
|
359
|
+
it "creates a nested structure under the target field" do
|
360
|
+
dsl_executor.send(:set_to_event_target, event, "new_field", %w[value1 value2])
|
361
|
+
expect(event.get("nested")).to eq({ "new_field" => %w[value1 value2] })
|
362
|
+
end
|
363
|
+
|
364
|
+
it "overwrites existing target field with new data" do
|
365
|
+
event.set("nested", { "existing_field" => "existing_value", "new_field" => "value0" })
|
366
|
+
dsl_executor.send(:set_to_event_target, event, "new_field", ["value1"])
|
367
|
+
expect(event.get("nested")).to eq({ "existing_field" => "existing_value", "new_field" => ["value1"] })
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
end
|