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.
@@ -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