logstash-output-kusto 2.1.6-java → 2.2.0-java
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 +8 -0
- data/README.md +42 -2
- data/lib/logstash/outputs/kusto/ingestor.rb +314 -13
- data/lib/logstash/outputs/kusto/streaming_chunker.rb +42 -0
- data/lib/logstash/outputs/kusto.rb +373 -41
- data/spec/outputs/kusto/ingestor_spec.rb +480 -0
- data/spec/outputs/kusto/streaming_chunker_spec.rb +57 -0
- data/spec/outputs/kusto_spec.rb +391 -0
- data/version +1 -1
- metadata +6 -3
- /data/vendor/jar-dependencies/org/logstash/outputs/logstash-output-kusto/{2.1.6/logstash-output-kusto-2.1.6.jar → 2.2.0/logstash-output-kusto-2.2.0.jar} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require_relative "../../spec_helpers.rb"
|
|
3
3
|
require 'logstash/outputs/kusto'
|
|
4
4
|
require 'logstash/outputs/kusto/ingestor'
|
|
5
|
+
require 'tempfile'
|
|
5
6
|
|
|
6
7
|
describe LogStash::Outputs::Kusto::Ingestor do
|
|
7
8
|
|
|
@@ -86,6 +87,485 @@ describe LogStash::Outputs::Kusto::Ingestor do
|
|
|
86
87
|
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
describe 'vendored streaming fallback policy' do
|
|
91
|
+
it 'queues uncompressed JSON above the SDK estimate and hard size boundaries' do
|
|
92
|
+
policy = Java::com.microsoft.azure.kusto.ingest.ManagedStreamingQueuingPolicy::Default
|
|
93
|
+
json_format =
|
|
94
|
+
Java::com.microsoft.azure.kusto.ingest.IngestionProperties::DataFormat::JSON
|
|
95
|
+
six_mib = 6 * 1024 * 1024
|
|
96
|
+
ten_mib = 10 * 1024 * 1024
|
|
97
|
+
|
|
98
|
+
expect(policy.shouldUseQueuedIngestion(six_mib, false, json_format)).to be(false)
|
|
99
|
+
expect(policy.shouldUseQueuedIngestion(six_mib + 1, false, json_format)).to be(true)
|
|
100
|
+
expect(policy.shouldUseQueuedIngestion(ten_mib, true, json_format)).to be(true)
|
|
101
|
+
expect(policy.shouldUseQueuedIngestion(ten_mib + 1, false, json_format)).to be(true)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '#upload with streaming' do
|
|
106
|
+
let(:streaming_client) { double('streaming client') }
|
|
107
|
+
let(:threadpool) { double('threadpool', shutdown: nil, wait_for_termination: nil) }
|
|
108
|
+
let(:sleeper) { double('sleeper', call: nil) }
|
|
109
|
+
let(:streaming_metric) { double('streaming metric', increment: nil) }
|
|
110
|
+
let(:ingestor) do
|
|
111
|
+
described_class.new(
|
|
112
|
+
ingest_url,
|
|
113
|
+
app_id,
|
|
114
|
+
app_key,
|
|
115
|
+
app_tenant,
|
|
116
|
+
managed_identity,
|
|
117
|
+
cliauth,
|
|
118
|
+
database,
|
|
119
|
+
table,
|
|
120
|
+
json_mapping,
|
|
121
|
+
delete_local,
|
|
122
|
+
proxy_host,
|
|
123
|
+
proxy_port,
|
|
124
|
+
proxy_protocol,
|
|
125
|
+
logger,
|
|
126
|
+
threadpool,
|
|
127
|
+
'streaming',
|
|
128
|
+
2,
|
|
129
|
+
0.01,
|
|
130
|
+
streaming_client,
|
|
131
|
+
sleeper,
|
|
132
|
+
streaming_metric
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with_streaming_file(payload = "{\"id\":1}\n")
|
|
137
|
+
file = Tempfile.new(['kusto-streaming', '.json'])
|
|
138
|
+
file.binmode
|
|
139
|
+
file.write(payload)
|
|
140
|
+
file.close
|
|
141
|
+
yield file.path
|
|
142
|
+
ensure
|
|
143
|
+
file&.unlink
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ingestion_result(status)
|
|
147
|
+
double(
|
|
148
|
+
'ingestion result',
|
|
149
|
+
getIngestionStatusCollection: [
|
|
150
|
+
double('ingestion status', status: status, details: nil, errorCode: nil)
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
%w[Succeeded Queued Pending].each do |status|
|
|
156
|
+
it "accepts #{status} and deletes the completed spool file" do
|
|
157
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result(status))
|
|
158
|
+
|
|
159
|
+
with_streaming_file do |path|
|
|
160
|
+
expect(ingestor.upload(path, true)).to eq(status)
|
|
161
|
+
expect(File.exist?(path)).to be(false)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
%w[Skipped PartiallySucceeded].each do |status|
|
|
167
|
+
it "quarantines final #{status} without replaying the whole file" do
|
|
168
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result(status))
|
|
169
|
+
|
|
170
|
+
with_streaming_file do |path|
|
|
171
|
+
expect(ingestor.upload(path, true)).to eq(status)
|
|
172
|
+
expect(File.exist?(path)).to be(false)
|
|
173
|
+
quarantine_path = "#{path}.#{status.downcase}"
|
|
174
|
+
expect(File.exist?(quarantine_path)).to be(true)
|
|
175
|
+
File.delete(quarantine_path)
|
|
176
|
+
end
|
|
177
|
+
expect(logger).to have_received(:warn)
|
|
178
|
+
expect(logger).to have_received(:error).with(
|
|
179
|
+
'Streaming request was quarantined after a final non-success status.',
|
|
180
|
+
hash_including(status: status)
|
|
181
|
+
)
|
|
182
|
+
expect(streaming_client).to have_received(:ingestFromFile).once
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'retains the spool file when Kusto returns Failed' do
|
|
187
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result('Failed'))
|
|
188
|
+
|
|
189
|
+
with_streaming_file do |path|
|
|
190
|
+
expect(ingestor.upload(path, true)).to be_nil
|
|
191
|
+
expect(File.exist?(path)).to be(true)
|
|
192
|
+
end
|
|
193
|
+
expect(streaming_metric).to have_received(:increment).with(:failures)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'retains the spool file when the SDK returns no status' do
|
|
197
|
+
result = double('ingestion result', getIngestionStatusCollection: [])
|
|
198
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(result)
|
|
199
|
+
|
|
200
|
+
with_streaming_file do |path|
|
|
201
|
+
expect(ingestor.upload(path, true)).to be_nil
|
|
202
|
+
expect(File.exist?(path)).to be(true)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'quarantines a mixed status collection instead of trusting the first status' do
|
|
207
|
+
result = double(
|
|
208
|
+
'ingestion result',
|
|
209
|
+
getIngestionStatusCollection: [
|
|
210
|
+
double('succeeded status', status: 'Succeeded'),
|
|
211
|
+
double('failed status', status: 'Failed')
|
|
212
|
+
]
|
|
213
|
+
)
|
|
214
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(result)
|
|
215
|
+
|
|
216
|
+
with_streaming_file do |path|
|
|
217
|
+
expect(ingestor.upload(path, true)).to eq('PartiallySucceeded')
|
|
218
|
+
quarantine_path = "#{path}.partiallysucceeded"
|
|
219
|
+
expect(File.exist?(quarantine_path)).to be(true)
|
|
220
|
+
File.delete(quarantine_path)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'passes the complete spool file and a source id to the SDK' do
|
|
225
|
+
payload = "{\"id\":1}\n{\"id\":2}\n"
|
|
226
|
+
captured_path = nil
|
|
227
|
+
captured_source_id = nil
|
|
228
|
+
allow(streaming_client).to receive(:ingestFromFile) do |source, _properties|
|
|
229
|
+
captured_path = source.getFilePath
|
|
230
|
+
captured_source_id = source.getSourceId
|
|
231
|
+
ingestion_result('Succeeded')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
with_streaming_file(payload) do |path|
|
|
235
|
+
ingestor.upload(path, false)
|
|
236
|
+
expect(captured_path).to eq(path)
|
|
237
|
+
end
|
|
238
|
+
expect(captured_source_id).not_to be_nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'reuses the source id when a spool file is recovered' do
|
|
242
|
+
source_ids = []
|
|
243
|
+
results = [ingestion_result('Failed'), ingestion_result('Succeeded')]
|
|
244
|
+
allow(streaming_client).to receive(:ingestFromFile) do |source, _properties|
|
|
245
|
+
source_ids << source.getSourceId.to_s
|
|
246
|
+
results.shift
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
with_streaming_file do |path|
|
|
250
|
+
ingestor.upload(path, true)
|
|
251
|
+
ingestor.upload(path, false)
|
|
252
|
+
File.delete("#{path}.completed")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
expect(source_ids.uniq.length).to eq(1)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'marks accepted files completed when debug retention is enabled' do
|
|
259
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result('Succeeded'))
|
|
260
|
+
|
|
261
|
+
with_streaming_file do |path|
|
|
262
|
+
expect(ingestor.upload(path, false)).to eq('Succeeded')
|
|
263
|
+
expect(File.exist?(path)).to be(false)
|
|
264
|
+
expect(File.exist?("#{path}.completed")).to be(true)
|
|
265
|
+
File.delete("#{path}.completed")
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'removes a committed batch directory after its final spool file completes' do
|
|
270
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result('Succeeded'))
|
|
271
|
+
|
|
272
|
+
Dir.mktmpdir('kusto-streaming-batch') do |directory|
|
|
273
|
+
batch_directory = File.join(directory, 'batch-completed.ready')
|
|
274
|
+
Dir.mkdir(batch_directory)
|
|
275
|
+
first_path = File.join(
|
|
276
|
+
batch_directory,
|
|
277
|
+
'stream-000000-11111111-1111-1111-1111-111111111111.json'
|
|
278
|
+
)
|
|
279
|
+
second_path = File.join(
|
|
280
|
+
batch_directory,
|
|
281
|
+
'stream-000001-22222222-2222-2222-2222-222222222222.json'
|
|
282
|
+
)
|
|
283
|
+
File.write(first_path, "{\"id\":1}\n")
|
|
284
|
+
File.write(second_path, "{\"id\":2}\n")
|
|
285
|
+
|
|
286
|
+
expect(ingestor.upload(first_path, true)).to eq('Succeeded')
|
|
287
|
+
expect(Dir.exist?(batch_directory)).to be(true)
|
|
288
|
+
expect(ingestor.upload(second_path, true)).to eq('Succeeded')
|
|
289
|
+
expect(Dir.exist?(batch_directory)).to be(false)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it 'retries transient service failures with bounded backoff and a stable source id' do
|
|
294
|
+
transient_error =
|
|
295
|
+
Java::com.microsoft.azure.kusto.ingest.exceptions.IngestionServiceException.new('transient')
|
|
296
|
+
attempts = 0
|
|
297
|
+
source_ids = []
|
|
298
|
+
allow(streaming_client).to receive(:ingestFromFile) do |source, _properties|
|
|
299
|
+
attempts += 1
|
|
300
|
+
source_ids << source.getSourceId.to_s
|
|
301
|
+
raise transient_error if attempts <= 2
|
|
302
|
+
|
|
303
|
+
ingestion_result('Succeeded')
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
with_streaming_file do |path|
|
|
307
|
+
expect(ingestor.upload(path, true)).to eq('Succeeded')
|
|
308
|
+
end
|
|
309
|
+
expect(sleeper).to have_received(:call).with(0.01).ordered
|
|
310
|
+
expect(sleeper).to have_received(:call).with(0.02).ordered
|
|
311
|
+
expect(source_ids.uniq.length).to eq(1)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'does not retry client failures and retains the spool file' do
|
|
315
|
+
client_error =
|
|
316
|
+
Java::com.microsoft.azure.kusto.ingest.exceptions.IngestionClientException.new('permanent')
|
|
317
|
+
allow(streaming_client).to receive(:ingestFromFile).and_raise(client_error)
|
|
318
|
+
|
|
319
|
+
with_streaming_file do |path|
|
|
320
|
+
expect(ingestor.upload(path, true)).to be_nil
|
|
321
|
+
expect(File.exist?(path)).to be(true)
|
|
322
|
+
end
|
|
323
|
+
expect(streaming_client).to have_received(:ingestFromFile).once
|
|
324
|
+
expect(sleeper).not_to have_received(:call)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'does not retry permanent service failures and retains the spool file' do
|
|
328
|
+
data_error =
|
|
329
|
+
Java::com.microsoft.azure.kusto.data.exceptions.DataServiceException.new(
|
|
330
|
+
'activity-id',
|
|
331
|
+
'permanent',
|
|
332
|
+
true
|
|
333
|
+
)
|
|
334
|
+
service_error =
|
|
335
|
+
Java::com.microsoft.azure.kusto.ingest.exceptions.IngestionServiceException.new(
|
|
336
|
+
'permanent',
|
|
337
|
+
data_error
|
|
338
|
+
)
|
|
339
|
+
allow(streaming_client).to receive(:ingestFromFile).and_raise(service_error)
|
|
340
|
+
|
|
341
|
+
with_streaming_file do |path|
|
|
342
|
+
expect(ingestor.upload(path, true)).to be_nil
|
|
343
|
+
expect(File.exist?(path)).to be(true)
|
|
344
|
+
end
|
|
345
|
+
expect(streaming_client).to have_received(:ingestFromFile).once
|
|
346
|
+
expect(sleeper).not_to have_received(:call)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'applies backpressure and starts another retry cycle after transient retries are exhausted' do
|
|
350
|
+
transient_error =
|
|
351
|
+
Java::com.microsoft.azure.kusto.ingest.exceptions.IngestionServiceException.new('transient')
|
|
352
|
+
attempts = 0
|
|
353
|
+
source_ids = []
|
|
354
|
+
allow(streaming_client).to receive(:ingestFromFile) do |source, _properties|
|
|
355
|
+
attempts += 1
|
|
356
|
+
source_ids << source.getSourceId.to_s
|
|
357
|
+
raise transient_error if attempts <= 3
|
|
358
|
+
|
|
359
|
+
ingestion_result('Succeeded')
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
with_streaming_file do |path|
|
|
363
|
+
expect(ingestor.upload(path, true)).to eq('Succeeded')
|
|
364
|
+
expect(File.exist?(path)).to be(false)
|
|
365
|
+
end
|
|
366
|
+
expect(streaming_client).to have_received(:ingestFromFile).exactly(4).times
|
|
367
|
+
expect(sleeper).to have_received(:call).with(0.01).once
|
|
368
|
+
expect(sleeper).to have_received(:call).with(0.02).once
|
|
369
|
+
expect(sleeper).to have_received(:call).with(0.04).once
|
|
370
|
+
expect(streaming_metric).to have_received(:increment).with(:retry_cycles)
|
|
371
|
+
expect(source_ids.uniq.length).to eq(1)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it 'retains the spool file for unexpected SDK failures' do
|
|
375
|
+
allow(streaming_client).to receive(:ingestFromFile).and_raise(StandardError, 'unexpected')
|
|
376
|
+
|
|
377
|
+
with_streaming_file do |path|
|
|
378
|
+
expect(ingestor.upload(path, true)).to be_nil
|
|
379
|
+
expect(File.exist?(path)).to be(true)
|
|
380
|
+
end
|
|
381
|
+
expect(streaming_metric).to have_received(:increment).with(:failures)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it 'logs a missing spool file without allowing the error to escape the upload task' do
|
|
385
|
+
file = Tempfile.new(['missing-kusto-streaming', '.json'])
|
|
386
|
+
path = file.path
|
|
387
|
+
file.close!
|
|
388
|
+
|
|
389
|
+
expect(streaming_client).not_to receive(:ingestFromFile)
|
|
390
|
+
expect { ingestor.upload(path, true) }.not_to raise_error
|
|
391
|
+
expect(logger).to have_received(:error).with(
|
|
392
|
+
"File doesn't exist! Unrecoverable error.",
|
|
393
|
+
hash_including(exception: Errno::ENOENT, path: path)
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'ingests synchronously when the streaming executor rejects a committed spool file' do
|
|
398
|
+
allow(threadpool).to receive(:remaining_capacity).and_return(10)
|
|
399
|
+
allow(threadpool).to receive(:post).and_raise(Concurrent::RejectedExecutionError)
|
|
400
|
+
allow(streaming_client).to receive(:ingestFromFile).and_return(ingestion_result('Succeeded'))
|
|
401
|
+
|
|
402
|
+
with_streaming_file do |path|
|
|
403
|
+
expect(ingestor.upload_async(path, true)).to eq('Succeeded')
|
|
404
|
+
expect(File.exist?(path)).to be(false)
|
|
405
|
+
end
|
|
406
|
+
expect(logger).to have_received(:error).with(
|
|
407
|
+
'Failed to enqueue Kusto ingestion.',
|
|
408
|
+
hash_including(path: kind_of(String))
|
|
409
|
+
)
|
|
410
|
+
expect(logger).to have_received(:warn).with(
|
|
411
|
+
'Streaming executor rejected a request; ingesting synchronously for backpressure.',
|
|
412
|
+
hash_including(path: kind_of(String))
|
|
413
|
+
)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it 'preserves queued-mode enqueue failure behavior' do
|
|
417
|
+
allow(threadpool).to receive(:remaining_capacity).and_return(10)
|
|
418
|
+
allow(threadpool).to receive(:post).and_raise(Concurrent::RejectedExecutionError)
|
|
419
|
+
queued_ingestor = described_class.new(
|
|
420
|
+
ingest_url,
|
|
421
|
+
app_id,
|
|
422
|
+
app_key,
|
|
423
|
+
app_tenant,
|
|
424
|
+
managed_identity,
|
|
425
|
+
cliauth,
|
|
426
|
+
database,
|
|
427
|
+
table,
|
|
428
|
+
json_mapping,
|
|
429
|
+
delete_local,
|
|
430
|
+
proxy_host,
|
|
431
|
+
proxy_port,
|
|
432
|
+
proxy_protocol,
|
|
433
|
+
logger,
|
|
434
|
+
threadpool,
|
|
435
|
+
'queued',
|
|
436
|
+
2,
|
|
437
|
+
1,
|
|
438
|
+
streaming_client
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
expect do
|
|
442
|
+
queued_ingestor.upload_async('/queued/request.json', true)
|
|
443
|
+
end.to raise_error(Concurrent::RejectedExecutionError)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'drains the worker pool and closes the streaming client on stop' do
|
|
447
|
+
allow(streaming_client).to receive(:close)
|
|
448
|
+
|
|
449
|
+
ingestor.stop
|
|
450
|
+
|
|
451
|
+
expect(streaming_client).to have_received(:close)
|
|
452
|
+
expect(threadpool).to have_received(:shutdown)
|
|
453
|
+
expect(threadpool).to have_received(:wait_for_termination).with(nil)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'interrupts an in-flight retry backoff during shutdown' do
|
|
457
|
+
transient_error =
|
|
458
|
+
Java::com.microsoft.azure.kusto.ingest.exceptions.IngestionServiceException.new('transient')
|
|
459
|
+
attempted = Queue.new
|
|
460
|
+
allow(streaming_client).to receive(:ingestFromFile) do
|
|
461
|
+
attempted << true
|
|
462
|
+
raise transient_error
|
|
463
|
+
end
|
|
464
|
+
allow(streaming_client).to receive(:close)
|
|
465
|
+
interruptible = described_class.new(
|
|
466
|
+
ingest_url,
|
|
467
|
+
app_id,
|
|
468
|
+
app_key,
|
|
469
|
+
app_tenant,
|
|
470
|
+
managed_identity,
|
|
471
|
+
cliauth,
|
|
472
|
+
database,
|
|
473
|
+
table,
|
|
474
|
+
json_mapping,
|
|
475
|
+
delete_local,
|
|
476
|
+
proxy_host,
|
|
477
|
+
proxy_port,
|
|
478
|
+
proxy_protocol,
|
|
479
|
+
logger,
|
|
480
|
+
threadpool,
|
|
481
|
+
'streaming',
|
|
482
|
+
2,
|
|
483
|
+
30,
|
|
484
|
+
streaming_client,
|
|
485
|
+
nil,
|
|
486
|
+
streaming_metric
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
with_streaming_file do |path|
|
|
490
|
+
upload_thread = Thread.new { interruptible.upload(path, true) }
|
|
491
|
+
attempted.pop
|
|
492
|
+
interruptible.stop
|
|
493
|
+
|
|
494
|
+
expect(upload_thread.join(1)).to eq(upload_thread)
|
|
495
|
+
expect(File.exist?(path)).to be(true)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
it 'skips completion directory fsync on Windows' do
|
|
500
|
+
allocated = described_class.allocate
|
|
501
|
+
allow(Gem).to receive(:win_platform?).and_return(true)
|
|
502
|
+
expect(File).not_to receive(:open)
|
|
503
|
+
|
|
504
|
+
expect { allocated.send(:fsync_directory, 'C:/spool') }.not_to raise_error
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
describe 'streaming client creation' do
|
|
509
|
+
it 'uses the SDK streaming factory and lets the SDK derive both endpoints' do
|
|
510
|
+
factory = Java::com.microsoft.azure.kusto.ingest.IngestClientFactory
|
|
511
|
+
client = double('managed client', close: nil)
|
|
512
|
+
allow(factory).to receive(:createManagedStreamingIngestClient).and_return(client)
|
|
513
|
+
threadpool = double('threadpool', shutdown: nil, wait_for_termination: nil)
|
|
514
|
+
|
|
515
|
+
ingestor = described_class.new(
|
|
516
|
+
ingest_url,
|
|
517
|
+
app_id,
|
|
518
|
+
app_key,
|
|
519
|
+
app_tenant,
|
|
520
|
+
managed_identity,
|
|
521
|
+
cliauth,
|
|
522
|
+
database,
|
|
523
|
+
table,
|
|
524
|
+
json_mapping,
|
|
525
|
+
delete_local,
|
|
526
|
+
nil,
|
|
527
|
+
proxy_port,
|
|
528
|
+
proxy_protocol,
|
|
529
|
+
logger,
|
|
530
|
+
threadpool,
|
|
531
|
+
'streaming'
|
|
532
|
+
)
|
|
533
|
+
ingestor.stop
|
|
534
|
+
|
|
535
|
+
expect(factory).to have_received(:createManagedStreamingIngestClient).once
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
it 'enables endpoint correction when creating a managed client with a proxy' do
|
|
539
|
+
factory = Java::com.microsoft.azure.kusto.ingest.IngestClientFactory
|
|
540
|
+
client = double('managed client', close: nil)
|
|
541
|
+
allow(factory).to receive(:createManagedStreamingIngestClient).and_return(client)
|
|
542
|
+
threadpool = double('threadpool', shutdown: nil, wait_for_termination: nil)
|
|
543
|
+
|
|
544
|
+
ingestor = described_class.new(
|
|
545
|
+
ingest_url,
|
|
546
|
+
app_id,
|
|
547
|
+
app_key,
|
|
548
|
+
app_tenant,
|
|
549
|
+
managed_identity,
|
|
550
|
+
cliauth,
|
|
551
|
+
database,
|
|
552
|
+
table,
|
|
553
|
+
json_mapping,
|
|
554
|
+
delete_local,
|
|
555
|
+
proxy_host,
|
|
556
|
+
proxy_port,
|
|
557
|
+
proxy_protocol,
|
|
558
|
+
logger,
|
|
559
|
+
threadpool,
|
|
560
|
+
'streaming'
|
|
561
|
+
)
|
|
562
|
+
ingestor.stop
|
|
563
|
+
|
|
564
|
+
expect(factory).to have_received(:createManagedStreamingIngestClient)
|
|
565
|
+
.with(anything, anything, true)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
89
569
|
# describe 'receiving events' do
|
|
90
570
|
|
|
91
571
|
# context 'with non-zero flush interval' do
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require_relative '../../spec_helpers'
|
|
4
|
+
require 'logstash/outputs/kusto/streaming_chunker'
|
|
5
|
+
|
|
6
|
+
describe LogStash::Outputs::Kusto::StreamingChunker do
|
|
7
|
+
describe '#chunks' do
|
|
8
|
+
it 'returns no chunks for an empty batch' do
|
|
9
|
+
expect(described_class.new(10).chunks([])).to eq([])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'combines events while their encoded bytes fit within the limit' do
|
|
13
|
+
chunks = described_class.new(10).chunks(%w[1234 567 89])
|
|
14
|
+
|
|
15
|
+
expect(chunks).to eq([%w[1234 567 89]])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'starts a new chunk before the next event would exceed the limit' do
|
|
19
|
+
chunks = described_class.new(6).chunks(%w[1234 567 89])
|
|
20
|
+
|
|
21
|
+
expect(chunks).to eq([%w[1234], %w[567 89]])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'allows a chunk to be exactly the configured limit' do
|
|
25
|
+
chunks = described_class.new(6).chunks(%w[123 456])
|
|
26
|
+
|
|
27
|
+
expect(chunks).to eq([%w[123 456]])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'keeps an oversized event intact and places it in its own chunk' do
|
|
31
|
+
oversized = 'x' * 11
|
|
32
|
+
chunks = described_class.new(10).chunks(['before', oversized, 'after'])
|
|
33
|
+
|
|
34
|
+
expect(chunks).to eq([['before'], [oversized], ['after']])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'measures bytes instead of characters' do
|
|
38
|
+
multibyte = "\u20ac\u20ac" # six UTF-8 bytes
|
|
39
|
+
chunks = described_class.new(6).chunks([multibyte, 'x'])
|
|
40
|
+
|
|
41
|
+
expect(chunks).to eq([[multibyte], ['x']])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'preserves event order and encoded separators' do
|
|
45
|
+
encoded = ["{\"id\":1}\n", "{\"id\":2}\n", "{\"id\":3}\n"]
|
|
46
|
+
chunks = described_class.new(18).chunks(encoded)
|
|
47
|
+
|
|
48
|
+
expect(chunks.flatten).to eq(encoded)
|
|
49
|
+
expect(chunks.map(&:join)).to eq(["{\"id\":1}\n{\"id\":2}\n", "{\"id\":3}\n"])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'rejects a non-positive byte limit' do
|
|
53
|
+
expect { described_class.new(0) }.to raise_error(ArgumentError)
|
|
54
|
+
expect { described_class.new(-1) }.to raise_error(ArgumentError)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|