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.
@@ -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
- # Elasticsearch query string. Read the Elasticsearch query string documentation.
28
- # 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
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 the result set will be nested under the target field
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
- #Load query if it exists
168
- if @query_template
169
- if File.zero?(@query_template)
170
- raise "template is empty"
171
- end
172
- file = File.open(@query_template, 'r')
173
- @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)
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
- matched = false
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
- # 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
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.2.0'
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