logstash-input-elasticsearch 4.23.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -15
- data/docs/index.asciidoc +23 -355
- data/lib/logstash/inputs/elasticsearch/aggregation.rb +8 -11
- data/lib/logstash/inputs/elasticsearch/paginated_search.rb +2 -12
- data/lib/logstash/inputs/elasticsearch.rb +47 -205
- data/logstash-input-elasticsearch.gemspec +3 -3
- data/spec/fixtures/test_certs/ca.crt +18 -17
- data/spec/fixtures/test_certs/ca.der.sha256 +1 -1
- data/spec/fixtures/test_certs/es.crt +18 -17
- data/spec/inputs/elasticsearch_spec.rb +22 -251
- data/spec/inputs/integration/elasticsearch_spec.rb +2 -10
- metadata +3 -23
- data/lib/logstash/inputs/elasticsearch/cursor_tracker.rb +0 -58
- data/lib/logstash/inputs/elasticsearch/esql.rb +0 -153
- data/spec/fixtures/test_certs/GENERATED_AT +0 -1
- data/spec/fixtures/test_certs/es.chain.crt +0 -38
- data/spec/fixtures/test_certs/renew.sh +0 -15
- data/spec/inputs/cursor_tracker_spec.rb +0 -72
- data/spec/inputs/elasticsearch_esql_spec.rb +0 -180
- data/spec/inputs/integration/elasticsearch_esql_spec.rb +0 -150
@@ -21,13 +21,6 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
21
21
|
let(:es_version) { "7.5.0" }
|
22
22
|
let(:cluster_info) { {"version" => {"number" => es_version, "build_flavor" => build_flavor}, "tagline" => "You Know, for Search"} }
|
23
23
|
|
24
|
-
def elastic_ruby_v8_client_available?
|
25
|
-
Elasticsearch::Transport
|
26
|
-
false
|
27
|
-
rescue NameError # NameError: uninitialized constant Elasticsearch::Transport if Elastic Ruby client is not available
|
28
|
-
true
|
29
|
-
end
|
30
|
-
|
31
24
|
before(:each) do
|
32
25
|
Elasticsearch::Client.send(:define_method, :ping) { } # define no-action ping method
|
33
26
|
allow_any_instance_of(Elasticsearch::Client).to receive(:info).and_return(cluster_info)
|
@@ -65,6 +58,19 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
65
58
|
end
|
66
59
|
end
|
67
60
|
|
61
|
+
describe 'handling obsolete settings' do
|
62
|
+
[{:name => 'ssl', :replacement => 'ssl_enabled', :sample_value => true},
|
63
|
+
{:name => 'ca_file', :replacement => 'ssl_certificate_authorities', :sample_value => 'spec/fixtures/test_certs/ca.crt'},
|
64
|
+
{:name => 'ssl_certificate_verification', :replacement => 'ssl_verification_mode', :sample_value => false }].each do | obsolete_setting|
|
65
|
+
context "with obsolete #{obsolete_setting[:name]}" do
|
66
|
+
let (:config) { {obsolete_setting[:name] => obsolete_setting[:sample_value]} }
|
67
|
+
it "should raise a config error with the appropriate message" do
|
68
|
+
expect { plugin.register }.to raise_error LogStash::ConfigurationError, /The setting `#{obsolete_setting[:name]}` in plugin `elasticsearch` is obsolete and is no longer available. Set '#{obsolete_setting[:replacement]}' instead/i
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
68
74
|
context "against not authentic Elasticsearch" do
|
69
75
|
before(:each) do
|
70
76
|
Elasticsearch::Client.send(:define_method, :ping) { raise Elasticsearch::UnsupportedProductError.new("Fake error") } # define error ping method
|
@@ -86,11 +92,9 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
86
92
|
|
87
93
|
before do
|
88
94
|
allow(Elasticsearch::Client).to receive(:new).and_return(es_client)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
allow(es_client).to receive(:info).and_raise(Elasticsearch::Transport::Transport::Errors::BadRequest.new)
|
93
|
-
end
|
95
|
+
allow(es_client).to receive(:info).and_raise(
|
96
|
+
Elasticsearch::Transport::Transport::Errors::BadRequest.new
|
97
|
+
)
|
94
98
|
end
|
95
99
|
|
96
100
|
it "raises an exception" do
|
@@ -662,28 +666,11 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
662
666
|
context 'if the `docinfo_target` exist but is not of type hash' do
|
663
667
|
let(:config) { base_config.merge 'docinfo' => true, "docinfo_target" => 'metadata_with_string' }
|
664
668
|
let(:do_register) { false }
|
665
|
-
let(:mock_queue) { double('Queue', :<< => nil) }
|
666
|
-
let(:hit) { response.dig('hits', 'hits').first }
|
667
|
-
|
668
|
-
it 'emits a tagged event with JSON-serialized event in [event][original]' do
|
669
|
-
allow(plugin).to receive(:logger).and_return(double('Logger').as_null_object)
|
670
669
|
|
670
|
+
it 'raises an exception if the `docinfo_target` exist but is not of type hash' do
|
671
|
+
expect(client).not_to receive(:clear_scroll)
|
671
672
|
plugin.register
|
672
|
-
plugin.run(
|
673
|
-
|
674
|
-
expect(mock_queue).to have_received(:<<) do |event|
|
675
|
-
expect(event).to be_a_kind_of LogStash::Event
|
676
|
-
|
677
|
-
expect(event.get('tags')).to include("_elasticsearch_input_failure")
|
678
|
-
expect(event.get('[event][original]')).to be_a_kind_of String
|
679
|
-
expect(JSON.load(event.get('[event][original]'))).to eq hit
|
680
|
-
end
|
681
|
-
|
682
|
-
expect(plugin.logger)
|
683
|
-
.to have_received(:warn).with(
|
684
|
-
a_string_including("Event creation error, original data now in [event][original] field"),
|
685
|
-
a_hash_including(:message => a_string_including('unable to merge docinfo fields into docinfo_target=`metadata_with_string`'),
|
686
|
-
:data => a_string_including('"_id":"C5b2xLQwTZa76jBmHIbwHQ"')))
|
673
|
+
expect { plugin.run([]) }.to raise_error(Exception, /incompatible event/)
|
687
674
|
end
|
688
675
|
|
689
676
|
end
|
@@ -740,13 +727,8 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
740
727
|
it "should set host(s)" do
|
741
728
|
plugin.register
|
742
729
|
client = plugin.send(:client)
|
743
|
-
|
744
|
-
|
745
|
-
Elasticsearch::Transport::Client
|
746
|
-
rescue
|
747
|
-
target_field = :@hosts
|
748
|
-
end
|
749
|
-
expect( client.transport.instance_variable_get(target_field) ).to eql [{
|
730
|
+
|
731
|
+
expect( client.transport.instance_variable_get(:@seeds) ).to eql [{
|
750
732
|
:scheme => "https",
|
751
733
|
:host => "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io",
|
752
734
|
:port => 9243,
|
@@ -1152,7 +1134,7 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
1152
1134
|
|
1153
1135
|
context "when there's an exception" do
|
1154
1136
|
before(:each) do
|
1155
|
-
allow(client).to receive(:search).and_raise RuntimeError
|
1137
|
+
allow(client).to receive(:search).and_raise RuntimeError
|
1156
1138
|
end
|
1157
1139
|
it 'produces no events' do
|
1158
1140
|
plugin.run queue
|
@@ -1266,220 +1248,9 @@ describe LogStash::Inputs::Elasticsearch, :ecs_compatibility_support do
|
|
1266
1248
|
end
|
1267
1249
|
end
|
1268
1250
|
|
1269
|
-
context '#push_hit' do
|
1270
|
-
let(:config) do
|
1271
|
-
{
|
1272
|
-
'docinfo' => true, # include ids
|
1273
|
-
'docinfo_target' => '[@metadata][docinfo]'
|
1274
|
-
}
|
1275
|
-
end
|
1276
|
-
|
1277
|
-
let(:hit) do
|
1278
|
-
JSON.load(<<~EOJSON)
|
1279
|
-
{
|
1280
|
-
"_index" : "test_bulk_index_2",
|
1281
|
-
"_type" : "_doc",
|
1282
|
-
"_id" : "sHe6A3wBesqF7ydicQvG",
|
1283
|
-
"_score" : 1.0,
|
1284
|
-
"_source" : {
|
1285
|
-
"@timestamp" : "2021-09-20T15:02:02.557Z",
|
1286
|
-
"message" : "ping",
|
1287
|
-
"@version" : "17",
|
1288
|
-
"sequence" : 7,
|
1289
|
-
"host" : {
|
1290
|
-
"name" : "maybe.local",
|
1291
|
-
"ip" : "127.0.0.1"
|
1292
|
-
}
|
1293
|
-
}
|
1294
|
-
}
|
1295
|
-
EOJSON
|
1296
|
-
end
|
1297
|
-
|
1298
|
-
let(:mock_queue) { double('queue', :<< => nil) }
|
1299
|
-
|
1300
|
-
before(:each) do
|
1301
|
-
plugin.send(:setup_cursor_tracker)
|
1302
|
-
end
|
1303
|
-
|
1304
|
-
it 'pushes a generated event to the queue' do
|
1305
|
-
plugin.send(:push_hit, hit, mock_queue)
|
1306
|
-
expect(mock_queue).to have_received(:<<) do |event|
|
1307
|
-
expect(event).to be_a_kind_of LogStash::Event
|
1308
|
-
|
1309
|
-
# fields overriding defaults
|
1310
|
-
expect(event.timestamp.to_s).to eq("2021-09-20T15:02:02.557Z")
|
1311
|
-
expect(event.get('@version')).to eq("17")
|
1312
|
-
|
1313
|
-
# structure from hit's _source
|
1314
|
-
expect(event.get('message')).to eq("ping")
|
1315
|
-
expect(event.get('sequence')).to eq(7)
|
1316
|
-
expect(event.get('[host][name]')).to eq("maybe.local")
|
1317
|
-
expect(event.get('[host][ip]')).to eq("127.0.0.1")
|
1318
|
-
|
1319
|
-
# docinfo fields
|
1320
|
-
expect(event.get('[@metadata][docinfo][_index]')).to eq("test_bulk_index_2")
|
1321
|
-
expect(event.get('[@metadata][docinfo][_type]')).to eq("_doc")
|
1322
|
-
expect(event.get('[@metadata][docinfo][_id]')).to eq("sHe6A3wBesqF7ydicQvG")
|
1323
|
-
end
|
1324
|
-
end
|
1325
|
-
|
1326
|
-
context 'when event creation fails' do
|
1327
|
-
before(:each) do
|
1328
|
-
allow(plugin).to receive(:logger).and_return(double('Logger').as_null_object)
|
1329
|
-
|
1330
|
-
allow(plugin.event_factory).to receive(:new_event).and_call_original
|
1331
|
-
allow(plugin.event_factory).to receive(:new_event).with(a_hash_including hit['_source']).and_raise(RuntimeError, 'intentional')
|
1332
|
-
end
|
1333
|
-
|
1334
|
-
it 'pushes a tagged event containing a JSON-encoded hit in [event][original]' do
|
1335
|
-
plugin.send(:push_hit, hit, mock_queue)
|
1336
|
-
|
1337
|
-
expect(mock_queue).to have_received(:<<) do |event|
|
1338
|
-
expect(event).to be_a_kind_of LogStash::Event
|
1339
|
-
|
1340
|
-
expect(event.get('tags')).to include("_elasticsearch_input_failure")
|
1341
|
-
expect(event.get('[event][original]')).to be_a_kind_of String
|
1342
|
-
expect(JSON.load(event.get('[event][original]'))).to eq hit
|
1343
|
-
end
|
1344
|
-
|
1345
|
-
expect(plugin.logger)
|
1346
|
-
.to have_received(:warn).with(
|
1347
|
-
a_string_including("Event creation error, original data now in [event][original] field"),
|
1348
|
-
a_hash_including(:message => a_string_including('intentional'),
|
1349
|
-
:data => a_string_including('"_id":"sHe6A3wBesqF7ydicQvG"')))
|
1350
|
-
|
1351
|
-
end
|
1352
|
-
end
|
1353
|
-
end
|
1354
|
-
|
1355
1251
|
# @note can be removed once we depends on elasticsearch gem >= 6.x
|
1356
1252
|
def extract_transport(client) # on 7.x client.transport is a ES::Transport::Client
|
1357
1253
|
client.transport.respond_to?(:transport) ? client.transport.transport : client.transport
|
1358
1254
|
end
|
1359
1255
|
|
1360
|
-
describe "#ESQL" do
|
1361
|
-
let(:config) do
|
1362
|
-
{
|
1363
|
-
"query" => "FROM test-index | STATS count() BY field",
|
1364
|
-
"query_type" => "esql",
|
1365
|
-
"retries" => 3
|
1366
|
-
}
|
1367
|
-
end
|
1368
|
-
let(:es_version) { LogStash::Inputs::Elasticsearch::ES_ESQL_SUPPORT_VERSION }
|
1369
|
-
let(:ls_version) { LogStash::Inputs::Elasticsearch::LS_ESQL_SUPPORT_VERSION }
|
1370
|
-
|
1371
|
-
before(:each) do
|
1372
|
-
stub_const("LOGSTASH_VERSION", ls_version)
|
1373
|
-
end
|
1374
|
-
|
1375
|
-
describe "#initialize" do
|
1376
|
-
it "sets up the ESQL client with correct parameters" do
|
1377
|
-
expect(plugin.instance_variable_get(:@query_type)).to eq(config["query_type"])
|
1378
|
-
expect(plugin.instance_variable_get(:@query)).to eq(config["query"])
|
1379
|
-
expect(plugin.instance_variable_get(:@retries)).to eq(config["retries"])
|
1380
|
-
end
|
1381
|
-
end
|
1382
|
-
|
1383
|
-
describe "#register" do
|
1384
|
-
before(:each) do
|
1385
|
-
Elasticsearch::Client.send(:define_method, :ping) { }
|
1386
|
-
allow_any_instance_of(Elasticsearch::Client).to receive(:info).and_return(cluster_info)
|
1387
|
-
end
|
1388
|
-
it "creates ES|QL executor" do
|
1389
|
-
plugin.register
|
1390
|
-
expect(plugin.instance_variable_get(:@query_executor)).to be_an_instance_of(LogStash::Inputs::Elasticsearch::Esql)
|
1391
|
-
end
|
1392
|
-
end
|
1393
|
-
|
1394
|
-
describe "#validation" do
|
1395
|
-
|
1396
|
-
describe "LS version" do
|
1397
|
-
context "when compatible" do
|
1398
|
-
|
1399
|
-
it "does not raise an error" do
|
1400
|
-
expect { plugin.send(:validate_ls_version_for_esql_support!) }.not_to raise_error
|
1401
|
-
end
|
1402
|
-
end
|
1403
|
-
|
1404
|
-
context "when incompatible" do
|
1405
|
-
before(:each) do
|
1406
|
-
stub_const("LOGSTASH_VERSION", "8.10.0")
|
1407
|
-
end
|
1408
|
-
|
1409
|
-
it "raises a runtime error" do
|
1410
|
-
expect { plugin.send(:validate_ls_version_for_esql_support!) }
|
1411
|
-
.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}/)
|
1412
|
-
end
|
1413
|
-
end
|
1414
|
-
end
|
1415
|
-
|
1416
|
-
describe "ES version" do
|
1417
|
-
before(:each) do
|
1418
|
-
allow(plugin).to receive(:es_version).and_return("8.10.5")
|
1419
|
-
end
|
1420
|
-
|
1421
|
-
context "when incompatible" do
|
1422
|
-
it "raises a runtime error" do
|
1423
|
-
expect { plugin.send(:validate_es_for_esql_support!) }
|
1424
|
-
.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./)
|
1425
|
-
end
|
1426
|
-
end
|
1427
|
-
end
|
1428
|
-
|
1429
|
-
context "ES|QL query and DSL params used together" do
|
1430
|
-
let(:config) {
|
1431
|
-
super().merge({
|
1432
|
-
"index" => "my-index",
|
1433
|
-
"size" => 1,
|
1434
|
-
"slices" => 1,
|
1435
|
-
"search_api" => "auto",
|
1436
|
-
"docinfo" => true,
|
1437
|
-
"docinfo_target" => "[@metadata][docinfo]",
|
1438
|
-
"docinfo_fields" => ["_index"],
|
1439
|
-
"response_type" => "hits",
|
1440
|
-
"tracking_field" => "[@metadata][tracking]"
|
1441
|
-
})}
|
1442
|
-
|
1443
|
-
it "raises a config error" do
|
1444
|
-
mixed_fields = %w[index size slices docinfo_fields response_type tracking_field]
|
1445
|
-
expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /Configured #{mixed_fields} params are not allowed while using ES|QL query/)
|
1446
|
-
end
|
1447
|
-
end
|
1448
|
-
|
1449
|
-
describe "ES|QL query" do
|
1450
|
-
context "when query is valid" do
|
1451
|
-
it "does not raise an error" do
|
1452
|
-
expect { plugin.send(:validate_esql_query!) }.not_to raise_error
|
1453
|
-
end
|
1454
|
-
end
|
1455
|
-
|
1456
|
-
context "when query is empty" do
|
1457
|
-
let(:config) do
|
1458
|
-
{
|
1459
|
-
"query" => " "
|
1460
|
-
}
|
1461
|
-
end
|
1462
|
-
|
1463
|
-
it "raises a configuration error" do
|
1464
|
-
expect { plugin.send(:validate_esql_query!) }
|
1465
|
-
.to raise_error(LogStash::ConfigurationError, /`query` cannot be empty/)
|
1466
|
-
end
|
1467
|
-
end
|
1468
|
-
|
1469
|
-
context "when query doesn't align with ES syntax" do
|
1470
|
-
let(:config) do
|
1471
|
-
{
|
1472
|
-
"query" => "RANDOM query"
|
1473
|
-
}
|
1474
|
-
end
|
1475
|
-
|
1476
|
-
it "raises a configuration error" do
|
1477
|
-
source_commands = %w[FROM ROW SHOW]
|
1478
|
-
expect { plugin.send(:validate_esql_query!) }
|
1479
|
-
.to raise_error(LogStash::ConfigurationError, "`query` needs to start with any of #{source_commands}")
|
1480
|
-
end
|
1481
|
-
end
|
1482
|
-
end
|
1483
|
-
end
|
1484
|
-
end
|
1485
1256
|
end
|
@@ -4,7 +4,7 @@ require "logstash/plugin"
|
|
4
4
|
require "logstash/inputs/elasticsearch"
|
5
5
|
require_relative "../../../spec/es_helper"
|
6
6
|
|
7
|
-
describe LogStash::Inputs::Elasticsearch do
|
7
|
+
describe LogStash::Inputs::Elasticsearch, :integration => true do
|
8
8
|
|
9
9
|
SECURE_INTEGRATION = ENV['SECURE_INTEGRATION'].eql? 'true'
|
10
10
|
|
@@ -76,14 +76,6 @@ describe LogStash::Inputs::Elasticsearch do
|
|
76
76
|
shared_examples 'secured_elasticsearch' do
|
77
77
|
it_behaves_like 'an elasticsearch index plugin'
|
78
78
|
|
79
|
-
let(:unauth_exception_class) do
|
80
|
-
begin
|
81
|
-
Elasticsearch::Transport::Transport::Errors::Unauthorized
|
82
|
-
rescue
|
83
|
-
Elastic::Transport::Transport::Errors::Unauthorized
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
79
|
context "incorrect auth credentials" do
|
88
80
|
|
89
81
|
let(:config) do
|
@@ -93,7 +85,7 @@ describe LogStash::Inputs::Elasticsearch do
|
|
93
85
|
let(:queue) { [] }
|
94
86
|
|
95
87
|
it "fails to run the plugin" do
|
96
|
-
expect { plugin.register }.to raise_error
|
88
|
+
expect { plugin.register }.to raise_error Elasticsearch::Transport::Transport::Errors::Unauthorized
|
97
89
|
end
|
98
90
|
end
|
99
91
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-input-elasticsearch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,9 +92,6 @@ dependencies:
|
|
92
92
|
- - ">="
|
93
93
|
- !ruby/object:Gem::Version
|
94
94
|
version: 7.17.9
|
95
|
-
- - "<"
|
96
|
-
- !ruby/object:Gem::Version
|
97
|
-
version: '9'
|
98
95
|
name: elasticsearch
|
99
96
|
type: :runtime
|
100
97
|
prerelease: false
|
@@ -103,9 +100,6 @@ dependencies:
|
|
103
100
|
- - ">="
|
104
101
|
- !ruby/object:Gem::Version
|
105
102
|
version: 7.17.9
|
106
|
-
- - "<"
|
107
|
-
- !ruby/object:Gem::Version
|
108
|
-
version: '9'
|
109
103
|
- !ruby/object:Gem::Dependency
|
110
104
|
requirement: !ruby/object:Gem::Requirement
|
111
105
|
requirements:
|
@@ -278,29 +272,21 @@ files:
|
|
278
272
|
- lib/logstash/helpers/loggable_try.rb
|
279
273
|
- lib/logstash/inputs/elasticsearch.rb
|
280
274
|
- lib/logstash/inputs/elasticsearch/aggregation.rb
|
281
|
-
- lib/logstash/inputs/elasticsearch/cursor_tracker.rb
|
282
|
-
- lib/logstash/inputs/elasticsearch/esql.rb
|
283
275
|
- lib/logstash/inputs/elasticsearch/paginated_search.rb
|
284
276
|
- lib/logstash/inputs/elasticsearch/patches/_elasticsearch_transport_connections_selector.rb
|
285
277
|
- lib/logstash/inputs/elasticsearch/patches/_elasticsearch_transport_http_manticore.rb
|
286
278
|
- logstash-input-elasticsearch.gemspec
|
287
279
|
- spec/es_helper.rb
|
288
|
-
- spec/fixtures/test_certs/GENERATED_AT
|
289
280
|
- spec/fixtures/test_certs/ca.crt
|
290
281
|
- spec/fixtures/test_certs/ca.der.sha256
|
291
282
|
- spec/fixtures/test_certs/ca.key
|
292
|
-
- spec/fixtures/test_certs/es.chain.crt
|
293
283
|
- spec/fixtures/test_certs/es.crt
|
294
284
|
- spec/fixtures/test_certs/es.key
|
295
|
-
- spec/fixtures/test_certs/renew.sh
|
296
|
-
- spec/inputs/cursor_tracker_spec.rb
|
297
|
-
- spec/inputs/elasticsearch_esql_spec.rb
|
298
285
|
- spec/inputs/elasticsearch_spec.rb
|
299
286
|
- spec/inputs/elasticsearch_ssl_spec.rb
|
300
|
-
- spec/inputs/integration/elasticsearch_esql_spec.rb
|
301
287
|
- spec/inputs/integration/elasticsearch_spec.rb
|
302
288
|
- spec/inputs/paginated_search_spec.rb
|
303
|
-
homepage:
|
289
|
+
homepage: http://www.elastic.co/guide/en/logstash/current/index.html
|
304
290
|
licenses:
|
305
291
|
- Apache License (2.0)
|
306
292
|
metadata:
|
@@ -327,18 +313,12 @@ specification_version: 4
|
|
327
313
|
summary: Reads query results from an Elasticsearch cluster
|
328
314
|
test_files:
|
329
315
|
- spec/es_helper.rb
|
330
|
-
- spec/fixtures/test_certs/GENERATED_AT
|
331
316
|
- spec/fixtures/test_certs/ca.crt
|
332
317
|
- spec/fixtures/test_certs/ca.der.sha256
|
333
318
|
- spec/fixtures/test_certs/ca.key
|
334
|
-
- spec/fixtures/test_certs/es.chain.crt
|
335
319
|
- spec/fixtures/test_certs/es.crt
|
336
320
|
- spec/fixtures/test_certs/es.key
|
337
|
-
- spec/fixtures/test_certs/renew.sh
|
338
|
-
- spec/inputs/cursor_tracker_spec.rb
|
339
|
-
- spec/inputs/elasticsearch_esql_spec.rb
|
340
321
|
- spec/inputs/elasticsearch_spec.rb
|
341
322
|
- spec/inputs/elasticsearch_ssl_spec.rb
|
342
|
-
- spec/inputs/integration/elasticsearch_esql_spec.rb
|
343
323
|
- spec/inputs/integration/elasticsearch_spec.rb
|
344
324
|
- spec/inputs/paginated_search_spec.rb
|
@@ -1,58 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
|
3
|
-
module LogStash; module Inputs; class Elasticsearch
|
4
|
-
class CursorTracker
|
5
|
-
include LogStash::Util::Loggable
|
6
|
-
|
7
|
-
attr_reader :last_value
|
8
|
-
|
9
|
-
def initialize(last_run_metadata_path:, tracking_field:, tracking_field_seed:)
|
10
|
-
@last_run_metadata_path = last_run_metadata_path
|
11
|
-
@last_value_hashmap = Java::java.util.concurrent.ConcurrentHashMap.new
|
12
|
-
@last_value = IO.read(@last_run_metadata_path) rescue nil || tracking_field_seed
|
13
|
-
@tracking_field = tracking_field
|
14
|
-
logger.info "Starting value for cursor field \"#{@tracking_field}\": #{@last_value}"
|
15
|
-
@mutex = Mutex.new
|
16
|
-
end
|
17
|
-
|
18
|
-
def checkpoint_cursor(intermediate: true)
|
19
|
-
@mutex.synchronize do
|
20
|
-
if intermediate
|
21
|
-
# in intermediate checkpoints pick the smallest
|
22
|
-
converge_last_value {|v1, v2| v1 < v2 ? v1 : v2}
|
23
|
-
else
|
24
|
-
# in the last search of a PIT choose the largest
|
25
|
-
converge_last_value {|v1, v2| v1 > v2 ? v1 : v2}
|
26
|
-
@last_value_hashmap.clear
|
27
|
-
end
|
28
|
-
IO.write(@last_run_metadata_path, @last_value)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def converge_last_value(&block)
|
33
|
-
return if @last_value_hashmap.empty?
|
34
|
-
new_last_value = @last_value_hashmap.reduceValues(1000, &block)
|
35
|
-
logger.debug? && logger.debug("converge_last_value: got #{@last_value_hashmap.values.inspect}. won: #{new_last_value}")
|
36
|
-
return if new_last_value == @last_value
|
37
|
-
@last_value = new_last_value
|
38
|
-
logger.info "New cursor value for field \"#{@tracking_field}\" is: #{new_last_value}"
|
39
|
-
end
|
40
|
-
|
41
|
-
def record_last_value(event)
|
42
|
-
value = event.get(@tracking_field)
|
43
|
-
logger.trace? && logger.trace("storing last_value if #{@tracking_field} for #{Thread.current.object_id}: #{value}")
|
44
|
-
@last_value_hashmap.put(Thread.current.object_id, value)
|
45
|
-
end
|
46
|
-
|
47
|
-
def inject_cursor(query_json)
|
48
|
-
# ":present" means "now - 30s" to avoid grabbing partially visible data in the PIT
|
49
|
-
result = query_json.gsub(":last_value", @last_value.to_s).gsub(":present", now_minus_30s)
|
50
|
-
logger.debug("inject_cursor: injected values for ':last_value' and ':present'", :query => result)
|
51
|
-
result
|
52
|
-
end
|
53
|
-
|
54
|
-
def now_minus_30s
|
55
|
-
Java::java.time.Instant.now.minusSeconds(30).to_s
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end; end; end
|
@@ -1,153 +0,0 @@
|
|
1
|
-
require 'logstash/helpers/loggable_try'
|
2
|
-
|
3
|
-
module LogStash
|
4
|
-
module Inputs
|
5
|
-
class Elasticsearch
|
6
|
-
class Esql
|
7
|
-
include LogStash::Util::Loggable
|
8
|
-
|
9
|
-
ESQL_JOB = "ES|QL job"
|
10
|
-
|
11
|
-
ESQL_PARSERS_BY_TYPE = Hash.new(lambda { |x| x }).merge(
|
12
|
-
'date' => ->(value) { value && LogStash::Timestamp.new(value) },
|
13
|
-
)
|
14
|
-
|
15
|
-
# Initialize the ESQL query executor
|
16
|
-
# @param client [Elasticsearch::Client] The Elasticsearch client instance
|
17
|
-
# @param plugin [LogStash::Inputs::Elasticsearch] The parent plugin instance
|
18
|
-
def initialize(client, plugin)
|
19
|
-
@client = client
|
20
|
-
@event_decorator = plugin.method(:decorate_event)
|
21
|
-
@retries = plugin.params["retries"]
|
22
|
-
|
23
|
-
target_field = plugin.params["target"]
|
24
|
-
if target_field
|
25
|
-
def self.apply_target(path); "[#{target_field}][#{path}]"; end
|
26
|
-
else
|
27
|
-
def self.apply_target(path); path; end
|
28
|
-
end
|
29
|
-
|
30
|
-
@query = plugin.params["query"]
|
31
|
-
unless @query.include?('METADATA')
|
32
|
-
logger.info("`METADATA` not found the query. `_id`, `_version` and `_index` will not be available in the result", {:query => @query})
|
33
|
-
end
|
34
|
-
logger.debug("ES|QL executor initialized with", {:query => @query})
|
35
|
-
end
|
36
|
-
|
37
|
-
# Execute the ESQL query and process results
|
38
|
-
# @param output_queue [Queue] The queue to push processed events to
|
39
|
-
# @param query A query (to obey interface definition)
|
40
|
-
def do_run(output_queue, query)
|
41
|
-
logger.info("ES|QL executor has started")
|
42
|
-
response = retryable(ESQL_JOB) do
|
43
|
-
@client.esql.query({ body: { query: @query }, format: 'json', drop_null_columns: true })
|
44
|
-
end
|
45
|
-
# retriable already printed error details
|
46
|
-
return if response == false
|
47
|
-
|
48
|
-
if response&.headers&.dig("warning")
|
49
|
-
logger.warn("ES|QL executor received warning", {:warning_message => response.headers["warning"]})
|
50
|
-
end
|
51
|
-
columns = response['columns']&.freeze
|
52
|
-
values = response['values']&.freeze
|
53
|
-
logger.debug("ES|QL query response size: #{values&.size}")
|
54
|
-
|
55
|
-
process_response(columns, values, output_queue) if columns && values
|
56
|
-
end
|
57
|
-
|
58
|
-
# Execute a retryable operation with proper error handling
|
59
|
-
# @param job_name [String] Name of the job for logging purposes
|
60
|
-
# @yield The block to execute
|
61
|
-
# @return [Boolean] true if successful, false otherwise
|
62
|
-
def retryable(job_name, &block)
|
63
|
-
stud_try = ::LogStash::Helpers::LoggableTry.new(logger, job_name)
|
64
|
-
stud_try.try((@retries + 1).times) { yield }
|
65
|
-
rescue => e
|
66
|
-
error_details = {:message => e.message, :cause => e.cause}
|
67
|
-
error_details[:backtrace] = e.backtrace if logger.debug?
|
68
|
-
logger.error("#{job_name} failed with ", error_details)
|
69
|
-
false
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
# Process the ESQL response and push events to the output queue
|
75
|
-
# @param columns [Array[Hash]] The ESQL query response columns
|
76
|
-
# @param values [Array[Array]] The ESQL query response hits
|
77
|
-
# @param output_queue [Queue] The queue to push processed events to
|
78
|
-
def process_response(columns, values, output_queue)
|
79
|
-
column_specs = columns.map { |column| ColumnSpec.new(column) }
|
80
|
-
sub_element_mark_map = mark_sub_elements(column_specs)
|
81
|
-
multi_fields = sub_element_mark_map.filter_map { |key, val| key.name if val == true }
|
82
|
-
logger.warn("Multi-fields found in ES|QL result and they will not be available in the event. Please use `RENAME` command if you want to include them.", { :detected_multi_fields => multi_fields }) if multi_fields.any?
|
83
|
-
|
84
|
-
values.each do |row|
|
85
|
-
event = column_specs.zip(row).each_with_object(LogStash::Event.new) do |(column, value), event|
|
86
|
-
# `unless value.nil?` is a part of `drop_null_columns` that if some of columns' values are not `nil`, `nil` values appear
|
87
|
-
# we should continuously filter out them to achieve full `drop_null_columns` on each individual row (ideal `LIMIT 1` result)
|
88
|
-
# we also exclude sub-elements of main field
|
89
|
-
if value && sub_element_mark_map[column] == false
|
90
|
-
field_reference = apply_target(column.field_reference)
|
91
|
-
event.set(field_reference, ESQL_PARSERS_BY_TYPE[column.type].call(value))
|
92
|
-
end
|
93
|
-
end
|
94
|
-
@event_decorator.call(event)
|
95
|
-
output_queue << event
|
96
|
-
rescue => e
|
97
|
-
# if event creation fails with whatever reason, inform user and tag with failure and return entry as it is
|
98
|
-
logger.warn("Event creation error, ", message: e.message, exception: e.class, data: { "columns" => columns, "values" => [row] })
|
99
|
-
failed_event = LogStash::Event.new("columns" => columns, "values" => [row], "tags" => ['_elasticsearch_input_failure'])
|
100
|
-
output_queue << failed_event
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Determines whether each column in a collection is a nested sub-element (example "user.age")
|
105
|
-
# of another column in the same collection (example "user").
|
106
|
-
#
|
107
|
-
# @param columns [Array<ColumnSpec>] An array of objects with a `name` attribute representing field paths.
|
108
|
-
# @return [Hash<ColumnSpec, Boolean>] A hash mapping each column to `true` if it is a sub-element of another field, `false` otherwise.
|
109
|
-
# Time complexity: (O(NlogN+N*K)) where K is the number of conflict depth
|
110
|
-
# without (`prefix_set`) memoization, it would be O(N^2)
|
111
|
-
def mark_sub_elements(columns)
|
112
|
-
# Sort columns by name length (ascending)
|
113
|
-
sorted_columns = columns.sort_by { |c| c.name.length }
|
114
|
-
prefix_set = Set.new # memoization set
|
115
|
-
|
116
|
-
sorted_columns.each_with_object({}) do |column, memo|
|
117
|
-
# Split the column name into parts (e.g., "user.profile.age" → ["user", "profile", "age"])
|
118
|
-
parts = column.name.split('.')
|
119
|
-
|
120
|
-
# Generate all possible parent prefixes (e.g., "user", "user.profile")
|
121
|
-
# and check if any parent prefix exists in the set
|
122
|
-
parent_prefixes = (0...parts.size - 1).map { |i| parts[0..i].join('.') }
|
123
|
-
memo[column] = parent_prefixes.any? { |prefix| prefix_set.include?(prefix) }
|
124
|
-
prefix_set.add(column.name)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Class representing a column specification in the ESQL response['columns']
|
130
|
-
# The class's main purpose is to provide a structure for the event key
|
131
|
-
# columns is an array with `name` and `type` pair (example: `{"name"=>"@timestamp", "type"=>"date"}`)
|
132
|
-
# @attr_reader :name [String] The name of the column
|
133
|
-
# @attr_reader :type [String] The type of the column
|
134
|
-
class ColumnSpec
|
135
|
-
attr_reader :name, :type
|
136
|
-
|
137
|
-
def initialize(spec)
|
138
|
-
@name = isolate(spec.fetch('name'))
|
139
|
-
@type = isolate(spec.fetch('type'))
|
140
|
-
end
|
141
|
-
|
142
|
-
def field_reference
|
143
|
-
@_field_reference ||= '[' + name.gsub('.', '][') + ']'
|
144
|
-
end
|
145
|
-
|
146
|
-
private
|
147
|
-
def isolate(value)
|
148
|
-
value.frozen? ? value : value.clone.freeze
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
@@ -1 +0,0 @@
|
|
1
|
-
2024-12-26T22:27:15+00:00
|