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,86 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../helper'
|
5
|
+
require 'fluent/plugin/out_kusto'
|
6
|
+
require 'fluent/plugin/conffile'
|
7
|
+
|
8
|
+
class KustoOutputConfigTest < Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
Fluent::Test.setup
|
11
|
+
@base_conf = {
|
12
|
+
'kusto_endpoint' => 'https://example.kusto.windows.net',
|
13
|
+
'database_name' => 'testdb',
|
14
|
+
'table_name' => 'testtable',
|
15
|
+
'buffered' => 'true'
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'plugin raises error if buffer section present but buffered is false' do
|
20
|
+
conf = "#{config_str(@base_conf.merge('buffered' => 'false'))}\n<buffer>\n @type memory\n</buffer>\n"
|
21
|
+
assert_raise(Fluent::ConfigError) { create_driver(conf) }
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'plugin raises error when required param is empty string' do
|
25
|
+
%w[kusto_endpoint database_name table_name].each do |param|
|
26
|
+
conf_hash = @base_conf.dup
|
27
|
+
conf_hash[param] = ''
|
28
|
+
conf = config_str(conf_hash)
|
29
|
+
assert_raise(Fluent::ConfigError, "Should fail when #{param} is empty") { create_driver(conf) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'plugin raises error when required param is whitespace only' do
|
34
|
+
%w[kusto_endpoint database_name table_name].each do |param|
|
35
|
+
conf_hash = @base_conf.dup
|
36
|
+
conf_hash[param] = ' '
|
37
|
+
conf = config_str(conf_hash)
|
38
|
+
assert_raise(Fluent::ConfigError, "Should fail when #{param} is whitespace only") { create_driver(conf) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'plugin raises error when azure_cloud is invalid' do
|
43
|
+
assert_raise(ArgumentError) { OutputConfiguration.new(@base_conf.merge('azure_cloud' => 'InvalidCloud').transform_keys(&:to_sym)).validate_configuration }
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'plugin accepts valid managed_identity_client_id' do
|
47
|
+
conf = config_str(
|
48
|
+
@base_conf
|
49
|
+
.merge('auth_type' => 'user_managed_identity')
|
50
|
+
.merge('managed_identity_client_id' => 'valid_id')
|
51
|
+
.merge('endpoint' => @base_conf['kusto_endpoint'])
|
52
|
+
.merge('auth_type' => 'user_managed_identity') # Ensure required field is present
|
53
|
+
)
|
54
|
+
assert_nothing_raised { create_driver(conf) }
|
55
|
+
end
|
56
|
+
|
57
|
+
test 'plugin sets aad_endpoint based on azure_cloud' do
|
58
|
+
clouds = {
|
59
|
+
'AzureCloud' => 'https://login.microsoftonline.com',
|
60
|
+
'AzureChinaCloud' => 'https://login.chinacloudapi.cn',
|
61
|
+
'AzureUSGovernment' => 'https://login.microsoftonline.us'
|
62
|
+
}
|
63
|
+
base_conf_sym = @base_conf.transform_keys(&:to_sym)
|
64
|
+
clouds.each do |cloud, aad_url|
|
65
|
+
config = OutputConfiguration.new(
|
66
|
+
base_conf_sym.merge(
|
67
|
+
azure_cloud: cloud,
|
68
|
+
client_app_id: 'dummy-client-id',
|
69
|
+
client_app_secret: 'dummy-secret',
|
70
|
+
tenant_id: 'dummy-tenant'
|
71
|
+
)
|
72
|
+
)
|
73
|
+
assert_equal(aad_url, config.aad_endpoint)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def config_str(hash)
|
80
|
+
hash.map { |k, v| "#{k} #{v}" }.unshift('@type kusto').join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_driver(conf)
|
84
|
+
Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(conf)
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../helper'
|
5
|
+
require 'fluent/plugin/out_kusto'
|
6
|
+
|
7
|
+
class KustoOutputFormatTest < Test::Unit::TestCase
|
8
|
+
setup do
|
9
|
+
Fluent::Test.setup
|
10
|
+
@driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(<<-CONF)
|
11
|
+
@type kusto
|
12
|
+
endpoint https://example.kusto.windows.net
|
13
|
+
database_name testdb
|
14
|
+
table_name testtable
|
15
|
+
client_id dummy-client-id
|
16
|
+
client_secret dummy-secret
|
17
|
+
tenant_id dummy-tenant
|
18
|
+
buffered true
|
19
|
+
auth_type aad
|
20
|
+
CONF
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'format includes tag, timestamp, and record fields' do
|
24
|
+
tag = 'mytag'
|
25
|
+
time = Time.utc(2024, 1, 1, 12, 0, 0).to_i
|
26
|
+
record = { 'foo' => 'bar', 'baz' => 1 }
|
27
|
+
json = @driver.instance.format(tag, time, record)
|
28
|
+
parsed = JSON.parse(json)
|
29
|
+
assert_equal 'mytag', parsed['tag']
|
30
|
+
assert_equal Time.at(time).utc.iso8601, parsed['timestamp']
|
31
|
+
assert_equal({ 'foo' => 'bar', 'baz' => 1 }, parsed['record'])
|
32
|
+
end
|
33
|
+
|
34
|
+
test "format prefers record['tag'] over argument tag" do
|
35
|
+
tag = 'outertag'
|
36
|
+
time = Time.now.to_i
|
37
|
+
record = { 'tag' => 'innertag', 'foo' => 'bar' }
|
38
|
+
json = @driver.instance.format(tag, time, record)
|
39
|
+
parsed = JSON.parse(json)
|
40
|
+
assert_equal 'innertag', parsed['tag']
|
41
|
+
end
|
42
|
+
|
43
|
+
test "format uses record['tag'] over all fallbacks" do
|
44
|
+
tag = 'outer'
|
45
|
+
time = 1
|
46
|
+
record = { 'tag' => 'inner', 'host' => 'hostval', 'user' => 'userval', 'message' => '1.2.3.4' }
|
47
|
+
json = @driver.instance.format(tag, time, record)
|
48
|
+
parsed = JSON.parse(json)
|
49
|
+
assert_equal 'inner', parsed['tag']
|
50
|
+
end
|
51
|
+
|
52
|
+
test "format uses record['time'] over record['timestamp'] and time param" do
|
53
|
+
tag = 't'
|
54
|
+
time = 123
|
55
|
+
record = { 'time' => 't1', 'timestamp' => 't2' }
|
56
|
+
json = @driver.instance.format(tag, time, record)
|
57
|
+
parsed = JSON.parse(json)
|
58
|
+
assert_equal 't1', parsed['timestamp']
|
59
|
+
end
|
60
|
+
|
61
|
+
test "format uses record['timestamp'] over time param" do
|
62
|
+
tag = 't'
|
63
|
+
time = 123
|
64
|
+
record = { 'timestamp' => 't2' }
|
65
|
+
json = @driver.instance.format(tag, time, record)
|
66
|
+
parsed = JSON.parse(json)
|
67
|
+
assert_equal 't2', parsed['timestamp']
|
68
|
+
end
|
69
|
+
|
70
|
+
test 'format falls back to default_tag if no tag or fallback fields' do
|
71
|
+
tag = nil
|
72
|
+
time = nil
|
73
|
+
record = { 'foo' => 'bar' }
|
74
|
+
json = @driver.instance.format(tag, time, record)
|
75
|
+
parsed = JSON.parse(json)
|
76
|
+
assert_equal 'default_tag', parsed['tag']
|
77
|
+
end
|
78
|
+
|
79
|
+
test 'format removes tag and time from record in output' do
|
80
|
+
tag = 't'
|
81
|
+
time = 123
|
82
|
+
record = { 'foo' => 1, 'tag' => 't', 'time' => 'sometime' }
|
83
|
+
json = @driver.instance.format(tag, time, record)
|
84
|
+
parsed = JSON.parse(json)
|
85
|
+
assert_not_include parsed['record'].keys, 'tag'
|
86
|
+
assert_not_include parsed['record'].keys, 'time'
|
87
|
+
end
|
88
|
+
|
89
|
+
test 'format falls back to host, user, or IP in message if tag is missing' do
|
90
|
+
tag = nil
|
91
|
+
time = 0
|
92
|
+
record = { 'host' => 'hostval', 'foo' => 1 }
|
93
|
+
json = @driver.instance.format(tag, time, record)
|
94
|
+
parsed = JSON.parse(json)
|
95
|
+
assert_equal 'hostval', parsed['tag']
|
96
|
+
record2 = { 'user' => 'userval', 'foo' => 2 }
|
97
|
+
json2 = @driver.instance.format(tag, time, record2)
|
98
|
+
parsed2 = JSON.parse(json2)
|
99
|
+
assert_equal 'userval', parsed2['tag']
|
100
|
+
record3 = { 'message' => '192.168.1.1 something happened', 'foo' => 3 }
|
101
|
+
json3 = @driver.instance.format(tag, time, record3)
|
102
|
+
parsed3 = JSON.parse(json3)
|
103
|
+
assert_equal '192.168.1.1', parsed3['tag']
|
104
|
+
end
|
105
|
+
|
106
|
+
test 'format falls back to date/time in log message if timestamp missing' do
|
107
|
+
tag = 't'
|
108
|
+
time = nil
|
109
|
+
record = { 'foo' => 'bar', 'msg' => '[01/Jan/2024:12:00:00 +0000] log entry' }
|
110
|
+
json = @driver.instance.format(tag, time, record)
|
111
|
+
parsed = JSON.parse(json)
|
112
|
+
assert_equal '[01/Jan/2024:12:00:00 +0000] log entry', parsed['timestamp']
|
113
|
+
end
|
114
|
+
|
115
|
+
test 'format sets timestamp to empty string if no time info' do
|
116
|
+
tag = 't'
|
117
|
+
time = nil
|
118
|
+
record = { 'foo' => 'bar' }
|
119
|
+
json = @driver.instance.format(tag, time, record)
|
120
|
+
parsed = JSON.parse(json)
|
121
|
+
assert_equal '', parsed['timestamp']
|
122
|
+
end
|
123
|
+
|
124
|
+
test 'format uses first date/time pattern found in record' do
|
125
|
+
tag = 't'
|
126
|
+
time = nil
|
127
|
+
record = {
|
128
|
+
'foo' => 'no date here',
|
129
|
+
'bar' => '[01/Jan/2024:12:00:00 +0000] log entry',
|
130
|
+
'baz' => '[02/Feb/2025:13:00:00 +0000] another'
|
131
|
+
}
|
132
|
+
json = @driver.instance.format(tag, time, record)
|
133
|
+
parsed = JSON.parse(json)
|
134
|
+
assert_equal '[01/Jan/2024:12:00:00 +0000] log entry', parsed['timestamp']
|
135
|
+
end
|
136
|
+
|
137
|
+
test 'format output is valid JSON and ends with newline' do
|
138
|
+
tag = 't'
|
139
|
+
time = 1
|
140
|
+
record = { 'foo' => 'bar' }
|
141
|
+
json = @driver.instance.format(tag, time, record)
|
142
|
+
assert_nothing_raised { JSON.parse(json) }
|
143
|
+
assert_match(/\n\z/, json)
|
144
|
+
end
|
145
|
+
|
146
|
+
test 'format raises error on circular reference' do
|
147
|
+
tag = 't'
|
148
|
+
time = 1
|
149
|
+
record = {}
|
150
|
+
record['self'] = record
|
151
|
+
assert_raise_with_message(RuntimeError, 'Circular reference detected in record') do
|
152
|
+
@driver.instance.format(tag, time, record)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
test "format uses any key containing 'time' or 'date' for timestamp" do
|
157
|
+
tag = 't'
|
158
|
+
time = 123
|
159
|
+
record = { 'event_time' => '2024-01-01T12:00:00Z', 'foo' => 1 }
|
160
|
+
json = @driver.instance.format(tag, time, record)
|
161
|
+
parsed = JSON.parse(json)
|
162
|
+
assert_equal '2024-01-01T12:00:00Z', parsed['timestamp']
|
163
|
+
|
164
|
+
record2 = { 'logdate' => '2023-12-31', 'foo' => 2 }
|
165
|
+
json2 = @driver.instance.format(tag, time, record2)
|
166
|
+
parsed2 = JSON.parse(json2)
|
167
|
+
assert_equal '2023-12-31', parsed2['timestamp']
|
168
|
+
|
169
|
+
record3 = { 'sometime' => '', 'foo' => 3 }
|
170
|
+
json3 = @driver.instance.format(tag, time, record3)
|
171
|
+
parsed3 = JSON.parse(json3)
|
172
|
+
# Falls back to time param if key exists but is empty
|
173
|
+
assert_equal Time.at(time).utc.iso8601, parsed3['timestamp']
|
174
|
+
end
|
175
|
+
|
176
|
+
test 'format handles record with JSON string value' do
|
177
|
+
tag = 't'
|
178
|
+
time = 1
|
179
|
+
json_str = '{"foo": "bar", "baz": 1}'
|
180
|
+
record = { 'data' => json_str }
|
181
|
+
json = @driver.instance.format(tag, time, record)
|
182
|
+
parsed = JSON.parse(json)
|
183
|
+
assert_equal json_str, parsed['record']['data']
|
184
|
+
end
|
185
|
+
|
186
|
+
test 'format handles record with CSV string value' do
|
187
|
+
tag = 't'
|
188
|
+
time = 1
|
189
|
+
csv_str = "foo,bar,baz\n1,2,3"
|
190
|
+
record = { 'csv_data' => csv_str }
|
191
|
+
json = @driver.instance.format(tag, time, record)
|
192
|
+
parsed = JSON.parse(json)
|
193
|
+
assert_equal csv_str, parsed['record']['csv_data']
|
194
|
+
end
|
195
|
+
|
196
|
+
test 'format handles record with nested hash (parsed JSON)' do
|
197
|
+
tag = 't'
|
198
|
+
time = 1
|
199
|
+
nested = { 'foo' => 'bar', 'baz' => 1 }
|
200
|
+
record = { 'nested' => nested }
|
201
|
+
json = @driver.instance.format(tag, time, record)
|
202
|
+
parsed = JSON.parse(json)
|
203
|
+
assert_equal nested, parsed['record']['nested']
|
204
|
+
end
|
205
|
+
|
206
|
+
test 'format handles nil record gracefully' do
|
207
|
+
tag = 't'
|
208
|
+
time = 1
|
209
|
+
record = nil
|
210
|
+
json = @driver.instance.format(tag, time, record)
|
211
|
+
parsed = JSON.parse(json)
|
212
|
+
assert_equal 't', parsed['tag']
|
213
|
+
assert_equal Time.at(time).utc.iso8601, parsed['timestamp']
|
214
|
+
assert_equal({}, parsed['record'])
|
215
|
+
end
|
216
|
+
|
217
|
+
test 'format handles array record' do
|
218
|
+
tag = 't'
|
219
|
+
time = 1
|
220
|
+
record = [1, 2, 3]
|
221
|
+
json = @driver.instance.format(tag, time, record)
|
222
|
+
parsed = JSON.parse(json)
|
223
|
+
assert_equal 't', parsed['tag']
|
224
|
+
assert_equal Time.at(time).utc.iso8601, parsed['timestamp']
|
225
|
+
assert_equal [1, 2, 3], parsed['record']
|
226
|
+
end
|
227
|
+
|
228
|
+
test 'format handles deeply nested hash and array' do
|
229
|
+
tag = 't'
|
230
|
+
time = 1
|
231
|
+
record = { 'a' => { 'b' => [1, { 'c' => 2 }] } }
|
232
|
+
json = @driver.instance.format(tag, time, record)
|
233
|
+
parsed = JSON.parse(json)
|
234
|
+
assert_equal({ 'a' => { 'b' => [1, { 'c' => 2 }] } }, parsed['record'])
|
235
|
+
end
|
236
|
+
|
237
|
+
test 'format handles special characters in keys and values' do
|
238
|
+
tag = 't'
|
239
|
+
time = 1
|
240
|
+
record = { "uni\u2603" => "snowman\u2603", "newline" => "line1\nline2" }
|
241
|
+
json = @driver.instance.format(tag, time, record)
|
242
|
+
parsed = JSON.parse(json)
|
243
|
+
assert_equal "snowman\u2603", parsed['record']["uni\u2603"]
|
244
|
+
assert_equal "line1\nline2", parsed['record']['newline']
|
245
|
+
end
|
246
|
+
|
247
|
+
test 'format handles very large record' do
|
248
|
+
tag = 't'
|
249
|
+
time = 1
|
250
|
+
record = {}
|
251
|
+
1000.times { |i| record["key#{i}"] = i }
|
252
|
+
json = @driver.instance.format(tag, time, record)
|
253
|
+
parsed = JSON.parse(json)
|
254
|
+
assert_equal 1000, parsed['record'].size
|
255
|
+
end
|
256
|
+
|
257
|
+
test 'format handles boolean, nil, and float values' do
|
258
|
+
tag = 't'
|
259
|
+
time = 1
|
260
|
+
record = { 'bool' => true, 'nilval' => nil, 'float' => 1.23 }
|
261
|
+
json = @driver.instance.format(tag, time, record)
|
262
|
+
parsed = JSON.parse(json)
|
263
|
+
assert_equal true, parsed['record']['bool']
|
264
|
+
assert_nil parsed['record']['nilval']
|
265
|
+
assert_in_delta 1.23, parsed['record']['float'], 0.0001
|
266
|
+
end
|
267
|
+
|
268
|
+
test 'format handles empty hash and array' do
|
269
|
+
tag = 't'
|
270
|
+
time = 1
|
271
|
+
record = {}
|
272
|
+
json = @driver.instance.format(tag, time, record)
|
273
|
+
parsed = JSON.parse(json)
|
274
|
+
assert_equal({}, parsed['record'])
|
275
|
+
record2 = []
|
276
|
+
json2 = @driver.instance.format(tag, time, record2)
|
277
|
+
parsed2 = JSON.parse(json2)
|
278
|
+
assert_equal [], parsed2['record']
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
require_relative "../helper"
|
3
|
+
require "fluent/test/driver/output"
|
4
|
+
require "fluent/plugin/out_kusto.rb"
|
5
|
+
require "mocha/test_unit"
|
6
|
+
|
7
|
+
class KustoOutputProcessTest < Test::Unit::TestCase
|
8
|
+
setup do
|
9
|
+
Fluent::Test.setup
|
10
|
+
@driver = Fluent::Test::Driver::Output.new(Fluent::Plugin::KustoOutput).configure(<<-CONF)
|
11
|
+
@type kusto
|
12
|
+
endpoint https://example.kusto.windows.net
|
13
|
+
database_name testdb
|
14
|
+
table_name testtable
|
15
|
+
client_id dummy-client-id
|
16
|
+
client_secret dummy-secret
|
17
|
+
tenant_id dummy-tenant
|
18
|
+
buffered true
|
19
|
+
auth_type aad
|
20
|
+
CONF
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_mocks(ingester: nil, logger: nil)
|
24
|
+
@driver.instance.instance_variable_set(:@ingester, ingester) if ingester
|
25
|
+
@driver.instance.instance_variable_set(:@logger, logger) if logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def event_stream(arr)
|
29
|
+
Fluent::ArrayEventStream.new(arr)
|
30
|
+
end
|
31
|
+
|
32
|
+
def logger_stub
|
33
|
+
m = mock
|
34
|
+
m.stubs(:debug)
|
35
|
+
m.stubs(:error)
|
36
|
+
m.stubs(:warn)
|
37
|
+
m.stubs(:info)
|
38
|
+
m
|
39
|
+
end
|
40
|
+
|
41
|
+
def ingester_stub
|
42
|
+
m = mock
|
43
|
+
m.stubs(:upload_data_to_blob_and_queue)
|
44
|
+
m
|
45
|
+
end
|
46
|
+
|
47
|
+
test "process logs error but continues on upload failure" do
|
48
|
+
ingester_mock = mock; ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, "upload failed")
|
49
|
+
logger_mock = mock; logger_mock.stubs(:debug); logger_mock.expects(:error).at_least_once
|
50
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
51
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => "bar"}]])) }
|
52
|
+
end
|
53
|
+
|
54
|
+
test "process handles empty event stream" do
|
55
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
56
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([])) }
|
57
|
+
end
|
58
|
+
|
59
|
+
test "process handles non-hash record" do
|
60
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
61
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, "not a hash"]])) }
|
62
|
+
end
|
63
|
+
|
64
|
+
test "process handles nil record" do
|
65
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
66
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, nil]])) }
|
67
|
+
end
|
68
|
+
|
69
|
+
test "process handles format raising error (circular reference)" do
|
70
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
71
|
+
record = {}; record["self"] = record
|
72
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, record]])) }
|
73
|
+
end
|
74
|
+
|
75
|
+
test "process handles upload raising different errors" do
|
76
|
+
ingester_mock = mock; ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(IOError, "io error")
|
77
|
+
logger_mock = logger_stub; logger_mock.expects(:error).at_least_once
|
78
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
79
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => "bar"}]])) }
|
80
|
+
end
|
81
|
+
|
82
|
+
test "process continues after multiple failures" do
|
83
|
+
ingester_mock = mock; ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, "fail")
|
84
|
+
logger_mock = logger_stub; logger_mock.expects(:error).at_least_once
|
85
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
86
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => "bar"}], [Time.now.to_i, {"foo" => "baz"}]])) }
|
87
|
+
end
|
88
|
+
|
89
|
+
test "process handles logger being nil" do
|
90
|
+
set_mocks(ingester: ingester_stub, logger: nil)
|
91
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => "bar"}]])) }
|
92
|
+
end
|
93
|
+
|
94
|
+
test "process handles empty string time" do
|
95
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
96
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([["", {"foo" => "bar"}]])) }
|
97
|
+
end
|
98
|
+
|
99
|
+
test "process handles array as record" do
|
100
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
101
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, [1,2,3]]])) }
|
102
|
+
end
|
103
|
+
|
104
|
+
test "process handles very large record" do
|
105
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
106
|
+
big_record = {"foo" => "x" * 100_000}
|
107
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, big_record]])) }
|
108
|
+
end
|
109
|
+
|
110
|
+
test "process handles deeply nested record" do
|
111
|
+
set_mocks(ingester: ingester_stub, logger: logger_stub)
|
112
|
+
nested = {"a"=>{"b"=>{"c"=>{"d"=>1}}}}
|
113
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, nested]])) }
|
114
|
+
end
|
115
|
+
|
116
|
+
test "process handles binary data in record" do
|
117
|
+
logger_mock = mock; logger_mock.stubs(:debug); logger_mock.stubs(:error); logger_mock.stubs(:warn)
|
118
|
+
set_mocks(ingester: mock, logger: logger_mock)
|
119
|
+
record = {"bin" => "\xFF".b}
|
120
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, record]])) }
|
121
|
+
end
|
122
|
+
|
123
|
+
test "process encodes formatted data to UTF-8 and replaces invalid characters" do
|
124
|
+
ingester_mock = mock
|
125
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue)
|
126
|
+
logger_mock = logger_stub
|
127
|
+
logger_mock.stubs(:error)
|
128
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
129
|
+
bad_utf8 = "foo\xFFbar".force_encoding("ASCII-8BIT")
|
130
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => bad_utf8}]]) ) }
|
131
|
+
end
|
132
|
+
|
133
|
+
test "process handles event stream with mixed valid and invalid events" do
|
134
|
+
logger_mock = logger_stub
|
135
|
+
logger_mock.stubs(:error)
|
136
|
+
set_mocks(ingester: ingester_stub, logger: logger_mock)
|
137
|
+
es = event_stream([[Time.now.to_i, {"foo" => "bar"}], [Time.now.to_i, nil], [Time.now.to_i, "not a hash"]])
|
138
|
+
assert_nothing_raised { @driver.instance.process("test.tag", es) }
|
139
|
+
end
|
140
|
+
|
141
|
+
test "process logs error with correct message on upload failure" do
|
142
|
+
ingester_mock = mock
|
143
|
+
ingester_mock.stubs(:upload_data_to_blob_and_queue).raises(StandardError, "upload failed")
|
144
|
+
logger_mock = mock
|
145
|
+
logger_mock.stubs(:debug)
|
146
|
+
logger_mock.expects(:error).with { |msg| msg.include?("Failed to ingest event to Kusto") && msg.include?("upload failed") }
|
147
|
+
set_mocks(ingester: ingester_mock, logger: logger_mock)
|
148
|
+
assert_nothing_raised { @driver.instance.process("test.tag", event_stream([[Time.now.to_i, {"foo" => "bar"}]])) }
|
149
|
+
end
|
150
|
+
end
|