fluent-plugin-kusto 0.0.1.beta
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 +7 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +201 -0
- data/lib/fluent/plugin/auth/aad_tokenprovider.rb +105 -0
- data/lib/fluent/plugin/auth/azcli_tokenprovider.rb +51 -0
- data/lib/fluent/plugin/auth/mi_tokenprovider.rb +92 -0
- data/lib/fluent/plugin/auth/tokenprovider_base.rb +57 -0
- data/lib/fluent/plugin/auth/wif_tokenprovider.rb +50 -0
- data/lib/fluent/plugin/client.rb +155 -0
- data/lib/fluent/plugin/conffile.rb +155 -0
- data/lib/fluent/plugin/ingester.rb +136 -0
- data/lib/fluent/plugin/kusto_error_handler.rb +126 -0
- data/lib/fluent/plugin/kusto_query.rb +67 -0
- data/lib/fluent/plugin/out_kusto.rb +423 -0
- data/test/helper.rb +9 -0
- data/test/plugin/test_azcli_tokenprovider.rb +37 -0
- data/test/plugin/test_e2e_kusto.rb +683 -0
- data/test/plugin/test_out_kusto_config.rb +86 -0
- data/test/plugin/test_out_kusto_format.rb +280 -0
- data/test/plugin/test_out_kusto_process.rb +150 -0
- data/test/plugin/test_out_kusto_start.rb +429 -0
- data/test/plugin/test_out_kusto_try_write.rb +382 -0
- data/test/plugin/test_out_kusto_write.rb +370 -0
- metadata +171 -0
@@ -0,0 +1,382 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'ostruct'
|
5
|
+
require_relative '../helper'
|
6
|
+
require 'fluent/test/driver/output'
|
7
|
+
require 'fluent/plugin/out_kusto'
|
8
|
+
require 'mocha/test_unit'
|
9
|
+
|
10
|
+
class FakeKustoError < StandardError
|
11
|
+
def initialize(msg, permanent)
|
12
|
+
super(msg)
|
13
|
+
@permanent = permanent
|
14
|
+
end
|
15
|
+
|
16
|
+
def permanent?
|
17
|
+
@permanent
|
18
|
+
end
|
19
|
+
|
20
|
+
def is_permanent?
|
21
|
+
permanent?
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_code
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure_sub_code
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class KustoOutputTryWriteTest < Test::Unit::TestCase
|
34
|
+
setup do
|
35
|
+
Fluent::Test.setup
|
36
|
+
@driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(<<-CONF)
|
37
|
+
@type kusto
|
38
|
+
endpoint https://example.kusto.windows.net
|
39
|
+
database_name testdb
|
40
|
+
table_name testtable
|
41
|
+
client_id dummy-client-id
|
42
|
+
client_secret dummy-secret
|
43
|
+
tenant_id dummy-tenant
|
44
|
+
auth_type aad
|
45
|
+
buffered true
|
46
|
+
delayed true
|
47
|
+
CONF
|
48
|
+
@driver.instance.stubs(:commit_write)
|
49
|
+
end
|
50
|
+
|
51
|
+
teardown do
|
52
|
+
# Clean up any running threads and reset mocks
|
53
|
+
if @driver&.instance&.instance_variable_get(:@deferred_threads)
|
54
|
+
threads = @driver.instance.instance_variable_get(:@deferred_threads)
|
55
|
+
threads.each { |t| t.kill if t.alive? }
|
56
|
+
threads.clear
|
57
|
+
end
|
58
|
+
Mocha::Mockery.instance.teardown
|
59
|
+
end
|
60
|
+
|
61
|
+
def logger_stub
|
62
|
+
m = mock
|
63
|
+
m.stubs(:debug)
|
64
|
+
m.stubs(:error)
|
65
|
+
m.stubs(:info)
|
66
|
+
m.stubs(:warn)
|
67
|
+
m
|
68
|
+
end
|
69
|
+
|
70
|
+
def ingester_stub
|
71
|
+
m = mock
|
72
|
+
m.stubs(:upload_data_to_blob_and_queue)
|
73
|
+
m
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_mocks(ingester: nil, logger: nil)
|
77
|
+
@driver.instance.instance_variable_set(:@ingester, ingester) if ingester
|
78
|
+
@driver.instance.instance_variable_set(:@logger, logger) if logger
|
79
|
+
end
|
80
|
+
|
81
|
+
def chunk_stub(data: 'testdata', tag: 'test.tag', unique_id: 'uniqueid'.b, metadata: nil)
|
82
|
+
c = mock
|
83
|
+
c.stubs(:read).returns(data)
|
84
|
+
c.stubs(:metadata).returns(metadata || OpenStruct.new(tag: tag))
|
85
|
+
c.stubs(:unique_id).returns(unique_id)
|
86
|
+
c
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'try_write uploads compressed data to blob and queue with deferred commit' do
|
90
|
+
ingester_mock = ingester_stub
|
91
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
92
|
+
logger_mock = logger_stub
|
93
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
94
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
95
|
+
chunk = chunk_stub
|
96
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
97
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
98
|
+
sleep 1.2 # Give thread time to run
|
99
|
+
end
|
100
|
+
|
101
|
+
test 'try_write handles permanent Kusto error by dropping chunk' do
|
102
|
+
ingester_mock = ingester_stub
|
103
|
+
kusto_error = FakeKustoError.new('permanent fail', true)
|
104
|
+
KustoErrorHandler.stubs(:extract_kusto_error_type).returns(:permanent)
|
105
|
+
KustoErrorHandler.stubs(:from_kusto_error_type).returns(kusto_error)
|
106
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, 'fail')
|
107
|
+
logger_mock = logger_stub
|
108
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
109
|
+
chunk = chunk_stub
|
110
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
111
|
+
sleep 0.2
|
112
|
+
end
|
113
|
+
|
114
|
+
test 'try_write raises error on non-permanent Kusto error (triggers retry)' do
|
115
|
+
ingester_mock = ingester_stub
|
116
|
+
kusto_error = FakeKustoError.new('transient fail', false)
|
117
|
+
KustoErrorHandler.stubs(:extract_kusto_error_type).returns(:transient)
|
118
|
+
KustoErrorHandler.stubs(:from_kusto_error_type).returns(kusto_error)
|
119
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, 'fail')
|
120
|
+
logger_mock = logger_stub
|
121
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
122
|
+
chunk = chunk_stub
|
123
|
+
assert_raise(FakeKustoError) { @driver.instance.try_write(chunk) }
|
124
|
+
end
|
125
|
+
|
126
|
+
test 'try_write raises error on unknown error' do
|
127
|
+
ingester_mock = ingester_stub
|
128
|
+
KustoErrorHandler.stubs(:extract_kusto_error_type).returns(nil)
|
129
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(IOError, 'io fail')
|
130
|
+
logger_mock = logger_stub
|
131
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
132
|
+
chunk = chunk_stub
|
133
|
+
assert_raise(IOError) { @driver.instance.try_write(chunk) }
|
134
|
+
end
|
135
|
+
|
136
|
+
test 'try_write handles chunk metadata being nil' do
|
137
|
+
ingester_mock = ingester_stub
|
138
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
139
|
+
logger_mock = logger_stub
|
140
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
141
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
142
|
+
chunk = chunk_stub(metadata: nil)
|
143
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
144
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
145
|
+
sleep 1.2
|
146
|
+
end
|
147
|
+
|
148
|
+
test 'try_write handles chunk metadata without tag method' do
|
149
|
+
ingester_mock = ingester_stub
|
150
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
151
|
+
logger_mock = logger_stub
|
152
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
153
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
154
|
+
metadata_obj = Object.new
|
155
|
+
chunk = mock
|
156
|
+
chunk.stubs(:read).returns('testdata')
|
157
|
+
chunk.stubs(:metadata).returns(metadata_obj)
|
158
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
159
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
160
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
161
|
+
sleep 1.2
|
162
|
+
end
|
163
|
+
|
164
|
+
test 'try_write handles chunk unique_id being nil' do
|
165
|
+
ingester_mock = ingester_stub
|
166
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
167
|
+
logger_mock = logger_stub
|
168
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
169
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
170
|
+
chunk = chunk_stub(unique_id: nil)
|
171
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
172
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
173
|
+
sleep 1.2
|
174
|
+
end
|
175
|
+
|
176
|
+
test 'try_write handles chunk.read returning nil' do
|
177
|
+
ingester_mock = ingester_stub
|
178
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
179
|
+
logger_mock = logger_stub
|
180
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
181
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
182
|
+
chunk = chunk_stub(data: nil)
|
183
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
184
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
185
|
+
sleep 1.2
|
186
|
+
end
|
187
|
+
|
188
|
+
test 'try_write handles chunk.read returning empty string' do
|
189
|
+
ingester_mock = ingester_stub
|
190
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
191
|
+
logger_mock = logger_stub
|
192
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
193
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
194
|
+
chunk = chunk_stub(data: '')
|
195
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
196
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
197
|
+
sleep 1.2
|
198
|
+
end
|
199
|
+
|
200
|
+
test 'try_write handles chunk.read raising error' do
|
201
|
+
ingester_mock = mock
|
202
|
+
logger_mock = mock
|
203
|
+
logger_mock.stubs(:debug)
|
204
|
+
logger_mock.stubs(:error)
|
205
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
206
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
207
|
+
chunk = mock
|
208
|
+
chunk.stubs(:read).raises(StandardError, 'read fail')
|
209
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
210
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
211
|
+
assert_raise(StandardError) { @driver.instance.try_write(chunk) }
|
212
|
+
end
|
213
|
+
|
214
|
+
test 'try_write handles error in deferred commit thread' do
|
215
|
+
ingester_mock = mock
|
216
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
217
|
+
logger_mock = mock
|
218
|
+
logger_mock.stubs(:debug)
|
219
|
+
logger_mock.expects(:error).with(regexp_matches(/Error in deferred commit thread/)).at_least_once
|
220
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
221
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
222
|
+
# Simulate check_data_on_server raising error in thread
|
223
|
+
@driver.instance.stubs(:check_data_on_server).raises(StandardError, 'thread fail')
|
224
|
+
chunk = mock
|
225
|
+
chunk.stubs(:read).returns('testdata')
|
226
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
227
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
228
|
+
@driver.instance.stubs(:commit_write)
|
229
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
230
|
+
sleep 1.2
|
231
|
+
end
|
232
|
+
|
233
|
+
test 'try_write handles chunk with very large data' do
|
234
|
+
ingester_mock = ingester_stub
|
235
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
236
|
+
logger_mock = logger_stub
|
237
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
238
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
239
|
+
chunk = chunk_stub(data: 'x' * 10_000_000)
|
240
|
+
@driver.instance.expects(:commit_write).with(chunk.unique_id).once
|
241
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
242
|
+
sleep 1.2
|
243
|
+
end
|
244
|
+
|
245
|
+
test 'try_write is thread safe with concurrent calls' do
|
246
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
247
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
248
|
+
chunk = chunk_stub
|
249
|
+
@driver.instance.stubs(:commit_write)
|
250
|
+
threads = 5.times.map do
|
251
|
+
Thread.new { assert_nothing_raised { @driver.instance.try_write(chunk) } }
|
252
|
+
end
|
253
|
+
threads.each(&:join)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Removed test cases: unique_id as integer, unique_id as array, tag as empty string, unique_id with special characters, ignores return value
|
257
|
+
|
258
|
+
test 'try_write handles check_data_on_server always false (thread keeps running)' do
|
259
|
+
ingester_mock = mock
|
260
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
261
|
+
logger_mock = mock
|
262
|
+
logger_mock.stubs(:debug)
|
263
|
+
logger_mock.stubs(:error)
|
264
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
265
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
266
|
+
@driver.instance.stubs(:check_data_on_server).returns(false)
|
267
|
+
chunk = mock
|
268
|
+
chunk.stubs(:read).returns('testdata')
|
269
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
270
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
271
|
+
# We can't join the thread, but we can at least ensure no exception is raised
|
272
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
273
|
+
sleep 1.2
|
274
|
+
end
|
275
|
+
|
276
|
+
# Test that logger.debug and logger.error are called on success and error
|
277
|
+
test 'try_write logs debug and error messages appropriately' do
|
278
|
+
ingester_mock = mock
|
279
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
280
|
+
logger_mock = mock
|
281
|
+
logger_mock.stubs(:debug)
|
282
|
+
logger_mock.expects(:error).never
|
283
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
284
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
285
|
+
@driver.instance.stubs(:check_data_on_server).returns(true)
|
286
|
+
chunk = mock
|
287
|
+
chunk.stubs(:read).returns('testdata')
|
288
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
289
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
290
|
+
@driver.instance.stubs(:commit_write)
|
291
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
292
|
+
sleep 1.2
|
293
|
+
|
294
|
+
# Now test error logging
|
295
|
+
ingester_mock = mock
|
296
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, 'fail')
|
297
|
+
logger_mock = mock
|
298
|
+
logger_mock.stubs(:debug)
|
299
|
+
logger_mock.expects(:error).at_least_once
|
300
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
301
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
302
|
+
chunk = mock
|
303
|
+
chunk.stubs(:read).returns('testdata')
|
304
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
305
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
306
|
+
KustoErrorHandler.stubs(:extract_kusto_error_type).returns(:permanent)
|
307
|
+
KustoErrorHandler.stubs(:from_kusto_error_type).returns(FakeKustoError.new('permanent fail', true))
|
308
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
309
|
+
sleep 0.2
|
310
|
+
end
|
311
|
+
|
312
|
+
# Test that commit_write is not called if upload_data_to_blob_and_queue fails
|
313
|
+
test 'try_write does not call commit_write if upload_data_to_blob_and_queue fails' do
|
314
|
+
ingester_mock = mock
|
315
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, 'fail')
|
316
|
+
logger_mock = mock
|
317
|
+
logger_mock.stubs(:debug)
|
318
|
+
logger_mock.stubs(:error)
|
319
|
+
@driver.instance.instance_variable_set(:@ingester, ingester_mock)
|
320
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
321
|
+
chunk = mock
|
322
|
+
chunk.stubs(:read).returns('testdata')
|
323
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
324
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
325
|
+
@driver.instance.expects(:commit_write).never
|
326
|
+
KustoErrorHandler.stubs(:extract_kusto_error_type).returns(:permanent)
|
327
|
+
KustoErrorHandler.stubs(:from_kusto_error_type).returns(FakeKustoError.new('permanent fail', true))
|
328
|
+
assert_nothing_raised { @driver.instance.try_write(chunk) }
|
329
|
+
sleep 0.2
|
330
|
+
end
|
331
|
+
|
332
|
+
# Test behavior when @ingester is nil
|
333
|
+
test 'try_write raises error if @ingester is nil' do
|
334
|
+
@driver.instance.instance_variable_set(:@ingester, nil)
|
335
|
+
logger_mock = mock
|
336
|
+
logger_mock.stubs(:debug)
|
337
|
+
logger_mock.stubs(:error)
|
338
|
+
@driver.instance.instance_variable_set(:@logger, logger_mock)
|
339
|
+
chunk = mock
|
340
|
+
chunk.stubs(:read).returns('testdata')
|
341
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
342
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
343
|
+
assert_raise(NoMethodError) { @driver.instance.try_write(chunk) }
|
344
|
+
end
|
345
|
+
|
346
|
+
# Test for a configuration option affecting try_write (example: delayed false disables deferred commit)
|
347
|
+
test 'try_write commits immediately if delayed is false' do
|
348
|
+
driver2 = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(<<-CONF)
|
349
|
+
@type kusto
|
350
|
+
endpoint https://example.kusto.windows.net
|
351
|
+
database_name testdb
|
352
|
+
table_name testtable
|
353
|
+
client_id dummy-client-id
|
354
|
+
client_secret dummy-secret
|
355
|
+
tenant_id dummy-tenant
|
356
|
+
buffered true
|
357
|
+
delayed false
|
358
|
+
auth_type aad
|
359
|
+
CONF
|
360
|
+
|
361
|
+
# Set up minimal instance variables without calling start
|
362
|
+
driver2.instance.instance_variable_set(:@deferred_threads, [])
|
363
|
+
driver2.instance.instance_variable_set(:@shutdown_called, false)
|
364
|
+
driver2.instance.instance_variable_set(:@table_name, 'testtable')
|
365
|
+
driver2.instance.instance_variable_set(:@database_name, 'testdb')
|
366
|
+
|
367
|
+
ingester_mock = mock
|
368
|
+
ingester_mock.expects(:upload_data_to_blob_and_queue).once
|
369
|
+
logger_mock = mock
|
370
|
+
logger_mock.stubs(:debug)
|
371
|
+
logger_mock.stubs(:error)
|
372
|
+
logger_mock.stubs(:info)
|
373
|
+
driver2.instance.instance_variable_set(:@ingester, ingester_mock)
|
374
|
+
driver2.instance.instance_variable_set(:@logger, logger_mock)
|
375
|
+
chunk = mock
|
376
|
+
chunk.stubs(:read).returns('testdata')
|
377
|
+
chunk.stubs(:metadata).returns(OpenStruct.new(tag: 'test.tag'))
|
378
|
+
chunk.stubs(:unique_id).returns('uniqueid'.b)
|
379
|
+
driver2.instance.expects(:commit_write).with(chunk.unique_id).once
|
380
|
+
assert_nothing_raised { driver2.instance.try_write(chunk) }
|
381
|
+
end
|
382
|
+
end
|