logstash-input-elasticsearch 4.22.0 → 4.23.1

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.
@@ -1,7 +1,7 @@
1
1
  -----BEGIN CERTIFICATE-----
2
2
  MIIDIzCCAgugAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylFbGFz
3
- dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNDEyMjYy
4
- MjI3MTVaFw0yNTEyMjYyMjI3MTVaMA0xCzAJBgNVBAMTAmVzMIIBIjANBgkqhkiG
3
+ dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNjAxMjMx
4
+ NjM4MjVaFw0yNzAxMjMxNjM4MjVaMA0xCzAJBgNVBAMTAmVzMIIBIjANBgkqhkiG
5
5
  9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZLZvLSWDK7Ul+AaBnjU81dsfaow8zOjCC5V
6
6
  V21nXpYzQJoQbuWcvGYxwL7ZDs2ca4Wc8BVCj1NDduHuP7U+QIlUdQpl8kh5a0Zz
7
7
  36pcFw7UyF51/AzWixJrht/Azzkb5cpZtE22ZK0KhS4oCsjJmTN0EABAsGhDI9/c
@@ -10,17 +10,17 @@ MjNrUC7iP0dvfOuzAPp7ufY83h98jKKXUYV24snbbvmqoWI6GQQNSG/sEo1+1UGH
10
10
  hQNM3zcKKsjEMomBzVBc4SV3KXO0d/jGdDtlqsm2oXqlTMdtGwIDAQABo2cwZTAY
11
11
  BgNVHREEETAPgg1lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFQU
12
12
  K+6Cg2kExRj1xSDzEi4kkgKXMB8GA1UdIwQYMBaAFMgkye5+2l+TE0I6RsXRHjGB
13
- wpBGMA0GCSqGSIb3DQEBCwUAA4IBAQB6cZ7IrDzcAoOZgAt9RlOe2yzQeH+alttp
14
- CSQVINjJotS1WvmtqjBB6ArqLpXIGU89TZsktNe/NQJzgYSaMnlIuHVLFdxJYmwU
15
- T1cP6VC/brmqP/dd5y7VWE7Lp+Wd5CxKl/WY+9chmgc+a1fW/lnPEJJ6pca1Bo8b
16
- byIL0yY2IUv4R2eh1IyQl9oGH1GOPLgO7cY04eajxYcOVA2eDSItoyDtrJfkFP/P
17
- UXtC1JAkvWKuujFEiBj0AannhroWlp3gvChhBwCuCAU0KXD6g8BE8tn6oT1+FW7J
18
- avSfHxAe+VHtYhF8sJ8jrdm0d7E4GKS9UR/pkLAL1JuRdJ1VkPx3
13
+ wpBGMA0GCSqGSIb3DQEBCwUAA4IBAQCDFHoe8ntx/D69gb8uVQhUDI2ERb0Hocbh
14
+ BpQVidsKEv220zmZW3O9PX0aN4j3e4HUeRNSdLmtN+sXJ1xCMjHlzA5z5wYJOk2y
15
+ 4B/eCOzHoHfvR+l5NUSemZ5mk8V+gThGydPmLZBN3afjzd3NJ7NVaCA8cyKwe2jr
16
+ AMrIdGL5usoBSdbd7Ww0nWhLhqxmmV79+orCwAPJxkzsgFDK+vfIw4VHERXNoIPe
17
+ WKVBGpUCFPSfosLo9fSziNDXL71CrWZlPXt3Otip4QKQV5sZI2zxm1JtHdHX6Ul1
18
+ X7xkcUR5cOF0InxefGl9VHIZTYC5kSzAz95WV068j2Jxgu28GRP5
19
19
  -----END CERTIFICATE-----
20
20
  -----BEGIN CERTIFICATE-----
21
21
  MIIDFTCCAf2gAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylFbGFz
22
- dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNDEyMjYy
23
- MjI3MTVaFw0yNTEyMjYyMjI3MTVaMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlm
22
+ dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNjAxMjMx
23
+ NjM4MjVaFw0yNzAxMjMxNjM4MjVaMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlm
24
24
  aWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
