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,429 @@
1
+ # rubocop:disable all
2
+
3
+ require_relative '../helper'
4
+ require 'fluent/plugin/out_kusto'
5
+ require 'mocha/test_unit'
6
+
7
+ # Dummy classes for use in tests
8
+ OutputConfigurationDummy = Class.new do
9
+ def initialize(*_args)
10
+ dummy = Object.new
11
+ def dummy.method_missing(name, *_args)
12
+ name == :logger ? Logger.new(nil) : 'dummy_value'
13
+ end
14
+
15
+ def dummy.respond_to_missing?(name, include_private = false)
16
+ name == :logger || super
17
+ end
18
+ AadTokenProvider.new(dummy).send(:post_token_request)
19
+ end
20
+
21
+ def logger
22
+ Logger.new(nil)
23
+ end
24
+
25
+ def table_name
26
+ 'testtable'
27
+ end
28
+
29
+ def database_name
30
+ 'testdb'
31
+ end
32
+ end
33
+ IngesterDummy = Class.new do
34
+ def initialize(*args); end
35
+ end
36
+ TestIngesterMock1 = Class.new { def initialize(config); end }
37
+ TestIngesterMock2 = Class.new { def initialize(config); end }
38
+ TestIngesterMock3 = Class.new do
39
+ @called_with = nil
40
+ def initialize(config)
41
+ self.class.called_with = config
42
+ end
43
+
44
+ class << self
45
+ attr_reader :called_with
46
+ end
47
+
48
+ class << self
49
+ attr_writer :called_with
50
+ end
51
+ end
52
+ TestIngesterMock4 = Class.new { def initialize(config); end }
53
+ TestIngesterMock5 = Class.new { def initialize(config); end }
54
+ TestIngesterMock6 = Class.new { def initialize(config); end }
55
+ TestIngesterMock7 = Class.new { def initialize(config); end }
56
+
57
+ class KustoOutputStartTest < Test::Unit::TestCase
58
+ def setup
59
+ Fluent::Test.setup
60
+ @conf = <<-CONF
61
+ @type kusto
62
+ endpoint https://example.kusto.windows.net
63
+ database_name testdb
64
+ table_name testtable
65
+ client_id dummy-client-id
66
+ client_secret dummy-secret
67
+ tenant_id dummy-tenant
68
+ buffered true
69
+ auth_type aad
70
+ CONF
71
+ @aad_token_stub = { 'access_token' => 'fake', 'expires_in' => 3600 }
72
+ AadTokenProvider.any_instance.stubs(:post_token_request).returns(@aad_token_stub)
73
+ end
74
+
75
+ test 'plugin initializes and starts successfully with valid config' do
76
+ output_config_mock = mock
77
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
78
+ output_config_mock.stubs(:table_name).returns('testtable')
79
+ output_config_mock.stubs(:database_name).returns('testdb')
80
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
81
+ Object.const_set(:OutputConfiguration, Class.new)
82
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
83
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
84
+ Object.const_set(:Ingester, TestIngesterMock1)
85
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
86
+ assert_nothing_raised { driver.instance.start }
87
+ assert_equal output_config_mock.logger, driver.instance.instance_variable_get(:@logger)
88
+ end
89
+
90
+ test 'start propagates error if OutputConfiguration.new fails' do
91
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
92
+ Object.const_set(:OutputConfiguration, Class.new)
93
+ OutputConfiguration.stubs(:new).raises(StandardError, 'init failed')
94
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
95
+ Object.const_set(:Ingester, TestIngesterMock2)
96
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
97
+ assert_raise(StandardError, 'init failed') { driver.instance.start }
98
+ end
99
+
100
+ test 'start initializes OutputConfiguration and Ingester with expected arguments' do
101
+ output_config_mock = mock
102
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
103
+ output_config_mock.stubs(:table_name).returns('testtable')
104
+ output_config_mock.stubs(:database_name).returns('testdb')
105
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
106
+ Object.const_set(:OutputConfiguration, Class.new)
107
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
108
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
109
+ Object.const_set(:Ingester, TestIngesterMock3)
110
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
111
+ driver.instance.start
112
+ assert_equal output_config_mock, TestIngesterMock3.called_with,
113
+ 'Ingester should be initialized with OutputConfiguration'
114
+ end
115
+
116
+ test 'start sets logger from OutputConfiguration' do
117
+ output_config_mock = mock
118
+ logger_mock = mock
119
+ output_config_mock.stubs(:logger).returns(logger_mock)
120
+ output_config_mock.stubs(:table_name).returns('testtable')
121
+ output_config_mock.stubs(:database_name).returns('testdb')
122
+ TestIngesterMock4 = Class.new { def initialize(config); end }
123
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
124
+ Object.const_set(:OutputConfiguration, Class.new)
125
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
126
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
127
+ Object.const_set(:Ingester, TestIngesterMock4)
128
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
129
+ driver.instance.start
130
+ assert_equal logger_mock, driver.instance.instance_variable_get(:@logger)
131
+ end
132
+
133
+ test 'start is idempotent (multiple calls do not error)' do
134
+ output_config_mock = mock
135
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
136
+ output_config_mock.stubs(:table_name).returns('testtable')
137
+ output_config_mock.stubs(:database_name).returns('testdb')
138
+ TestIngesterMock5 = Class.new { def initialize(config); end }
139
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
140
+ Object.const_set(:OutputConfiguration, Class.new)
141
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
142
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
143
+ Object.const_set(:Ingester, TestIngesterMock5)
144
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
145
+ assert_nothing_raised { driver.instance.start }
146
+ assert_nothing_raised { driver.instance.start }
147
+ end
148
+
149
+ test 'start cleans up resources on shutdown' do
150
+ output_config_mock = mock
151
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
152
+ output_config_mock.stubs(:table_name).returns('testtable')
153
+ output_config_mock.stubs(:database_name).returns('testdb')
154
+ output_config_mock.stubs(:kusto_endpoint).returns('https://example.kusto.windows.net')
155
+ output_config_mock.stubs(:access_token).returns('fake-access-token')
156
+ ingester_mock = mock
157
+ ingester_mock.stubs(:access_token).returns('fake-access-token')
158
+ ingester_mock.expects(:shutdown).once
159
+ # Add client and token_provider mocks for shutdown
160
+ client_mock = mock
161
+ token_provider_mock = mock
162
+ token_provider_mock.stubs(:fetch_token).returns('fake-access-token')
163
+ client_mock.stubs(:token_provider).returns(token_provider_mock)
164
+ ingester_mock.stubs(:client).returns(client_mock)
165
+ ingester_mock.stubs(:token_provider).returns(token_provider_mock)
166
+ TestIngesterMock6 = Class.new { def initialize(config); end }
167
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
168
+ Object.const_set(:OutputConfiguration, Class.new)
169
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
170
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
171
+ Object.const_set(:Ingester, TestIngesterMock6)
172
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
173
+ driver.instance.start
174
+ driver.instance.instance_variable_set(:@ingester, ingester_mock)
175
+ driver.instance.shutdown
176
+ end
177
+
178
+ test 'start is thread-safe in multi-worker environment' do
179
+ output_config_mock = mock
180
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
181
+ output_config_mock.stubs(:table_name).returns('testtable')
182
+ output_config_mock.stubs(:database_name).returns('testdb')
183
+ TestIngesterMock7 = Class.new { def initialize(config); end }
184
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
185
+ Object.const_set(:OutputConfiguration, Class.new)
186
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
187
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
188
+ Object.const_set(:Ingester, TestIngesterMock7)
189
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
190
+ threads = []
191
+ 5.times { threads << Thread.new { assert_nothing_raised { driver.instance.start } } }
192
+ threads.each(&:join)
193
+ end
194
+
195
+ test 'start triggers authentication logic and handles failures' do
196
+ AadTokenProvider.any_instance.stubs(:post_token_request).raises(StandardError, 'auth failed')
197
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
198
+ Object.const_set(:OutputConfiguration, OutputConfigurationDummy)
199
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
200
+ Object.const_set(:Ingester, IngesterDummy)
201
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
202
+ assert_raise(StandardError, 'auth failed') { driver.instance.start }
203
+ end
204
+
205
+ test 'start passes all config params to OutputConfiguration' do
206
+ output_config_mock = mock
207
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
208
+ output_config_mock.stubs(:table_name).returns('testtable')
209
+ output_config_mock.stubs(:database_name).returns('testdb')
210
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
211
+ Object.const_set(:OutputConfiguration, Class.new)
212
+ OutputConfiguration.expects(:new).with(
213
+ has_entries(
214
+ client_app_id: 'dummy-client-id',
215
+ client_app_secret: 'dummy-secret',
216
+ tenant_id: 'dummy-tenant',
217
+ kusto_endpoint: 'https://example.kusto.windows.net',
218
+ database_name: 'testdb',
219
+ table_name: 'testtable',
220
+ azure_cloud: 'AzureCloud',
221
+ managed_identity_client_id: nil
222
+ )
223
+ ).returns(output_config_mock)
224
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
225
+ Object.const_set(:Ingester, TestIngesterMock1)
226
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
227
+ assert_nothing_raised { driver.instance.start }
228
+ end
229
+
230
+ test 'start raises Fluent::ConfigError if required config missing' do
231
+ bad_conf = <<-CONF
232
+ @type kusto
233
+ endpoint https://example.kusto.windows.net
234
+ # database_name missing
235
+ table_name testtable
236
+ client_id dummy-client-id
237
+ client_secret dummy-secret
238
+ tenant_id dummy-tenant
239
+ buffered true
240
+ auth_type aad
241
+ CONF
242
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
243
+ assert_raise(Fluent::ConfigError) { driver.configure(bad_conf) }
244
+ end
245
+
246
+ test 'start sets plugin state variables' do
247
+ output_config_mock = mock
248
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
249
+ output_config_mock.stubs(:table_name).returns('testtable')
250
+ output_config_mock.stubs(:database_name).returns('testdb')
251
+ output_config_mock.stubs(:kusto_endpoint).returns('https://example.kusto.windows.net')
252
+ output_config_mock.stubs(:access_token).returns('fake-access-token')
253
+ # Add stubs for required AAD fields
254
+ output_config_mock.stubs(:client_app_id).returns('dummy-client-id')
255
+ output_config_mock.stubs(:client_app_secret).returns('dummy-secret')
256
+ output_config_mock.stubs(:tenant_id).returns('dummy-tenant')
257
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
258
+ Object.const_set(:OutputConfiguration, Class.new)
259
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
260
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
261
+ Object.const_set(:Ingester, TestIngesterMock1)
262
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
263
+ driver.instance.start
264
+ assert_not_nil driver.instance.instance_variable_get(:@plugin_start_time)
265
+ assert_equal 0, driver.instance.instance_variable_get(:@total_bytes_ingested)
266
+ assert_equal 'testtable', driver.instance.instance_variable_get(:@table_name)
267
+ assert_equal 'testdb', driver.instance.instance_variable_get(:@database_name)
268
+ end
269
+
270
+ test 'start raises error for invalid azure_cloud value' do
271
+ conf = <<-CONF
272
+ @type kusto
273
+ endpoint https://example.kusto.windows.net
274
+ database_name testdb
275
+ table_name testtable
276
+ client_id dummy-client-id
277
+ client_secret dummy-secret
278
+ tenant_id dummy-tenant
279
+ buffered true
280
+ azure_cloud InvalidCloud
281
+ auth_type aad
282
+ CONF
283
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
284
+ driver.configure(conf)
285
+ assert_raise(ArgumentError) { driver.instance.start }
286
+ end
287
+
288
+ test 'start handles missing logger in OutputConfiguration' do
289
+ output_config_mock = mock
290
+ output_config_mock.stubs(:logger).returns(nil)
291
+ output_config_mock.stubs(:table_name).returns('testtable')
292
+ output_config_mock.stubs(:database_name).returns('testdb')
293
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
294
+ Object.const_set(:OutputConfiguration, Class.new)
295
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
296
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
297
+ Object.const_set(:Ingester, TestIngesterMock1)
298
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
299
+ assert_nothing_raised { driver.instance.start }
300
+ assert_nil driver.instance.instance_variable_get(:@logger)
301
+ end
302
+
303
+ test 'start raises error if OutputConfiguration returns nil for required fields' do
304
+ output_config_mock = mock
305
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
306
+ output_config_mock.stubs(:table_name).returns(nil)
307
+ output_config_mock.stubs(:database_name).returns(nil)
308
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
309
+ Object.const_set(:OutputConfiguration, Class.new)
310
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
311
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
312
+ Object.const_set(:Ingester, TestIngesterMock1)
313
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
314
+ driver.instance.start
315
+ assert_nil driver.instance.instance_variable_get(:@table_name)
316
+ assert_nil driver.instance.instance_variable_get(:@database_name)
317
+ end
318
+
319
+ test 'start raises error if Ingester initialization fails' do
320
+ output_config_mock = mock
321
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
322
+ output_config_mock.stubs(:table_name).returns('testtable')
323
+ output_config_mock.stubs(:database_name).returns('testdb')
324
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
325
+ Object.const_set(:OutputConfiguration, Class.new)
326
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
327
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
328
+ failing_ingester = Class.new { def initialize(_); raise 'ingester failed'; end }
329
+ Object.const_set(:Ingester, failing_ingester)
330
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
331
+ assert_raise(RuntimeError, 'ingester failed') { driver.instance.start }
332
+ end
333
+
334
+ test 'start with minimal required config succeeds' do
335
+ minimal_conf = <<-CONF
336
+ @type kusto
337
+ endpoint https://example.kusto.windows.net
338
+ database_name testdb
339
+ auth_type aad
340
+ table_name testtable
341
+ buffered true
342
+ CONF
343
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
344
+ assert_nothing_raised { driver.configure(minimal_conf); driver.instance.start }
345
+ end
346
+
347
+ test 'start with managed identity only and no AAD params' do
348
+ mi_conf = <<-CONF
349
+ @type kusto
350
+ endpoint https://example.kusto.windows.net
351
+ database_name testdb
352
+ table_name testtable
353
+ buffered true
354
+ auth_type user_managed_identity
355
+ managed_identity_client_id test-mi-id
356
+ CONF
357
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
358
+ assert_nothing_raised { driver.configure(mi_conf); driver.instance.start }
359
+ end
360
+
361
+ test 'start sets correct default values for optional config params' do
362
+ conf = <<-CONF
363
+ @type kusto
364
+ endpoint https://example.kusto.windows.net
365
+ database_name testdb
366
+ table_name testtable
367
+ auth_type system_managed_identity
368
+ buffered true
369
+ CONF
370
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
371
+ driver.configure(conf)
372
+ assert_equal false, driver.instance.delayed
373
+ assert_equal 'AzureCloud', driver.instance.azure_cloud
374
+ end
375
+
376
+ test 'start with extra unknown config params does not error' do
377
+ conf = <<-CONF
378
+ @type kusto
379
+ endpoint https://example.kusto.windows.net
380
+ database_name testdb
381
+ table_name testtable
382
+ buffered true
383
+ auth_type aad
384
+ unknown_param some_value
385
+ CONF
386
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput)
387
+ assert_nothing_raised { driver.configure(conf); driver.instance.start }
388
+ end
389
+
390
+ test 'start after shutdown does not reinitialize resources' do
391
+ output_config_mock = mock
392
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
393
+ output_config_mock.stubs(:table_name).returns('testtable')
394
+ output_config_mock.stubs(:database_name).returns('testdb')
395
+ output_config_mock.stubs(:kusto_endpoint).returns('https://example.kusto.windows.net')
396
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
397
+ Object.const_set(:OutputConfiguration, Class.new)
398
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
399
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
400
+ Object.const_set(:Ingester, TestIngesterMock1)
401
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
402
+ driver.instance.start
403
+ driver.instance.shutdown
404
+ assert_nothing_raised { driver.instance.start }
405
+ end
406
+
407
+ test 'start raises error if OutputConfiguration is nil' do
408
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
409
+ Object.const_set(:OutputConfiguration, Class.new)
410
+ OutputConfiguration.stubs(:new).returns(nil)
411
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
412
+ Object.const_set(:Ingester, TestIngesterMock1)
413
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
414
+ assert_raise(NoMethodError) { driver.instance.start }
415
+ end
416
+
417
+ test 'start raises error if Ingester class is missing' do
418
+ output_config_mock = mock
419
+ output_config_mock.stubs(:logger).returns(Logger.new(nil))
420
+ output_config_mock.stubs(:table_name).returns('testtable')
421
+ output_config_mock.stubs(:database_name).returns('testdb')
422
+ Object.send(:remove_const, :OutputConfiguration) if Object.const_defined?(:OutputConfiguration)
423
+ Object.const_set(:OutputConfiguration, Class.new)
424
+ OutputConfiguration.stubs(:new).returns(output_config_mock)
425
+ Object.send(:remove_const, :Ingester) if Object.const_defined?(:Ingester)
426
+ driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(@conf)
427
+ assert_raise(NameError) { driver.instance.start }
428
+ end
429
+ end