fluent-plugin-kafka 0.19.7 → 0.19.9
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/.github/dependabot.yml +17 -0
- data/.github/workflows/linux.yml +3 -16
- data/.github/workflows/stale-actions.yml +1 -1
- data/ChangeLog +75 -0
- data/README.md +53 -3
- data/fluent-plugin-kafka.gemspec +1 -1
- data/lib/fluent/plugin/in_kafka.rb +30 -13
- data/lib/fluent/plugin/in_kafka_group.rb +12 -4
- data/lib/fluent/plugin/in_rdkafka_group.rb +15 -3
- data/lib/fluent/plugin/kafka_plugin_util.rb +106 -1
- data/lib/fluent/plugin/out_kafka.rb +3 -3
- data/lib/fluent/plugin/out_kafka2.rb +37 -8
- data/lib/fluent/plugin/out_kafka_buffered.rb +3 -3
- data/lib/fluent/plugin/out_rdkafka.rb +6 -4
- data/lib/fluent/plugin/out_rdkafka2.rb +21 -48
- data/lib/fluent/plugin/rdkafka_patch/0_13_0.rb +43 -0
- data/lib/fluent/plugin/rdkafka_patch/0_16_0.rb +3 -2
- data/test/plugin/test_in_kafka.rb +153 -0
- data/test/plugin/test_in_kafka_group.rb +13 -0
- data/test/plugin/test_in_rdkafka_group.rb +198 -0
- data/test/plugin/test_kafka_plugin_util.rb +26 -0
- data/test/plugin/test_out_kafka2.rb +172 -0
- data/test/plugin/test_out_rdkafka.rb +105 -0
- data/test/plugin/test_out_rdkafka2.rb +341 -1
- metadata +5 -2
|
@@ -98,6 +98,7 @@ DESC
|
|
|
98
98
|
include Fluent::KafkaPluginUtil::AwsIamSettings
|
|
99
99
|
include Fluent::KafkaPluginUtil::SSLSettings
|
|
100
100
|
include Fluent::KafkaPluginUtil::SaslSettings
|
|
101
|
+
include Fluent::KafkaPluginUtil::PartitionSettings
|
|
101
102
|
|
|
102
103
|
def initialize
|
|
103
104
|
super
|
|
@@ -197,6 +198,10 @@ DESC
|
|
|
197
198
|
end
|
|
198
199
|
end
|
|
199
200
|
|
|
201
|
+
if @share_producer && @buffer_config.flush_thread_count > 1
|
|
202
|
+
log.warn "share_producer sends one chunk at a time even with flush_thread_count #{@buffer_config.flush_thread_count}. Set share_producer false to send chunks in parallel"
|
|
203
|
+
end
|
|
204
|
+
|
|
200
205
|
@producer_opts = {max_retries: @max_send_retries, required_acks: @required_acks, idempotent: @idempotent}
|
|
201
206
|
@producer_opts[:ack_timeout] = @ack_timeout if @ack_timeout
|
|
202
207
|
@producer_opts[:compression_codec] = @compression_codec.to_sym if @compression_codec
|
|
@@ -239,6 +244,7 @@ DESC
|
|
|
239
244
|
|
|
240
245
|
if @share_producer
|
|
241
246
|
@shared_producer = create_producer
|
|
247
|
+
@shared_producer_mutex = Mutex.new
|
|
242
248
|
else
|
|
243
249
|
@producers = {}
|
|
244
250
|
@producers_mutex = Mutex.new
|
|
@@ -273,8 +279,10 @@ DESC
|
|
|
273
279
|
|
|
274
280
|
def shutdown_producers
|
|
275
281
|
if @share_producer
|
|
276
|
-
@
|
|
277
|
-
|
|
282
|
+
@shared_producer_mutex.synchronize {
|
|
283
|
+
@shared_producer.shutdown if @shared_producer
|
|
284
|
+
@shared_producer = nil
|
|
285
|
+
}
|
|
278
286
|
else
|
|
279
287
|
@producers_mutex.synchronize {
|
|
280
288
|
shutdown_threads = @producers.map { |key, producer|
|
|
@@ -292,7 +300,7 @@ DESC
|
|
|
292
300
|
|
|
293
301
|
def get_producer
|
|
294
302
|
if @share_producer
|
|
295
|
-
@shared_producer
|
|
303
|
+
@shared_producer ||= create_producer
|
|
296
304
|
else
|
|
297
305
|
@producers_mutex.synchronize {
|
|
298
306
|
producer = @producers[Thread.current.object_id]
|
|
@@ -305,6 +313,17 @@ DESC
|
|
|
305
313
|
end
|
|
306
314
|
end
|
|
307
315
|
|
|
316
|
+
def discard_producer
|
|
317
|
+
if @share_producer
|
|
318
|
+
producer, @shared_producer = @shared_producer, nil
|
|
319
|
+
else
|
|
320
|
+
producer = @producers_mutex.synchronize { @producers.delete(Thread.current.object_id) }
|
|
321
|
+
end
|
|
322
|
+
producer.shutdown if producer
|
|
323
|
+
rescue => e
|
|
324
|
+
log.warn "failed to shut down the discarded producer:", :error => e.to_s, :error_class => e.class.to_s
|
|
325
|
+
end
|
|
326
|
+
|
|
308
327
|
def setup_formatter(conf)
|
|
309
328
|
type = conf['@type']
|
|
310
329
|
case type
|
|
@@ -314,8 +333,8 @@ DESC
|
|
|
314
333
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
315
334
|
Proc.new { |tag, time, record| Oj.dump(record) }
|
|
316
335
|
rescue LoadError
|
|
317
|
-
require '
|
|
318
|
-
Proc.new { |tag, time, record|
|
|
336
|
+
require 'json'
|
|
337
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
319
338
|
end
|
|
320
339
|
when 'ltsv'
|
|
321
340
|
require 'ltsv'
|
|
@@ -326,10 +345,20 @@ DESC
|
|
|
326
345
|
end
|
|
327
346
|
end
|
|
328
347
|
|
|
329
|
-
# TODO: optimize write performance
|
|
330
348
|
def write(chunk)
|
|
331
349
|
@writing_threads_mutex.synchronize { @writing_threads.add(Thread.current) }
|
|
332
350
|
|
|
351
|
+
if @share_producer
|
|
352
|
+
@shared_producer_mutex.synchronize { write_chunk(chunk) }
|
|
353
|
+
else
|
|
354
|
+
write_chunk(chunk)
|
|
355
|
+
end
|
|
356
|
+
ensure
|
|
357
|
+
@writing_threads_mutex.synchronize { @writing_threads.delete(Thread.current) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# TODO: optimize write performance
|
|
361
|
+
def write_chunk(chunk)
|
|
333
362
|
tag = chunk.metadata.tag
|
|
334
363
|
topic = if @topic
|
|
335
364
|
extract_placeholders(@topic, chunk)
|
|
@@ -351,6 +380,7 @@ DESC
|
|
|
351
380
|
partition_key = (@exclude_partition_key ? record.delete(@partition_key_key) : record[@partition_key_key]) || @default_partition_key
|
|
352
381
|
partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
|
|
353
382
|
message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
|
|
383
|
+
partition = coerce_partition(partition) unless partition.nil?
|
|
354
384
|
|
|
355
385
|
if mutate_headers
|
|
356
386
|
headers = base_headers.clone
|
|
@@ -416,12 +446,11 @@ DESC
|
|
|
416
446
|
log.warn "Exception ignored in tag : #{tag}" if ignore
|
|
417
447
|
# For safety, refresh client and its producers
|
|
418
448
|
refresh_client(false)
|
|
449
|
+
discard_producer
|
|
419
450
|
# raise UnrecoverableError for backup ignored exception chunk
|
|
420
451
|
raise Fluent::UnrecoverableError if ignore && exception_backup
|
|
421
452
|
# Raise exception to retry sendind messages
|
|
422
453
|
raise e unless ignore
|
|
423
|
-
ensure
|
|
424
|
-
@writing_threads_mutex.synchronize { @writing_threads.delete(Thread.current) }
|
|
425
454
|
end
|
|
426
455
|
end
|
|
427
456
|
end
|
|
@@ -119,7 +119,7 @@ DESC
|
|
|
119
119
|
@seed_brokers = []
|
|
120
120
|
z = Zookeeper.new(@zookeeper)
|
|
121
121
|
z.get_children(:path => @zookeeper_path)[:children].each do |id|
|
|
122
|
-
broker =
|
|
122
|
+
broker = JSON.parse(z.get(:path => @zookeeper_path + "/#{id}")[:data], allow_duplicate_key: true)
|
|
123
123
|
if @ssl_client_cert
|
|
124
124
|
@seed_brokers.push(pickup_ssl_endpoint(broker))
|
|
125
125
|
else
|
|
@@ -259,8 +259,8 @@ DESC
|
|
|
259
259
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
260
260
|
Proc.new { |tag, time, record| Oj.dump(record) }
|
|
261
261
|
rescue LoadError
|
|
262
|
-
require '
|
|
263
|
-
Proc.new { |tag, time, record|
|
|
262
|
+
require 'json'
|
|
263
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
264
264
|
end
|
|
265
265
|
elsif @output_data_type == 'ltsv'
|
|
266
266
|
require 'ltsv'
|
|
@@ -130,12 +130,14 @@ DESC
|
|
|
130
130
|
:"bootstrap.servers" => @brokers,
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if @ssl_ca_cert && @ssl_ca_cert[0]
|
|
133
|
+
if (@ssl_ca_cert && @ssl_ca_cert[0]) || @ssl_ca_certs_from_system || @ssl_client_cert || @ssl_client_cert_key
|
|
134
134
|
ssl = true
|
|
135
|
-
config[:"ssl.ca.location"] = @ssl_ca_cert[0]
|
|
135
|
+
config[:"ssl.ca.location"] = @ssl_ca_cert[0] if @ssl_ca_cert && @ssl_ca_cert[0]
|
|
136
136
|
config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
|
|
137
137
|
config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
|
|
138
138
|
config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
|
|
139
|
+
config[:"ssl.endpoint.identification.algorithm"] = @ssl_verify_hostname ? "https" : "none"
|
|
140
|
+
config[:"enable.ssl.certificate.verification"] = true
|
|
139
141
|
end
|
|
140
142
|
|
|
141
143
|
if @principal
|
|
@@ -228,8 +230,8 @@ DESC
|
|
|
228
230
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
229
231
|
Proc.new { |tag, time, record| Oj.dump(record) }
|
|
230
232
|
rescue LoadError
|
|
231
|
-
require '
|
|
232
|
-
Proc.new { |tag, time, record|
|
|
233
|
+
require 'json'
|
|
234
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
233
235
|
end
|
|
234
236
|
elsif @output_data_type == 'ltsv'
|
|
235
237
|
require 'ltsv'
|
|
@@ -7,14 +7,16 @@ require 'rdkafka'
|
|
|
7
7
|
|
|
8
8
|
begin
|
|
9
9
|
rdkafka_version = Gem::Version::create(Rdkafka::VERSION)
|
|
10
|
-
if rdkafka_version
|
|
11
|
-
require_relative 'rdkafka_patch/
|
|
12
|
-
elsif rdkafka_version == Gem::Version.create('0.12.0')
|
|
13
|
-
require_relative 'rdkafka_patch/0_12_0'
|
|
10
|
+
if rdkafka_version >= Gem::Version.create('0.16.0')
|
|
11
|
+
require_relative 'rdkafka_patch/0_16_0'
|
|
14
12
|
elsif rdkafka_version >= Gem::Version.create('0.14.0')
|
|
15
13
|
require_relative 'rdkafka_patch/0_14_0'
|
|
16
|
-
elsif rdkafka_version >= Gem::Version.create('0.
|
|
17
|
-
require_relative 'rdkafka_patch/
|
|
14
|
+
elsif rdkafka_version >= Gem::Version.create('0.13.0')
|
|
15
|
+
require_relative 'rdkafka_patch/0_13_0'
|
|
16
|
+
elsif rdkafka_version >= Gem::Version.create('0.12.0')
|
|
17
|
+
require_relative 'rdkafka_patch/0_12_0'
|
|
18
|
+
else
|
|
19
|
+
require_relative 'rdkafka_patch/0_11_0'
|
|
18
20
|
end
|
|
19
21
|
rescue LoadError, NameError
|
|
20
22
|
raise "unable to patch rdkafka."
|
|
@@ -97,7 +99,6 @@ DESC
|
|
|
97
99
|
config_param :enqueue_retry_backoff, :integer, :default => 3
|
|
98
100
|
config_param :max_enqueue_bytes_per_second, :size, :default => nil, :desc => 'The maximum number of enqueueing bytes per second'
|
|
99
101
|
|
|
100
|
-
config_param :service_name, :string, :default => nil, :desc => 'Used for sasl.kerberos.service.name'
|
|
101
102
|
config_param :unrecoverable_error_codes, :array, :default => ["topic_authorization_failed", "msg_size_too_large"],
|
|
102
103
|
:desc => 'Handle some of the error codes should be unrecoverable if specified'
|
|
103
104
|
|
|
@@ -111,6 +112,8 @@ DESC
|
|
|
111
112
|
|
|
112
113
|
include Fluent::KafkaPluginUtil::SSLSettings
|
|
113
114
|
include Fluent::KafkaPluginUtil::SaslSettings
|
|
115
|
+
include Fluent::KafkaPluginUtil::PartitionSettings
|
|
116
|
+
include Fluent::KafkaPluginUtil::RdkafkaSecuritySettings
|
|
114
117
|
|
|
115
118
|
class EnqueueRate
|
|
116
119
|
class LimitExceeded < StandardError
|
|
@@ -250,39 +253,7 @@ DESC
|
|
|
250
253
|
end
|
|
251
254
|
|
|
252
255
|
def build_config
|
|
253
|
-
config = {:"bootstrap.servers" => @brokers}
|
|
254
|
-
|
|
255
|
-
if @ssl_ca_cert && @ssl_ca_cert[0]
|
|
256
|
-
ssl = true
|
|
257
|
-
config[:"ssl.ca.location"] = @ssl_ca_cert[0]
|
|
258
|
-
config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
|
|
259
|
-
config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
|
|
260
|
-
config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
if @principal
|
|
264
|
-
sasl = true
|
|
265
|
-
config[:"sasl.mechanisms"] = "GSSAPI"
|
|
266
|
-
config[:"sasl.kerberos.principal"] = @principal
|
|
267
|
-
config[:"sasl.kerberos.service.name"] = @service_name if @service_name
|
|
268
|
-
config[:"sasl.kerberos.keytab"] = @keytab if @keytab
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
if @username && @password
|
|
272
|
-
sasl = true
|
|
273
|
-
config[:"sasl.mechanisms"] = 'PLAIN'
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
if ssl && sasl
|
|
277
|
-
security_protocol = "SASL_SSL"
|
|
278
|
-
elsif ssl && !sasl
|
|
279
|
-
security_protocol = "SSL"
|
|
280
|
-
elsif !ssl && sasl
|
|
281
|
-
security_protocol = "SASL_PLAINTEXT"
|
|
282
|
-
else
|
|
283
|
-
security_protocol = "PLAINTEXT"
|
|
284
|
-
end
|
|
285
|
-
config[:"security.protocol"] = security_protocol
|
|
256
|
+
config = {:"bootstrap.servers" => @brokers}.merge(rdkafka_security_config)
|
|
286
257
|
|
|
287
258
|
config[:"compression.codec"] = @compression_codec if @compression_codec
|
|
288
259
|
config[:"message.send.max.retries"] = @max_send_retries if @max_send_retries
|
|
@@ -292,14 +263,14 @@ DESC
|
|
|
292
263
|
config[:"queue.buffering.max.messages"] = @rdkafka_buffering_max_messages if @rdkafka_buffering_max_messages
|
|
293
264
|
config[:"message.max.bytes"] = @rdkafka_message_max_bytes if @rdkafka_message_max_bytes
|
|
294
265
|
config[:"batch.num.messages"] = @rdkafka_message_max_num if @rdkafka_message_max_num
|
|
295
|
-
config[:"sasl.username"] = @username if @username
|
|
296
|
-
config[:"sasl.password"] = @password if @password
|
|
297
266
|
config[:"enable.idempotence"] = @idempotent if @idempotent
|
|
298
267
|
|
|
299
268
|
@rdkafka_options.each { |k, v|
|
|
300
269
|
config[k.to_sym] = v
|
|
301
270
|
}
|
|
302
271
|
|
|
272
|
+
validate_sasl_over_ssl(config)
|
|
273
|
+
|
|
303
274
|
config
|
|
304
275
|
end
|
|
305
276
|
|
|
@@ -406,6 +377,8 @@ DESC
|
|
|
406
377
|
record.delete(@topic_key) if @exclude_topic_key
|
|
407
378
|
partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
|
|
408
379
|
message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
|
|
380
|
+
partition = coerce_partition(partition) unless partition.nil?
|
|
381
|
+
message_key = message_key.to_s unless message_key.nil?
|
|
409
382
|
|
|
410
383
|
@headers_from_record_accessors.each do |key, header_accessor|
|
|
411
384
|
headers[key] = header_accessor.call(record)
|
|
@@ -451,6 +424,7 @@ DESC
|
|
|
451
424
|
log.warn "Delivery failed and matched regexp pattern #{@discard_kafka_delivery_failed_regex}. Discard events:", :error => e.to_s, :error_class => e.class.to_s, :tag => tag
|
|
452
425
|
else
|
|
453
426
|
log.warn "Send exception occurred: #{e} at #{e.backtrace.first}"
|
|
427
|
+
raise Fluent::UnrecoverableError, "Rejected due to #{e}" if unrecoverable_error?(e)
|
|
454
428
|
# Raise exception to retry sendind messages
|
|
455
429
|
raise e
|
|
456
430
|
end
|
|
@@ -507,15 +481,14 @@ DESC
|
|
|
507
481
|
|
|
508
482
|
raise e
|
|
509
483
|
else
|
|
510
|
-
|
|
511
|
-
# some of the errors should be handled as an unrecoverable error
|
|
512
|
-
raise Fluent::UnrecoverableError, "Rejected due to #{e}"
|
|
513
|
-
else
|
|
514
|
-
raise e
|
|
515
|
-
end
|
|
484
|
+
raise e
|
|
516
485
|
end
|
|
517
486
|
end
|
|
518
487
|
end
|
|
519
488
|
end
|
|
489
|
+
|
|
490
|
+
def unrecoverable_error?(e)
|
|
491
|
+
e.is_a?(Rdkafka::RdkafkaError) && unrecoverable_error_codes.include?(e.code.to_s)
|
|
492
|
+
end
|
|
520
493
|
end
|
|
521
494
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class Rdkafka::NativeKafka
|
|
2
|
+
# return false if producer is forcefully closed, otherwise return true
|
|
3
|
+
def close(timeout=nil, object_id=nil)
|
|
4
|
+
return true if closed?
|
|
5
|
+
|
|
6
|
+
synchronize do
|
|
7
|
+
# Indicate to the outside world that we are closing
|
|
8
|
+
@closing = true
|
|
9
|
+
|
|
10
|
+
thread_status = :unknown
|
|
11
|
+
if @polling_thread
|
|
12
|
+
# Indicate to polling thread that we're closing
|
|
13
|
+
@polling_thread[:closing] = true
|
|
14
|
+
|
|
15
|
+
# Wait for the polling thread to finish up,
|
|
16
|
+
# this can be aborted in practice if this
|
|
17
|
+
# code runs from a finalizer.
|
|
18
|
+
thread_status = @polling_thread.join(timeout)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Destroy the client after locking both mutexes
|
|
22
|
+
@poll_mutex.lock
|
|
23
|
+
|
|
24
|
+
# This check prevents a race condition, where we would enter the close in two threads
|
|
25
|
+
# and after unlocking the primary one that hold the lock but finished, ours would be unlocked
|
|
26
|
+
# and would continue to run, trying to destroy inner twice
|
|
27
|
+
if @inner
|
|
28
|
+
Rdkafka::Bindings.rd_kafka_destroy(@inner)
|
|
29
|
+
@inner = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
!thread_status.nil?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Rdkafka::Producer
|
|
38
|
+
def close(timeout = nil)
|
|
39
|
+
return true if closed?
|
|
40
|
+
ObjectSpace.undefine_finalizer(self)
|
|
41
|
+
@native_kafka.close(timeout)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -24,7 +24,7 @@ class Rdkafka::NativeKafka
|
|
|
24
24
|
# This check prevents a race condition, where we would enter the close in two threads
|
|
25
25
|
# and after unlocking the primary one that hold the lock but finished, ours would be unlocked
|
|
26
26
|
# and would continue to run, trying to destroy inner twice
|
|
27
|
-
|
|
27
|
+
return unless @inner
|
|
28
28
|
|
|
29
29
|
Rdkafka::Bindings.rd_kafka_destroy(@inner)
|
|
30
30
|
@inner = nil
|
|
@@ -40,7 +40,7 @@ class Rdkafka::Producer
|
|
|
40
40
|
return true if closed?
|
|
41
41
|
ObjectSpace.undefine_finalizer(self)
|
|
42
42
|
|
|
43
|
-
@native_kafka.close(timeout) do
|
|
43
|
+
closed = @native_kafka.close(timeout) do
|
|
44
44
|
# We need to remove the topics references objects before we destroy the producer,
|
|
45
45
|
# otherwise they would leak out
|
|
46
46
|
@topics_refs_map.each_value do |refs|
|
|
@@ -51,5 +51,6 @@ class Rdkafka::Producer
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
@topics_refs_map.clear
|
|
54
|
+
closed
|
|
54
55
|
end
|
|
55
56
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'helper'
|
|
2
2
|
require 'fluent/test/driver/input'
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'json'
|
|
4
5
|
|
|
5
6
|
class KafkaInputTest < Test::Unit::TestCase
|
|
6
7
|
def setup
|
|
@@ -34,6 +35,158 @@ class KafkaInputTest < Test::Unit::TestCase
|
|
|
34
35
|
assert_false d.instance.multi_workers_ready?
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
class TopicWatcherTest < self
|
|
39
|
+
FakeMessage = Struct.new(:value, :key, :offset, :create_time)
|
|
40
|
+
|
|
41
|
+
class FakeRouter
|
|
42
|
+
attr_reader :emitted
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@emitted = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit_stream(tag, es)
|
|
49
|
+
records = []
|
|
50
|
+
es.each { |time, record| records << record }
|
|
51
|
+
@emitted << [tag, records]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class OverflowRouter < FakeRouter
|
|
56
|
+
def initialize(overflow_tag)
|
|
57
|
+
super()
|
|
58
|
+
@overflow_tag = overflow_tag
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def emit_stream(tag, es)
|
|
62
|
+
raise Fluent::KafkaInput::BufferChunkOverflowError, "too large" if tag == @overflow_tag
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_topic_watcher(messages, router, tag_source: :record, add_prefix: nil, add_suffix: nil)
|
|
68
|
+
kafka = Object.new
|
|
69
|
+
kafka.define_singleton_method(:fetch_messages) { |**args| messages }
|
|
70
|
+
parser = Proc.new { |msg, te| JSON.parse(msg.value) }
|
|
71
|
+
|
|
72
|
+
Fluent::KafkaInput::TopicWatcher.new(
|
|
73
|
+
Fluent::KafkaInput::TopicEntry.new(TOPIC_NAME, 0, 0),
|
|
74
|
+
kafka,
|
|
75
|
+
1,
|
|
76
|
+
parser,
|
|
77
|
+
add_prefix,
|
|
78
|
+
add_suffix,
|
|
79
|
+
nil,
|
|
80
|
+
router,
|
|
81
|
+
nil,
|
|
82
|
+
:now,
|
|
83
|
+
'time',
|
|
84
|
+
tag_source,
|
|
85
|
+
'tag')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def tagged_message(tag, message, offset)
|
|
89
|
+
FakeMessage.new({'tag' => tag, 'message' => message}.to_json, nil, offset, Time.now)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_consume_with_tag_source_record_emits_stream_per_tag
|
|
93
|
+
messages = [
|
|
94
|
+
tagged_message('app.trusted', 'record 1', 0),
|
|
95
|
+
tagged_message('app.trusted', 'record 2', 1),
|
|
96
|
+
tagged_message('attacker.controlled', 'record 3', 2),
|
|
97
|
+
]
|
|
98
|
+
router = FakeRouter.new
|
|
99
|
+
create_topic_watcher(messages, router).consume
|
|
100
|
+
|
|
101
|
+
assert_equal([['app.trusted', ['record 1', 'record 2']],
|
|
102
|
+
['attacker.controlled', ['record 3']]],
|
|
103
|
+
router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_consume_with_tag_source_record_applies_prefix_and_suffix_per_tag
|
|
107
|
+
messages = [
|
|
108
|
+
tagged_message('app.trusted', 'record 1', 0),
|
|
109
|
+
tagged_message('attacker.controlled', 'record 2', 1),
|
|
110
|
+
]
|
|
111
|
+
router = FakeRouter.new
|
|
112
|
+
create_topic_watcher(messages, router, add_prefix: 'prefix', add_suffix: 'suffix').consume
|
|
113
|
+
|
|
114
|
+
assert_equal(['prefix.app.trusted.suffix', 'prefix.attacker.controlled.suffix'],
|
|
115
|
+
router.emitted.map(&:first))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_consume_with_tag_source_record_skips_unparsable_message
|
|
119
|
+
messages = [
|
|
120
|
+
tagged_message('app.trusted', 'record 1', 0),
|
|
121
|
+
FakeMessage.new('this is not json', nil, 1, Time.now),
|
|
122
|
+
tagged_message('attacker.controlled', 'record 2', 2),
|
|
123
|
+
]
|
|
124
|
+
router = FakeRouter.new
|
|
125
|
+
create_topic_watcher(messages, router).consume
|
|
126
|
+
|
|
127
|
+
assert_equal([['app.trusted', ['record 1']],
|
|
128
|
+
['attacker.controlled', ['record 2']]],
|
|
129
|
+
router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_consume_with_tag_source_record_skips_invalid_tag
|
|
133
|
+
messages = [
|
|
134
|
+
tagged_message('app.trusted', 'record 1', 0),
|
|
135
|
+
tagged_message(7, 'record 2', 1),
|
|
136
|
+
FakeMessage.new({'message' => 'record 3'}.to_json, nil, 2, Time.now),
|
|
137
|
+
tagged_message('', 'record 4', 3),
|
|
138
|
+
tagged_message('attacker.controlled', 'record 5', 4),
|
|
139
|
+
]
|
|
140
|
+
router = FakeRouter.new
|
|
141
|
+
watcher = create_topic_watcher(messages, router)
|
|
142
|
+
watcher.consume
|
|
143
|
+
|
|
144
|
+
assert_equal([['app.trusted', ['record 1']],
|
|
145
|
+
['attacker.controlled', ['record 5']]],
|
|
146
|
+
router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
|
|
147
|
+
assert_equal(5, watcher.instance_variable_get(:@next_offset))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_consume_advances_offset_when_every_message_is_skipped
|
|
151
|
+
messages = [
|
|
152
|
+
FakeMessage.new('this is not json', nil, 0, Time.now),
|
|
153
|
+
tagged_message(nil, 'record 1', 1),
|
|
154
|
+
]
|
|
155
|
+
router = FakeRouter.new
|
|
156
|
+
watcher = create_topic_watcher(messages, router)
|
|
157
|
+
watcher.consume
|
|
158
|
+
|
|
159
|
+
assert_equal([], router.emitted)
|
|
160
|
+
assert_equal(2, watcher.instance_variable_get(:@next_offset))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def test_consume_with_tag_source_topic_emits_single_stream
|
|
164
|
+
messages = [
|
|
165
|
+
tagged_message('app.trusted', 'record 1', 0),
|
|
166
|
+
tagged_message('attacker.controlled', 'record 2', 1),
|
|
167
|
+
]
|
|
168
|
+
router = FakeRouter.new
|
|
169
|
+
create_topic_watcher(messages, router, tag_source: :topic).consume
|
|
170
|
+
|
|
171
|
+
assert_equal([[TOPIC_NAME, ['record 1', 'record 2']]],
|
|
172
|
+
router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_consume_skips_buffer_chunk_overflow
|
|
176
|
+
messages = [
|
|
177
|
+
tagged_message('app.large', 'record 1', 0),
|
|
178
|
+
tagged_message('app.small', 'record 2', 1),
|
|
179
|
+
]
|
|
180
|
+
router = OverflowRouter.new('app.large')
|
|
181
|
+
watcher = create_topic_watcher(messages, router)
|
|
182
|
+
watcher.consume
|
|
183
|
+
|
|
184
|
+
assert_equal([['app.small', ['record 2']]],
|
|
185
|
+
router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
|
|
186
|
+
assert_equal(2, watcher.instance_variable_get(:@next_offset))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
37
190
|
class ConsumeTest < self
|
|
38
191
|
def setup
|
|
39
192
|
@kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
|
|
@@ -36,6 +36,19 @@ class KafkaGroupInputTest < Test::Unit::TestCase
|
|
|
36
36
|
assert_true d.instance.multi_workers_ready?
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def test_emit_events_with_buffer_chunk_overflow
|
|
40
|
+
d = create_driver
|
|
41
|
+
es = Fluent::MultiEventStream.new
|
|
42
|
+
router = Object.new
|
|
43
|
+
mock(router).emit_stream("test", es).once { raise Fluent::Plugin::Buffer::BufferChunkOverflowError, "too large" }
|
|
44
|
+
stub(d.instance).router { router }
|
|
45
|
+
|
|
46
|
+
assert_nothing_raised do
|
|
47
|
+
d.instance.emit_events("test", es)
|
|
48
|
+
end
|
|
49
|
+
assert_true d.logs.any? { |log| log.include?("Skipped records larger than the buffer chunk limit size") && log.include?("too large") }
|
|
50
|
+
end
|
|
51
|
+
|
|
39
52
|
class ConsumeTest < self
|
|
40
53
|
def setup
|
|
41
54
|
@kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
|