25
25
  AQ8AMIIBCgKCAQEArUe66xG4Y2zO13gRC+rBwyvxe+c01pqV6ukw6isIbJIQWs1/
26
26
  QfEMhUwYwKs6/UXxK+VwardcA2zYwngXbGGEtms+mpUfH5CdJnrqW7lHz1BVK4yH
@@ -29,10 +29,10 @@ QfEMhUwYwKs6/UXxK+VwardcA2zYwngXbGGEtms+mpUfH5CdJnrqW7lHz1BVK4yH
29
29
  i4lUiR6Uo9D6WMFjeRYFF7GolCy/I1SzWBmmOnNhQLO5VxcNG4ldhBcapZeGwE98
30
30
  m/5lxLIwgFR9ZP8bXdxZTWLC58/LQ2NqOjA9mwIDAQABozIwMDAPBgNVHRMBAf8E
31
31
  BTADAQH/MB0GA1UdDgQWBBTIJMnuftpfkxNCOkbF0R4xgcKQRjANBgkqhkiG9w0B
32
- AQsFAAOCAQEAhfg/cmXc4Uh90yiXU8jOW8saQjTsq4ZMDQiLfJsNmNNYmHFN0vhv
33
- lJRI1STdy7+GpjS5QbrMjQIxWSS8X8xysE4Rt81IrWmLuao35TRFyoiE1seBQ5sz
34
- p/BxZUe57JvWi9dyzv2df4UfWFdGBhzdr80odZmz4i5VIv6qCKJKsGikcuLpepmp
35
- E/UKnKHeR/dFWsxzA9P2OzHTUNBMOOA2PyAUL49pwoChwJeOWN/zAgwMWLbuHFG0
36
- IN0u8swAmeH98QdvzbhiOatGNpqfTNvQEDc19yVjfXKpBVZQ79WtronYSqrbrUa1
37
- T2zD8bIVP7CdddD/UmpT1SSKh4PJxudy5Q==
32
+ AQsFAAOCAQEAgZPz/e29AepHZu8uuO6+75b6Uf88m8x5m7XL13m9+vTTSqLvCiBw
33
+ JCf5caO73iOx8AlTT2iSlKPAS/7ogik5anZziqweHmBakb+HNQcUB4Vv4VBo5Ai3
34
+ SdOm28uu4EznVLUILPUHl4FQGxhn6ba1tLEWgxlD/ynBtfcmh9UFtBqIawrpAArv
35
+ TKwGBSkcSxh/yOIo+qbyBh9EyxxlGW/Z08m9qjNqHATDxCelcTsQLhCHkvNShR4j
36
+ v1sFw5XQ8H3zQU6w4WzV81A3K8erIU8WnvdisXLk0T1VB5KURy4+UDyw44tOAPD2
37
+ vBq3gYLgcVEMYVfOWCzDy7LS49leXRzAOA==
38
38
  -----END CERTIFICATE-----
@@ -1,7 +1,7 @@
1
1
  -----BEGIN CERTIFICATE-----
2
2
  MIIDIzCCAgugAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylFbGFz
3
- dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNDEyMjYy
4
- MjI3MTVaFw0yNTEyMjYyMjI3MTVaMA0xCzAJBgNVBAMTAmVzMIIBIjANBgkqhkiG
3
+ dGljIENlcnRpZmljYXRlIFRvb2wgQXV0b2dlbmVyYXRlZCBDQTAeFw0yNjAxMjMx
4
+ NjM4MjVaFw0yNzAxMjMxNjM4MjVaMA0xCzAJBgNVBAMTAmVzMIIBIjANBgkqhkiG
5
5
  9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZLZvLSWDK7Ul+AaBnjU81dsfaow8zOjCC5V
6
6
  V21nXpYzQJoQbuWcvGYxwL7ZDs2ca4Wc8BVCj1NDduHuP7U+QIlUdQpl8kh5a0Zz
7
7
  36pcFw7UyF51/AzWixJrht/Azzkb5cpZtE22ZK0KhS4oCsjJmTN0EABAsGhDI9/c
