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.
@@ -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
- @shared_producer.shutdown
277
- @shared_producer = nil
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 'yajl'
318
- Proc.new { |tag, time, record| Yajl::Encoder.encode(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 = Yajl.load(z.get(:path => @zookeeper_path + "/#{id}")[:data])
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 'yajl'
263
- Proc.new { |tag, time, record| Yajl::Encoder.encode(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 'yajl'
232
- Proc.new { |tag, time, record| Yajl::Encoder.encode(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 < Gem::Version.create('0.12.0')
11
- require_relative 'rdkafka_patch/0_11_0'
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.16.0')
17
- require_relative 'rdkafka_patch/0_16_0'
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
- if unrecoverable_error_codes.include?(e.code.to_s)
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
- retun unless @inner
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')