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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/docs/index.asciidoc +126 -6
- data/lib/logstash/inputs/elasticsearch/esql.rb +153 -0
- data/lib/logstash/inputs/elasticsearch.rb +110 -39
- data/logstash-input-elasticsearch.gemspec +2 -2
- data/spec/fixtures/test_certs/GENERATED_AT +1 -1
- data/spec/fixtures/test_certs/ca.crt +8 -8
- data/spec/fixtures/test_certs/ca.der.sha256 +1 -1
- data/spec/fixtures/test_certs/es.chain.crt +16 -16
- data/spec/fixtures/test_certs/es.crt +8 -8
- data/spec/inputs/elasticsearch_esql_spec.rb +180 -0
- data/spec/inputs/elasticsearch_spec.rb +167 -6
- data/spec/inputs/integration/elasticsearch_esql_spec.rb +150 -0
- data/version +1 -0
- metadata +26 -23
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
2
|
MIIDIzCCAgugAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylFbGFz
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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(:
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
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
|