@@ -10,10 +10,10 @@ MjNrUC7iP0dvfOuzAPp7ufY83h98jKKXUYV24snbbvmqoWI6GQQNSG/sEo1+1UGH
10
10
  hQNM3zcKKsjEMomBzVBc4SV3KXO0d/jGdDtlqsm2oXqlTMdtGwIDAQABo2cwZTAY
11
11
  BgNVHREEETAPgg1lbGFzdGljc2VhcmNoMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFQU
12
12
  K+6Cg2kExRj1xSDzEi4kkgKXMB8GA1UdIwQYMBaAFMgkye5+2l+TE0I6RsXRHjGB
13
- wpBGMA0GCSqGSIb3DQEBCwUAA4IBAQB6cZ7IrDzcAoOZgAt9RlOe2yzQeH+alttp
14
- CSQVINjJotS1WvmtqjBB6ArqLpXIGU89TZsktNe/NQJzgYSaMnlIuHVLFdxJYmwU
15
- T1cP6VC/brmqP/dd5y7VWE7Lp+Wd5CxKl/WY+9chmgc+a1fW/lnPEJJ6pca1Bo8b
16
- byIL0yY2IUv4R2eh1IyQl9oGH1GOPLgO7cY04eajxYcOVA2eDSItoyDtrJfkFP/P
17
- UXtC1JAkvWKuujFEiBj0AannhroWlp3gvChhBwCuCAU0KXD6g8BE8tn6oT1+FW7J
18
- avSfHxAe+VHtYhF8sJ8jrdm0d7E4GKS9UR/pkLAL1JuRdJ1VkPx3
13
+ wpBGMA0GCSqGSIb3DQEBCwUAA4IBAQCDFHoe8ntx/D69gb8uVQhUDI2ERb0Hocbh
14
+ BpQVidsKEv220zmZW3O9PX0aN4j3e4HUeRNSdLmtN+sXJ1xCMjHlzA5z5wYJOk2y
15
+ 4B/eCOzHoHfvR+l5NUSemZ5mk8V+gThGydPmLZBN3afjzd3NJ7NVaCA8cyKwe2jr
16
+ AMrIdGL5usoBSdbd7Ww0nWhLhqxmmV79+orCwAPJxkzsgFDK+vfIw4VHERXNoIPe
17
+ WKVBGpUCFPSfosLo9fSziNDXL71CrWZlPXt3Otip4QKQV5sZI2zxm1JtHdHX6Ul1
18
+ X7xkcUR5cOF0InxefGl9VHIZTYC5kSzAz95WV068j2Jxgu28GRP5
19
19
  -----END CERTIFICATE-----
