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.
@@ -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
- if elastic_ruby_v8_client_available?
90
- allow(es_client).to receive(:info).and_raise(Elastic::Transport::Transport::Errors::BadRequest.new)
91
- else
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(mock_queue)
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
- target_field = :@seeds
744
- begin
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.new("test exception")
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 unauth_exception_class
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.23.0
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: 2025-06-06 00:00:00.000000000 Z
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: https://elastic.co/logstash
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