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.
@@ -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
@@ -0,0 +1,211 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/filters/elasticsearch"
4
+
5
+ describe LogStash::Filters::Elasticsearch::EsqlExecutor 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
+ "query_type" => "esql",
12
+ "query" => "FROM test-index | STATS count() BY field | LIMIT 10"
13
+ }
14
+ end
15
+ let(:esql_executor) { described_class.new(plugin, logger) }
16
+
17
+ context "when initializes" do
18
+ it "sets up the ESQL executor with correct parameters" do
19
+ allow(logger).to receive(:debug)
20
+ allow(logger).to receive(:warn)
21
+ expect(esql_executor.instance_variable_get(:@query)).to eq(plugin_config["query"])
22
+ expect(esql_executor.instance_variable_get(:@referenced_params)).to eq({})
23
+ expect(esql_executor.instance_variable_get(:@static_params)).to eq([])
24
+ expect(esql_executor.instance_variable_get(:@tag_on_failure)).to eq(["_elasticsearch_lookup_failure"])
25
+ end
26
+ end
27
+
28
+ context "when processes" do
29
+ let(:plugin_config) {
30
+ super()
31
+ .merge(
32
+ {
33
+ "query" => "FROM my-index | WHERE field = ?foo | LIMIT 5",
34
+ "query_params" => { "foo" => "[bar]" }
35
+ })
36
+ }
37
+ let(:event) { LogStash::Event.new({}) }
38
+ let(:response) {
39
+ {
40
+ 'values' => [["foo", "bar", nil]],
41
+ 'columns' => [{ 'name' => 'id', 'type' => 'keyword' }, { 'name' => 'val', 'type' => 'keyword' }, { 'name' => 'odd', 'type' => 'keyword' }]
42
+ }
43
+ }
44
+
45
+ before do
46
+ allow(logger).to receive(:debug)
47
+ allow(logger).to receive(:warn)
48
+ end
49
+
50
+ it "resolves parameters" do
51
+ expect(event).to receive(:get).with("[bar]").and_return("resolved_value")
52
+ resolved_params = esql_executor.send(:resolve_parameters, event)
53
+ expect(resolved_params).to include("foo" => "resolved_value")
54
+ end
55
+
56
+ it "executes the query with resolved parameters" do
57
+ allow(logger).to receive(:debug)
58
+ expect(event).to receive(:get).with("[bar]").and_return("resolved_value")
59
+ expect(client).to receive(:esql_query).with(
60
+ { body: { query: plugin_config["query"], params: [{ "foo" => "resolved_value" }] }, format: 'json', drop_null_columns: true, })
61
+ resolved_params = esql_executor.send(:resolve_parameters, event)
62
+ esql_executor.send(:execute_query, client, resolved_params)
63
+ end
64
+
65
+ it "informs warning if received warning" do
66
+ allow(response).to receive(:headers).and_return({ "warning" => "some warning" })
67
+ expect(logger).to receive(:warn).with("ES|QL executor received warning", { :message => "some warning" })
68
+ esql_executor.send(:inform_warning, response)
69
+ end
70
+
71
+ it "processes the response and adds metadata" do
72
+ expect(event).to receive(:set).with("[@metadata][total_values]", 1)
73
+ # [id], [val] aren't resolved via sprintf, use as it is
74
+ expect(event).to receive(:set).with("[id]", "foo")
75
+ expect(event).to receive(:set).with("[val]", "bar")
76
+ esql_executor.send(:process_response, event, response)
77
+ end
78
+
79
+ it "executes chain of processes" do
80
+ allow(plugin).to receive(:decorate)
81
+ allow(logger).to receive(:debug)
82
+ allow(response).to receive(:headers).and_return({})
83
+ expect(client).to receive(:esql_query).with(
84
+ {
85
+ body: { query: plugin_config["query"], params: [{"foo"=>"resolve_me"}] },
86
+ format: 'json',
87
+ drop_null_columns: true,
88
+ }).and_return(response)
89
+
90
+ event = LogStash::Event.new({ "hello" => "world", "bar" => "resolve_me" })
91
+ expect { esql_executor.process(client, event) }.to_not raise_error
92
+ expect(event.get("[@metadata][total_values]")).to eq(1)
93
+ expect(event.get("hello")).to eq("world")
94
+ expect(event.get("val")).to eq("bar")
95
+ expect(event.get("odd")).to be_nil # filters out non-exist fields
96
+ end
97
+
98
+ it "tags on plugin failures" do
99
+ expect(event).to receive(:get).with("[bar]").and_raise("Event#get Invalid FieldReference error")
100
+
101
+ expect(logger).to receive(:error).with("Failed to process ES|QL filter", exception: instance_of(RuntimeError))
102
+ expect(event).to receive(:tag).with("_elasticsearch_lookup_failure")
103
+ esql_executor.process(client, event)
104
+ end
105
+
106
+ it "tags on query execution failures" do
107
+ allow(logger).to receive(:debug)
108
+ allow(client).to receive(:esql_query).and_raise("Query execution error")
109
+
110
+ expect(logger).to receive(:error).with("Failed to process ES|QL filter", exception: instance_of(RuntimeError))
111
+ expect(event).to receive(:tag).with("_elasticsearch_lookup_failure")
112
+ esql_executor.process(client, event)
113
+ end
114
+
115
+ describe "#target" do
116
+ let(:event) { LogStash::Event.new({ "hello" => "world", "bar" => "resolve_me" }) }
117
+ let(:response) {
118
+ super().merge({ 'values' => [["foo", "bar", nil], %w[hello again world], %w[another value here]] })
119
+ }
120
+ before(:each) do
121
+ expect(client).to receive(:esql_query).with(any_args).and_return(response)
122
+ allow(plugin).to receive(:decorate)
123
+ allow(logger).to receive(:debug)
124
+ allow(response).to receive(:headers).and_return({})
125
+ end
126
+
127
+ context "when specified" do
128
+ let(:plugin_config) {
129
+ super().merge({ "target" => "my-target" })
130
+ }
131
+
132
+ it "sets all query results into event" do
133
+ expected_result = [
134
+ {"id"=>"foo", "val"=>"bar"},
135
+ {"id"=>"hello", "val"=>"again", "odd"=>"world"},
136
+ {"id"=>"another", "val"=>"value", "odd"=>"here"}
137
+ ]
138
+ expect { esql_executor.process(client, event) }.to_not raise_error
139
+ expect(event.get("[@metadata][total_values]")).to eq(3)
140
+ expect(event.get("my-target").size).to eq(3)
141
+ expect(event.get("my-target")).to eq(expected_result)
142
+ end
143
+ end
144
+
145
+ context "when not specified" do
146
+ shared_examples "first result into the event" do
147
+ it "sets" do
148
+ expect { esql_executor.process(client, event) }.to_not raise_error
149
+ expect(event.get("[@metadata][total_values]")).to eq(3)
150
+ expect(event.get("id")).to eq("foo")
151
+ expect(event.get("val")).to eq("bar")
152
+ expect(event.get("odd")).to eq(nil)
153
+ end
154
+ end
155
+ context "when limit is included in the query" do
156
+ let(:plugin_config) {
157
+ super().merge({ "query" => "FROM my-index | LIMIT 555" })
158
+ }
159
+ it_behaves_like "first result into the event"
160
+ end
161
+
162
+ context "when limit is not included in the query" do
163
+ let(:plugin_config) {
164
+ super().merge({ "query" => "FROM my-index" })
165
+ }
166
+ it_behaves_like "first result into the event"
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ describe "#query placeholders" do
173
+ before(:each) do
174
+ allow(logger).to receive(:debug)
175
+ allow(logger).to receive(:warn)
176
+ plugin.send(:validate_esql_query_and_params!)
177
+ end
178
+
179
+ context "when `query_params` is an Array contains {key => val} entries" do
180
+ let(:plugin_config) {
181
+ super()
182
+ .merge(
183
+ {
184
+ "query" => "FROM my-index | LIMIT 1",
185
+ "query_params" => [{ "a" => "b" }, { "c" => "[b]" }, { "e" => 1 }, { "f" => "[g]" }],
186
+ })
187
+ }
188
+
189
+ it "separates references and static params at initialization" do
190
+ expect(esql_executor.instance_variable_get(:@referenced_params)).to eq({"c" => "[b]", "f" => "[g]"})
191
+ expect(esql_executor.instance_variable_get(:@static_params)).to eq([{"a" => "b"}, {"e" => 1}])
192
+ end
193
+ end
194
+
195
+ context "when `query_params` is a Hash" do
196
+ let(:plugin_config) {
197
+ super()
198
+ .merge(
199
+ {
200
+ "query" => "FROM my-index | LIMIT 1",
201
+ "query_params" => { "a" => "b", "c" => "[b]", "e" => 1, "f" => "[g]" },
202
+ })
203
+ }
204
+
205
+ it "separates references and static params at initialization" do
206
+ expect(esql_executor.instance_variable_get(:@referenced_params)).to eq({"c" => "[b]", "f" => "[g]"})
207
+ expect(esql_executor.instance_variable_get(:@static_params)).to eq([{"a" => "b"}, {"e" => 1}])
208
+ end
209
+ end
210
+ end
211
+ end if LOGSTASH_VERSION >= LogStash::Filters::Elasticsearch::LS_ESQL_SUPPORT_VERSION