@@ -0,0 +1,180 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/inputs/elasticsearch"
4
+ require "elasticsearch"
5
+
6
+ describe LogStash::Inputs::Elasticsearch::Esql do
7
+ let(:client) { instance_double(Elasticsearch::Client) }
8
+ let(:esql_client) { double("esql-client") }
9
+
10
+ let(:plugin) { instance_double(LogStash::Inputs::Elasticsearch, params: plugin_config, decorate_event: nil) }
11
+ let(:plugin_config) do
12
+ {
13
+ "query" => "FROM test-index | STATS count() BY field",
14
+ "retries" => 3
15
+ }
16
+ end
17
+ let(:esql_executor) { described_class.new(client, plugin) }
18
+
19
+ describe "#initialization" do
20
+ it "sets up the ESQL client with correct parameters" do
21
+ expect(esql_executor.instance_variable_get(:@query)).to eq(plugin_config["query"])
22
+ expect(esql_executor.instance_variable_get(:@retries)).to eq(plugin_config["retries"])
23
+ expect(esql_executor.instance_variable_get(:@target_field)).to eq(nil)
24
+ end
25
+ end
26
+
27
+ describe "#execution" do
28
+ let(:output_queue) { Queue.new }
29
+
30
+ context "when faces error while retrying" do
31
+ it "retries the given block the specified number of times" do
32
+ attempts = 0
33
+ result = esql_executor.retryable("Test Job") do
34
+ attempts += 1
35
+ raise StandardError if attempts < 3
36
+ "success"
37
+ end
38
+ expect(attempts).to eq(3)
39
+ expect(result).to eq("success")
40
+ end
41
+
42
+ it "returns false if the block fails all attempts" do
43
+ result = esql_executor.retryable("Test Job") do
44
+ raise StandardError
45
+ end
46
+ expect(result).to eq(false)
47
+ end
48
+ end
49
+
50
+ context "when executing chain of processes" do
51
+ let(:response) { { 'values' => [%w[foo bar]], 'columns' => [{ 'name' => 'a.b.1.d', 'type' => 'keyword' },
52
+ { 'name' => 'h_g.k$l.m.0', 'type' => 'keyword' }] } }
53
+
54
+ before do
55
+ allow(esql_executor).to receive(:retryable).and_yield
56
+ allow(client).to receive_message_chain(:esql, :query).and_return(response)
57
+ end
58
+
59
+ it "executes the ESQL query and processes the results" do
60
+ allow(response).to receive(:headers).and_return({})
61
+ esql_executor.do_run(output_queue, plugin_config["query"])
62
+ expect(output_queue.size).to eq(1)
63
+
64
+ event = output_queue.pop
65
+ expect(event.get('[a][b][1][d]')).to eq('foo')
66
+ expect(event.get('[h_g][k$l][m][0]')).to eq('bar')
67
+ end
68
+
69
+ it "logs a warning if the response contains a warning header" do
70
+ allow(response).to receive(:headers).and_return({ "warning" => "some warning" })
71
+ expect(esql_executor.logger).to receive(:warn).with("ES|QL executor received warning", { :warning_message => "some warning" })
72
+ esql_executor.do_run(output_queue, plugin_config["query"])
73
+ end
74
+
75
+ it "does not log a warning if the response does not contain a warning header" do
76
+ allow(response).to receive(:headers).and_return({})
77
+ expect(esql_executor.logger).not_to receive(:warn)
78
+ esql_executor.do_run(output_queue, plugin_config["query"])
79
+ end
80
+ end
81
+
82
+ describe "multiple rows in the result" do
83
+ let(:response) { { 'values' => rows, 'columns' => [{ 'name' => 'key.1', 'type' => 'keyword' },
84
+ { 'name' => 'key.2', 'type' => 'keyword' }] } }
85
+
86
+ before do
87
+ allow(esql_executor).to receive(:retryable).and_yield
88
+ allow(client).to receive_message_chain(:esql, :query).and_return(response)
89
+ allow(response).to receive(:headers).and_return({})
90
+ end
91
+
92
+ context "when mapping" do
93
+ let(:rows) { [%w[foo bar], %w[hello world]] }
94
+
95
+ it "1:1 maps rows to events" do
96
+ esql_executor.do_run(output_queue, plugin_config["query"])
97
+ expect(output_queue.size).to eq(2)
98
+
99
+ event_1 = output_queue.pop
100
+ expect(event_1.get('[key][1]')).to eq('foo')
101
+ expect(event_1.get('[key][2]')).to eq('bar')
102
+
103
+ event_2 = output_queue.pop
104
+ expect(event_2.get('[key][1]')).to eq('hello')
105
+ expect(event_2.get('[key][2]')).to eq('world')
106
+ end
107
+ end
108
+
109
+ context "when partial nil values appear" do
110
+ let(:rows) { [[nil, "bar"], ["hello", nil]] }
111
+
112
+ it "ignores the nil values" do
113
+ esql_executor.do_run(output_queue, plugin_config["query"])
114
+ expect(output_queue.size).to eq(2)
115
+
116
+ event_1 = output_queue.pop
117
+ expect(event_1.get('[key][1]')).to eq(nil)
118
+ expect(event_1.get('[key][2]')).to eq('bar')
119
+
120
+ event_2 = output_queue.pop
121
+ expect(event_2.get('[key][1]')).to eq('hello')
122
+ expect(event_2.get('[key][2]')).to eq(nil)
123
+ end
124
+ end
125
+ end
126
+
127
+ context "when sub-elements occur in the result" do
128
+ let(:response) { {
129
+ 'values' => [[50, 1, 100], [50, 0, 1000], [50, 9, 99999]],
130
+ 'columns' =>
131
+ [
132
+ { 'name' => 'time', 'type' => 'long' },
133
+ { 'name' => 'time.min', 'type' => 'long' },
134
+ { 'name' => 'time.max', 'type' => 'long' },
135
+ ]
136
+ } }
137
+
138
+ before do
139
+ allow(esql_executor).to receive(:retryable).and_yield
140
+ allow(client).to receive_message_chain(:esql, :query).and_return(response)
141
+ allow(response).to receive(:headers).and_return({})
142
+ end
143
+
144
+ it "includes 1st depth elements into event" do
145
+ esql_executor.do_run(output_queue, plugin_config["query"])
146
+
147
+ expect(output_queue.size).to eq(3)
148
+ 3.times do
149
+ event = output_queue.pop
150
+ expect(event.get('time')).to eq(50)
151
+ expect(event.get('[time][min]')).to eq(nil)
152
+ expect(event.get('[time][max]')).to eq(nil)
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "#column spec" do
159
+ let(:valid_spec) { { 'name' => 'field.name', 'type' => 'keyword' } }
160
+ let(:column_spec) { LogStash::Inputs::Elasticsearch::ColumnSpec.new(valid_spec) }
161
+
162
+ context "when initializes" do
163
+ it "sets the name and type attributes" do
164
+ expect(column_spec.name).to eq("field.name")
165
+ expect(column_spec.type).to eq("keyword")
166
+ end
167
+
168
+ it "freezes the name and type attributes" do
169
+ expect(column_spec.name).to be_frozen
170
+ expect(column_spec.type).to be_frozen
171
+ end
172
+ end
173
+
174
+ context "when calls the field reference" do
175
+ it "returns the correct field reference format" do
176
+ expect(column_spec.field_reference).to eq("[field][name]")
177
+ end
178
+ end
179
+ end
180
+ end if LOGSTASH_VERSION >= LogStash::Inputs::Elasticsearch::LS_ESQL_SUPPORT_VERSION
@@ -810,14 +810,50 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
810
810
  end
811
811
 
812
812
  context "with ssl" do
813
- let(:config) { super().merge({ 'api_key' => LogStash::Util::Password.new('foo:bar'), "ssl_enabled" => true }) }
813
+ let(:api_key_value) { nil }
814
+ let(:config) { super().merge("ssl_enabled" => true, 'api_key' => LogStash::Util::Password.new(api_key_value)) }
815
+ let(:encoded_api_key) { Base64.strict_encode64('foo:bar') }
814
816
 
815
- it "should set authorization" do
816
- plugin.register
817
- client = plugin.send(:client)
818
- auth_header = extract_transport(client).options[:transport_options][:headers]['Authorization']
817
+ shared_examples "a plugin that sets the ApiKey authorization header" do
818
+ it "correctly sets the Authorization header" do
819
+ plugin.register
820
+ client = plugin.send(:client)
821
+ auth_header = extract_transport(client).options[:transport_options][:headers]['Authorization']
822
+
823
+ expect(auth_header).to eql("ApiKey #{encoded_api_key}")
824
+ end
825
+ end
826
+
827
+ context "with a non-encoded API key" do
828
+ let(:api_key_value) { "foo:bar" }
829
+ it_behaves_like "a plugin that sets the ApiKey authorization header"
830
+ end
819
831
 
820
- expect( auth_header ).to eql "ApiKey #{Base64.strict_encode64('foo:bar')}"
832
+ context "with an encoded API key" do
833
+ let(:api_key_value) { encoded_api_key }
834
+ it_behaves_like "a plugin that sets the ApiKey authorization header"
835
+ end
836
+
837
+ context "with an Elastic Cloud API key (essu_ prefix)" do
838
+ # The suffix is intentionally not canonical base64: a Cloud key is opaque
839
+ # and must be forwarded verbatim regardless of its payload encoding.
840
+ let(:api_key_value) { "essu_VFZGblZreFhTekJ4ZDB4M2NHUnZRMEU2YzNWd1pYSnpaV055WlhRPQ==AAAAAAAA" }
841
+
842
+ it "sets the Authorization header verbatim without re-encoding" do
843
+ plugin.register
844
+ client = plugin.send(:client)
845
+ auth_header = extract_transport(client).options[:transport_options][:headers]['Authorization']
846
+
847
+ expect(auth_header).to eql("ApiKey #{api_key_value}")
848
+ end
849
+ end
850
+
851
+ context "with an unrecognized api_key format" do
852
+ let(:api_key_value) { "not-a-valid-key" }
853
+
854
+ it "fails registration with a configuration error" do
855
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /Invalid api_key format/)
856
+ end
821
857
  end
822
858
 
823
859
  context 'user also set' do
@@ -1357,4 +1393,129 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
1357
1393
  client.transport.respond_to?(:transport) ? client.transport.transport : client.transport
1358
1394
  end
1359
1395
 
1396
+ describe "#ESQL" do
1397
+ let(:config) do
1398
+ {
1399
+ "query" => "FROM test-index | STATS count() BY field",
1400
+ "query_type" => "esql",
1401
+ "retries" => 3
1402
+ }
1403
+ end
1404
+ let(:es_version) { LogStash::Inputs::Elasticsearch::ES_ESQL_SUPPORT_VERSION }
1405
+ let(:ls_version) { LogStash::Inputs::Elasticsearch::LS_ESQL_SUPPORT_VERSION }
1406
+
1407
+ before(:each) do
1408
+ stub_const("LOGSTASH_VERSION", ls_version)
1409
+ end
1410
+
1411
+ describe "#initialize" do
1412
+ it "sets up the ESQL client with correct parameters" do
1413
+ expect(plugin.instance_variable_get(:@query_type)).to eq(config["query_type"])
1414
+ expect(plugin.instance_variable_get(:@query)).to eq(config["query"])
1415
+ expect(plugin.instance_variable_get(:@retries)).to eq(config["retries"])
1416
+ end
1417
+ end
1418
+
1419
+ describe "#register" do
1420
+ before(:each) do
1421
+ Elasticsearch::Client.send(:define_method, :ping) { }
1422
+ allow_any_instance_of(Elasticsearch::Client).to receive(:info).and_return(cluster_info)
1423
+ end
1424
+ it "creates ES|QL executor" do
1425
+ plugin.register
1426
+ expect(plugin.instance_variable_get(:@query_executor)).to be_an_instance_of(LogStash::Inputs::Elasticsearch::Esql)
1427
+ end
1428
+ end
1429
+
1430
+ describe "#validation" do
1431
+
1432
+ describe "LS version" do
1433
+ context "when compatible" do
1434
+
1435
+ it "does not raise an error" do
1436
+ expect { plugin.send(:validate_ls_version_for_esql_support!) }.not_to raise_error
1437
+ end
1438
+ end
1439
+
1440
+ context "when incompatible" do
1441
+ before(:each) do
1442
+ stub_const("LOGSTASH_VERSION", "8.10.0")
1443
+ end
1444
+
1445
+ it "raises a runtime error" do
1446
+ expect { plugin.send(:validate_ls_version_for_esql_support!) }
1447
+ .to raise_error(RuntimeError, /Current version of Logstash does not include Elasticsearch client which supports ES|QL. Please upgrade Logstash to at least #{ls_version}/)
1448
+ end
1449
+ end
1450
+ end
1451
+
1452
+ describe "ES version" do
1453
+ before(:each) do
1454
+ allow(plugin).to receive(:es_version).and_return("8.10.5")
1455
+ end
1456
+
1457
+ context "when incompatible" do
1458
+ it "raises a runtime error" do
1459
+ expect { plugin.send(:validate_es_for_esql_support!) }
1460
+ .to raise_error(RuntimeError, /Connected Elasticsearch 8.10.5 version does not supports ES|QL. ES|QL feature requires at least Elasticsearch #{es_version} version./)
1461
+ end
1462
+ end
1463
+ end
1464
+
1465
+ context "ES|QL query and DSL params used together" do
1466
+ let(:config) {
1467
+ super().merge({
1468
+ "index" => "my-index",
1469
+ "size" => 1,
1470
+ "slices" => 1,
1471
+ "search_api" => "auto",
1472
+ "docinfo" => true,
1473
+ "docinfo_target" => "[@metadata][docinfo]",
1474
+ "docinfo_fields" => ["_index"],
1475
+ "response_type" => "hits",
1476
+ "tracking_field" => "[@metadata][tracking]"
1477
+ })}
1478
+
1479
+ it "raises a config error" do
1480
+ mixed_fields = %w[index size slices docinfo_fields response_type tracking_field]
1481
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /Configured #{mixed_fields} params are not allowed while using ES|QL query/)
1482
+ end
1483
+ end
1484
+
1485
+ describe "ES|QL query" do
1486
+ context "when query is valid" do
1487
+ it "does not raise an error" do
1488
+ expect { plugin.send(:validate_esql_query!) }.not_to raise_error
1489
+ end
1490
+ end
1491
+
1492
+ context "when query is empty" do
1493
+ let(:config) do
1494
+ {
1495
+ "query" => " "
1496
+ }
1497
+ end
1498
+
1499
+ it "raises a configuration error" do
1500
+ expect { plugin.send(:validate_esql_query!) }
1501
+ .to raise_error(LogStash::ConfigurationError, /`query` cannot be empty/)
1502
+ end
1503
+ end
1504
+
1505
+ context "when query doesn't align with ES syntax" do
1506
+ let(:config) do
1507
+ {
1508
+ "query" => "RANDOM query"
1509
+ }
1510
+ end
1511
+
1512
+ it "raises a configuration error" do
1513
+ source_commands = %w[FROM ROW SHOW]
1514
+ expect { plugin.send(:validate_esql_query!) }
1515
+ .to raise_error(LogStash::ConfigurationError, "`query` needs to start with any of #{source_commands}")
1516
+ end
1517
+ end
1518
+ end
1519
+ end
1520
+ end
1360
1521
  end
@@ -0,0 +1,150 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/inputs/elasticsearch"
4
+ require "elasticsearch"
5
+ require_relative "../../../spec/es_helper"
6
+
7
+ describe LogStash::Inputs::Elasticsearch, integration: true do
8
+
9
+ SECURE_INTEGRATION = ENV['SECURE_INTEGRATION'].eql? 'true'
10
+ ES_HOSTS = ["http#{SECURE_INTEGRATION ? 's' : nil}://#{ESHelper.get_host_port}"]
11
+
12
+ let(:plugin) { described_class.new(config) }
13
+ let(:es_index) { "logstash-esql-integration-#{rand(1000)}" }
14
+ let(:test_documents) do
15
+ [
16
+ { "message" => "test message 1", "type" => "a", "count" => 1 },
17
+ { "message" => "test message 2", "type" => "a", "count" => 2 },
18
+ { "message" => "test message 3", "type" => "b", "count" => 3 },
19
+ { "message" => "test message 4", "type" => "b", "count" => 4 },
20
+ { "message" => "test message 5", "type" => "c", "count" => 5 }
21
+ ]
22
+ end
23
+ let(:config) do
24
+ {
25
+ "hosts" => ES_HOSTS,
26
+ "query_type" => "esql"
27
+ }
28
+ end
29
+ let(:es_client) do
30
+ Elasticsearch::Client.new(hosts: ES_HOSTS)
31
+ end
32
+
33
+ before(:all) do
34
+ is_ls_with_esql_supported_client = Gem::Version.create(LOGSTASH_VERSION) >= Gem::Version.create(LogStash::Inputs::Elasticsearch::LS_ESQL_SUPPORT_VERSION)
35
+ skip "LS version does not have ES client which supports ES|QL" unless is_ls_with_esql_supported_client
36
+
37
+ # Skip tests if ES version doesn't support ES||QL
38
+ es_client = Elasticsearch::Client.new(hosts: ES_HOSTS) # need to separately create since let isn't allowed in before(:context)
39
+ es_version_info = es_client.info["version"]
40
+ es_gem_version = Gem::Version.create(es_version_info["number"])
41
+ skip "ES version does not support ES|QL" if es_gem_version.nil? || es_gem_version < Gem::Version.create(LogStash::Inputs::Elasticsearch::ES_ESQL_SUPPORT_VERSION)
42
+ end
43
+
44
+ before(:each) do
45
+ # Create index with test documents
46
+ es_client.indices.create(index: es_index, body: {}) unless es_client.indices.exists?(index: es_index)
47
+
48
+ test_documents.each do |doc|
49
+ es_client.index(index: es_index, body: doc, refresh: true)
50
+ end
51
+ end
52
+
53
+ after(:each) do
54
+ es_client.indices.delete(index: es_index) if es_client.indices.exists?(index: es_index)
55
+ end
56
+
57
+ context "#run ES|QL queries" do
58
+
59
+ before do
60
+ stub_const("LOGSTASH_VERSION", LogStash::Inputs::Elasticsearch::LS_ESQL_SUPPORT_VERSION)
61
+ allow_any_instance_of(LogStash::Inputs::Elasticsearch).to receive(:exit_plugin?).and_return false, true
62
+ end
63
+
64
+ before(:each) do
65
+ plugin.register
66
+ end
67
+
68
+ shared_examples "ESQL query execution" do |expected_count|
69
+ it "correctly retrieves documents" do
70
+ queue = Queue.new
71
+ plugin.run(queue)
72
+
73
+ event_count = 0
74
+ expected_count.times do |i|
75
+ event = queue.pop
76
+ expect(event).to be_a(LogStash::Event)
77
+ event_count += 1
78
+ end
79
+ expect(event_count).to eq(expected_count)
80
+ end
81
+ end
82
+
83
+ context "#FROM query" do
84
+ let(:config) do
85
+ super().merge("query" => "FROM #{es_index} | SORT count")
86
+ end
87
+
88
+ include_examples "ESQL query execution", 5
89
+ end
90
+
91
+ context "#FROM query and WHERE clause" do
92
+ let(:config) do
93
+ super().merge("query" => "FROM #{es_index} | WHERE type == \"a\" | SORT count")
94
+ end
95
+
96
+ include_examples "ESQL query execution", 2
97
+ end
98
+
99
+ context "#STATS aggregation" do
100
+ let(:config) do
101
+ super().merge("query" => "FROM #{es_index} | STATS avg(count) BY type")
102
+ end
103
+
104
+ it "retrieves aggregated stats" do
105
+ queue = Queue.new
106
+ plugin.run(queue)
107
+ results = []
108
+ 3.times do
109
+ event = queue.pop
110
+ expect(event).to be_a(LogStash::Event)
111
+ results << event.get("avg(count)")
112
+ end
113
+
114
+ expected_averages = [1.5, 3.5, 5.0]
115
+ expect(results.sort).to eq(expected_averages)
116
+ end
117
+ end
118
+
119
+ context "#METADATA" do
120
+ let(:config) do
121
+ super().merge("query" => "FROM #{es_index} METADATA _index, _id, _version | DROP message.keyword, type.keyword | SORT count")
122
+ end
123
+
124
+ it "includes document metadata" do
125
+ queue = Queue.new
126
+ plugin.run(queue)
127
+
128
+ 5.times do
129
+ event = queue.pop
130
+ expect(event).to be_a(LogStash::Event)
131
+ expect(event.get("_index")).not_to be_nil
132
+ expect(event.get("_id")).not_to be_nil
133
+ expect(event.get("_version")).not_to be_nil
134
+ end
135
+ end
136
+ end
137
+
138
+ context "#invalid ES|QL query" do
139
+ let(:config) do
140
+ super().merge("query" => "FROM undefined index | LIMIT 1")
141
+ end
142
+
143
+ it "doesn't produce events" do
144
+ queue = Queue.new
145
+ plugin.run(queue)
146
+ expect(queue.empty?).to eq(true)
147
+ end
148
+ end
149
+ end
150
+ end
data/version ADDED
@@ -0,0 +1 @@
1
+ 4.23.1