fluent-plugin-kafka-xst 0.19.1

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yaml +39 -0
  5. data/.github/dependabot.yml +6 -0
  6. data/.github/workflows/linux.yml +45 -0
  7. data/.github/workflows/stale-actions.yml +24 -0
  8. data/.gitignore +2 -0
  9. data/ChangeLog +344 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE +14 -0
  12. data/README.md +594 -0
  13. data/Rakefile +12 -0
  14. data/ci/prepare-kafka-server.sh +33 -0
  15. data/examples/README.md +3 -0
  16. data/examples/out_kafka2/dynamic_topic_based_on_tag.conf +32 -0
  17. data/examples/out_kafka2/protobuf-formatter.conf +23 -0
  18. data/examples/out_kafka2/record_key.conf +31 -0
  19. data/fluent-plugin-kafka.gemspec +27 -0
  20. data/lib/fluent/plugin/in_kafka.rb +388 -0
  21. data/lib/fluent/plugin/in_kafka_group.rb +394 -0
  22. data/lib/fluent/plugin/in_rdkafka_group.rb +305 -0
  23. data/lib/fluent/plugin/kafka_plugin_util.rb +84 -0
  24. data/lib/fluent/plugin/kafka_producer_ext.rb +308 -0
  25. data/lib/fluent/plugin/out_kafka.rb +268 -0
  26. data/lib/fluent/plugin/out_kafka2.rb +427 -0
  27. data/lib/fluent/plugin/out_kafka_buffered.rb +374 -0
  28. data/lib/fluent/plugin/out_rdkafka.rb +324 -0
  29. data/lib/fluent/plugin/out_rdkafka2.rb +526 -0
  30. data/test/helper.rb +34 -0
  31. data/test/plugin/test_in_kafka.rb +66 -0
  32. data/test/plugin/test_in_kafka_group.rb +69 -0
  33. data/test/plugin/test_kafka_plugin_util.rb +44 -0
  34. data/test/plugin/test_out_kafka.rb +68 -0
  35. data/test/plugin/test_out_kafka2.rb +138 -0
  36. data/test/plugin/test_out_kafka_buffered.rb +68 -0
  37. data/test/plugin/test_out_rdkafka2.rb +182 -0
  38. metadata +214 -0
@@ -0,0 +1,526 @@
1
+ require 'thread'
2
+ require 'logger'
3
+ require 'fluent/plugin/output'
4
+ require 'fluent/plugin/kafka_plugin_util'
5
+
6
+ require 'rdkafka'
7
+
8
+ # This is required for `rdkafka` version >= 0.12.0
9
+ # Overriding the close method in order to provide a time limit for when it should be forcibly closed
10
+ class Rdkafka::Producer::Client
11
+ # return false if producer is forcefully closed, otherwise return true
12
+ def close(timeout=nil)
13
+ return unless @native
14
+
15
+ # Indicate to polling thread that we're closing
16
+ @polling_thread[:closing] = true
17
+ # Wait for the polling thread to finish up
18
+ thread = @polling_thread.join(timeout)
19
+
20
+ Rdkafka::Bindings.rd_kafka_destroy(@native)
21
+
22
+ @native = nil
23
+
24
+ return !thread.nil?
25
+ end
26
+ end
27
+
28
+ class Rdkafka::Producer
29
+ # return false if producer is forcefully closed, otherwise return true
30
+ def close(timeout = nil)
31
+ rdkafka_version = Rdkafka::VERSION || '0.0.0'
32
+ # Rdkafka version >= 0.12.0 changed its internals
33
+ if Gem::Version::create(rdkafka_version) >= Gem::Version.create('0.12.0')
34
+ ObjectSpace.undefine_finalizer(self)
35
+
36
+ return @client.close(timeout)
37
+ end
38
+
39
+ @closing = true
40
+ # Wait for the polling thread to finish up
41
+ # If the broker isn't alive, the thread doesn't exit
42
+ if timeout
43
+ thr = @polling_thread.join(timeout)
44
+ return !!thr
45
+ else
46
+ @polling_thread.join
47
+ return true
48
+ end
49
+ end
50
+ end
51
+
52
+ module Fluent::Plugin
53
+ class Fluent::Rdkafka2Output < Output
54
+ Fluent::Plugin.register_output('rdkafka2', self)
55
+
56
+ helpers :inject, :formatter, :record_accessor
57
+
58
+ config_param :brokers, :string, :default => 'localhost:9092',
59
+ :desc => <<-DESC
60
+ Set brokers directly:
61
+ <broker1_host>:<broker1_port>,<broker2_host>:<broker2_port>,..
62
+ Brokers: you can choose to use either brokers or zookeeper.
63
+ DESC
64
+ config_param :topic, :string, :default => nil, :desc => "kafka topic. Placeholders are supported"
65
+ config_param :topic_key, :string, :default => 'topic', :desc => "Field for kafka topic"
66
+ config_param :default_topic, :string, :default => nil,
67
+ :desc => "Default output topic when record doesn't have topic field"
68
+ config_param :use_default_for_unknown_topic, :bool, :default => false, :desc => "If true, default_topic is used when topic not found"
69
+ config_param :use_default_for_unknown_partition_error, :bool, :default => false, :desc => "If true, default_topic is used when received unknown_partition error"
70
+ config_param :message_key_key, :string, :default => 'message_key', :desc => "Field for kafka message key"
71
+ config_param :default_message_key, :string, :default => nil
72
+ config_param :partition_key, :string, :default => 'partition', :desc => "Field for kafka partition"
73
+ config_param :default_partition, :integer, :default => nil
74
+ config_param :output_data_type, :string, :default => 'json', :obsoleted => "Use <format> section instead"
75
+ config_param :output_include_tag, :bool, :default => false, :obsoleted => "Use <inject> section instead"
76
+ config_param :output_include_time, :bool, :default => false, :obsoleted => "Use <inject> section instead"
77
+ config_param :exclude_partition, :bool, :default => false,
78
+ :desc => <<-DESC
79
+ Set true to remove partition from data
80
+ DESC
81
+ config_param :exclude_message_key, :bool, :default => false,
82
+ :desc => <<-DESC
83
+ Set true to remove message_key from data
84
+ DESC
85
+ config_param :exclude_topic_key, :bool, :default => false,
86
+ :desc => <<-DESC
87
+ Set true to remove topic key from data
88
+ DESC
89
+ config_param :exclude_fields, :array, :default => [], value_type: :string,
90
+ :desc => 'Fields to remove from data where the value is a jsonpath to a record value'
91
+ config_param :headers, :hash, default: {}, symbolize_keys: true, value_type: :string,
92
+ :desc => 'Kafka message headers'
93
+ config_param :headers_from_record, :hash, default: {}, symbolize_keys: true, value_type: :string,
94
+ :desc => 'Kafka message headers where the header value is a jsonpath to a record value'
95
+
96
+ config_param :max_send_retries, :integer, :default => 2,
97
+ :desc => "Number of times to retry sending of messages to a leader. Used for message.send.max.retries"
98
+ config_param :required_acks, :integer, :default => -1,
99
+ :desc => "The number of acks required per request. Used for request.required.acks"
100
+ config_param :ack_timeout, :time, :default => nil,
101
+ :desc => "How long the producer waits for acks. Used for request.timeout.ms"
102
+ config_param :compression_codec, :string, :default => nil,
103
+ :desc => <<-DESC
104
+ The codec the producer uses to compress messages. Used for compression.codec
105
+ Supported codecs: (gzip|snappy)
106
+ DESC
107
+ config_param :record_key, :string, :default => nil,
108
+ :desc => <<-DESC
109
+ A jsonpath to a record value pointing to the field which will be passed to the formatter and sent as the Kafka message payload.
110
+ If defined, only this field in the record will be sent to Kafka as the message payload.
111
+ DESC
112
+ config_param :use_event_time, :bool, :default => false, :desc => 'Use fluentd event time for rdkafka timestamp'
113
+ config_param :max_send_limit_bytes, :size, :default => nil
114
+ config_param :discard_kafka_delivery_failed, :bool, :default => false
115
+ config_param :rdkafka_buffering_max_ms, :integer, :default => nil, :desc => 'Used for queue.buffering.max.ms'
116
+ config_param :rdkafka_buffering_max_messages, :integer, :default => nil, :desc => 'Used for queue.buffering.max.messages'
117
+ config_param :rdkafka_message_max_bytes, :integer, :default => nil, :desc => 'Used for message.max.bytes'
118
+ config_param :rdkafka_message_max_num, :integer, :default => nil, :desc => 'Used for batch.num.messages'
119
+ config_param :rdkafka_delivery_handle_poll_timeout, :integer, :default => 30, :desc => 'Timeout for polling message wait'
120
+ config_param :rdkafka_options, :hash, :default => {}, :desc => 'Set any rdkafka configuration. See https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md'
121
+ config_param :share_producer, :bool, :default => false, :desc => 'share kafka producer between flush threads'
122
+
123
+ config_param :max_enqueue_retries, :integer, :default => 3
124
+ config_param :enqueue_retry_backoff, :integer, :default => 3
125
+ config_param :max_enqueue_bytes_per_second, :size, :default => nil, :desc => 'The maximum number of enqueueing bytes per second'
126
+
127
+ config_param :service_name, :string, :default => nil, :desc => 'Used for sasl.kerberos.service.name'
128
+
129
+ config_section :buffer do
130
+ config_set_default :chunk_keys, ["topic"]
131
+ end
132
+ config_section :format do
133
+ config_set_default :@type, 'json'
134
+ config_set_default :add_newline, false
135
+ end
136
+
137
+ include Fluent::KafkaPluginUtil::SSLSettings
138
+ include Fluent::KafkaPluginUtil::SaslSettings
139
+
140
+ class EnqueueRate
141
+ class LimitExceeded < StandardError
142
+ attr_reader :next_retry_clock
143
+ def initialize(next_retry_clock)
144
+ @next_retry_clock = next_retry_clock
145
+ end
146
+ end
147
+
148
+ def initialize(limit_bytes_per_second)
149
+ @mutex = Mutex.new
150
+ @start_clock = Fluent::Clock.now
151
+ @bytes_per_second = 0
152
+ @limit_bytes_per_second = limit_bytes_per_second
153
+ @commits = {}
154
+ end
155
+
156
+ def raise_if_limit_exceeded(bytes_to_enqueue)
157
+ return if @limit_bytes_per_second.nil?
158
+
159
+ @mutex.synchronize do
160
+ @commits[Thread.current] = {
161
+ clock: Fluent::Clock.now,
162
+ bytesize: bytes_to_enqueue,
163
+ }
164
+
165
+ @bytes_per_second += @commits[Thread.current][:bytesize]
166
+ duration = @commits[Thread.current][:clock] - @start_clock
167
+
168
+ if duration < 1.0
169
+ if @bytes_per_second > @limit_bytes_per_second
170
+ raise LimitExceeded.new(@start_clock + 1.0)
171
+ end
172
+ else
173
+ @start_clock = @commits[Thread.current][:clock]
174
+ @bytes_per_second = @commits[Thread.current][:bytesize]
175
+ end
176
+ end
177
+ end
178
+
179
+ def revert
180
+ return if @limit_bytes_per_second.nil?
181
+
182
+ @mutex.synchronize do
183
+ return unless @commits[Thread.current]
184
+ return unless @commits[Thread.current][:clock]
185
+ if @commits[Thread.current][:clock] >= @start_clock
186
+ @bytes_per_second -= @commits[Thread.current][:bytesize]
187
+ end
188
+ @commits[Thread.current] = nil
189
+ end
190
+ end
191
+ end
192
+
193
+ def initialize
194
+ super
195
+
196
+ @producers = nil
197
+ @producers_mutex = nil
198
+ @shared_producer = nil
199
+ @enqueue_rate = nil
200
+ @writing_threads_mutex = Mutex.new
201
+ @writing_threads = Set.new
202
+ end
203
+
204
+ def configure(conf)
205
+ super
206
+ log.instance_eval {
207
+ def add(level, message = nil)
208
+ if message.nil?
209
+ if block_given?
210
+ message = yield
211
+ else
212
+ return
213
+ end
214
+ end
215
+
216
+ # Follow rdkakfa's log level. See also rdkafka-ruby's bindings.rb: https://github.com/appsignal/rdkafka-ruby/blob/e5c7261e3f2637554a5c12b924be297d7dca1328/lib/rdkafka/bindings.rb#L117
217
+ case level
218
+ when Logger::FATAL
219
+ self.fatal(message)
220
+ when Logger::ERROR
221
+ self.error(message)
222
+ when Logger::WARN
223
+ self.warn(message)
224
+ when Logger::INFO
225
+ self.info(message)
226
+ when Logger::DEBUG
227
+ self.debug(message)
228
+ else
229
+ self.trace(message)
230
+ end
231
+ end
232
+ }
233
+ Rdkafka::Config.logger = log
234
+ config = build_config
235
+ @rdkafka = Rdkafka::Config.new(config)
236
+
237
+ if @default_topic.nil?
238
+ if @use_default_for_unknown_topic || @use_default_for_unknown_partition_error
239
+ raise Fluent::ConfigError, "default_topic must be set when use_default_for_unknown_topic or use_default_for_unknown_partition_error is true"
240
+ end
241
+ if @chunk_keys.include?(@topic_key) && !@chunk_key_tag
242
+ log.warn "Use '#{@topic_key}' field of event record for topic but no fallback. Recommend to set default_topic or set 'tag' in buffer chunk keys like <buffer #{@topic_key},tag>"
243
+ end
244
+ else
245
+ if @chunk_key_tag
246
+ log.warn "default_topic is set. Fluentd's event tag is not used for topic"
247
+ end
248
+ end
249
+
250
+ formatter_conf = conf.elements('format').first
251
+ unless formatter_conf
252
+ raise Fluent::ConfigError, "<format> section is required."
253
+ end
254
+ unless formatter_conf["@type"]
255
+ raise Fluent::ConfigError, "format/@type is required."
256
+ end
257
+ @formatter_proc = setup_formatter(formatter_conf)
258
+ @topic_key_sym = @topic_key.to_sym
259
+
260
+ @headers_from_record_accessors = {}
261
+ @headers_from_record.each do |key, value|
262
+ @headers_from_record_accessors[key] = record_accessor_create(value)
263
+ end
264
+
265
+ @exclude_field_accessors = @exclude_fields.map do |field|
266
+ record_accessor_create(field)
267
+ end
268
+
269
+ @enqueue_rate = EnqueueRate.new(@max_enqueue_bytes_per_second) unless @max_enqueue_bytes_per_second.nil?
270
+
271
+ @record_field_accessor = nil
272
+ @record_field_accessor = record_accessor_create(@record_key) unless @record_key.nil?
273
+ end
274
+
275
+ def build_config
276
+ config = {:"bootstrap.servers" => @brokers}
277
+
278
+ if @ssl_ca_cert && @ssl_ca_cert[0]
279
+ ssl = true
280
+ config[:"ssl.ca.location"] = @ssl_ca_cert[0]
281
+ config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
282
+ config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
283
+ config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
284
+ end
285
+
286
+ if @principal
287
+ sasl = true
288
+ config[:"sasl.mechanisms"] = "GSSAPI"
289
+ config[:"sasl.kerberos.principal"] = @principal
290
+ config[:"sasl.kerberos.service.name"] = @service_name if @service_name
291
+ config[:"sasl.kerberos.keytab"] = @keytab if @keytab
292
+ end
293
+
294
+ if ssl && sasl
295
+ security_protocol = "SASL_SSL"
296
+ elsif ssl && !sasl
297
+ security_protocol = "SSL"
298
+ elsif !ssl && sasl
299
+ security_protocol = "SASL_PLAINTEXT"
300
+ else
301
+ security_protocol = "PLAINTEXT"
302
+ end
303
+ config[:"security.protocol"] = security_protocol
304
+
305
+ config[:"compression.codec"] = @compression_codec if @compression_codec
306
+ config[:"message.send.max.retries"] = @max_send_retries if @max_send_retries
307
+ config[:"request.required.acks"] = @required_acks if @required_acks
308
+ config[:"request.timeout.ms"] = @ack_timeout * 1000 if @ack_timeout
309
+ config[:"queue.buffering.max.ms"] = @rdkafka_buffering_max_ms if @rdkafka_buffering_max_ms
310
+ config[:"queue.buffering.max.messages"] = @rdkafka_buffering_max_messages if @rdkafka_buffering_max_messages
311
+ config[:"message.max.bytes"] = @rdkafka_message_max_bytes if @rdkafka_message_max_bytes
312
+ config[:"batch.num.messages"] = @rdkafka_message_max_num if @rdkafka_message_max_num
313
+ config[:"sasl.username"] = @username if @username
314
+ config[:"sasl.password"] = @password if @password
315
+
316
+ @rdkafka_options.each { |k, v|
317
+ config[k.to_sym] = v
318
+ }
319
+
320
+ config
321
+ end
322
+
323
+ def start
324
+ if @share_producer
325
+ @shared_producer = @rdkafka.producer
326
+ else
327
+ @producers = {}
328
+ @producers_mutex = Mutex.new
329
+ end
330
+
331
+ super
332
+ end
333
+
334
+ def multi_workers_ready?
335
+ true
336
+ end
337
+
338
+ def wait_writing_threads
339
+ done = false
340
+ until done do
341
+ @writing_threads_mutex.synchronize do
342
+ done = true if @writing_threads.empty?
343
+ end
344
+ sleep(1) unless done
345
+ end
346
+ end
347
+
348
+ def shutdown
349
+ super
350
+ wait_writing_threads
351
+ shutdown_producers
352
+ end
353
+
354
+ def shutdown_producers
355
+ if @share_producer
356
+ close_producer(@shared_producer)
357
+ @shared_producer = nil
358
+ else
359
+ @producers_mutex.synchronize {
360
+ shutdown_threads = @producers.map { |key, producer|
361
+ th = Thread.new {
362
+ close_producer(producer)
363
+ }
364
+ th.abort_on_exception = true
365
+ th
366
+ }
367
+ shutdown_threads.each { |th| th.join }
368
+ @producers = {}
369
+ }
370
+ end
371
+ end
372
+
373
+ def close_producer(producer)
374
+ unless producer.close(10)
375
+ log.warn("Queue is forcefully closed after 10 seconds wait")
376
+ end
377
+ end
378
+
379
+ def get_producer
380
+ if @share_producer
381
+ @shared_producer
382
+ else
383
+ @producers_mutex.synchronize {
384
+ producer = @producers[Thread.current.object_id]
385
+ unless producer
386
+ producer = @rdkafka.producer
387
+ @producers[Thread.current.object_id] = producer
388
+ end
389
+ producer
390
+ }
391
+ end
392
+ end
393
+
394
+ def setup_formatter(conf)
395
+ type = conf['@type']
396
+ case type
397
+ when 'ltsv'
398
+ require 'ltsv'
399
+ Proc.new { |tag, time, record| LTSV.dump(record) }
400
+ else
401
+ @formatter = formatter_create(usage: 'rdkafka-plugin', conf: conf)
402
+ @formatter.method(:format)
403
+ end
404
+ end
405
+
406
+ def write(chunk)
407
+ @writing_threads_mutex.synchronize { @writing_threads.add(Thread.current) }
408
+ tag = chunk.metadata.tag
409
+ topic = if @topic
410
+ extract_placeholders(@topic, chunk)
411
+ else
412
+ (chunk.metadata.variables && chunk.metadata.variables[@topic_key_sym]) || @default_topic || tag
413
+ end
414
+
415
+ handlers = []
416
+
417
+ headers = @headers.clone
418
+
419
+ begin
420
+ producer = get_producer
421
+ chunk.msgpack_each { |time, record|
422
+ begin
423
+ record = inject_values_to_record(tag, time, record)
424
+ record.delete(@topic_key) if @exclude_topic_key
425
+ partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
426
+ message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
427
+
428
+ @headers_from_record_accessors.each do |key, header_accessor|
429
+ headers[key] = header_accessor.call(record)
430
+ end
431
+
432
+ unless @exclude_fields.empty?
433
+ @exclude_field_accessors.each do |exclude_field_acessor|
434
+ exclude_field_acessor.delete(record)
435
+ end
436
+ end
437
+
438
+ record = @record_field_accessor.call(record) unless @record_field_accessor.nil?
439
+ record_buf = @formatter_proc.call(tag, time, record)
440
+ record_buf_bytes = record_buf.bytesize
441
+ if @max_send_limit_bytes && record_buf_bytes > @max_send_limit_bytes
442
+ log.warn "record size exceeds max_send_limit_bytes. Skip event:", :time => time, :record_size => record_buf_bytes
443
+ log.debug "Skipped event:", :record => record
444
+ next
445
+ end
446
+ rescue StandardError => e
447
+ log.warn "unexpected error during format record. Skip broken event:", :error => e.to_s, :error_class => e.class.to_s, :time => time, :record => record
448
+ next
449
+ end
450
+
451
+ handler = enqueue_with_retry(producer, topic, record_buf, message_key, partition, headers, time)
452
+ if @rdkafka_delivery_handle_poll_timeout != 0
453
+ handlers << handler
454
+ end
455
+ }
456
+ handlers.each { |handler|
457
+ handler.wait(max_wait_timeout: @rdkafka_delivery_handle_poll_timeout)
458
+ }
459
+ end
460
+ rescue Exception => e
461
+ if @discard_kafka_delivery_failed
462
+ log.warn "Delivery failed. Discard events:", :error => e.to_s, :error_class => e.class.to_s, :tag => tag
463
+ else
464
+ log.warn "Send exception occurred: #{e} at #{e.backtrace.first}"
465
+ # Raise exception to retry sendind messages
466
+ raise e
467
+ end
468
+ ensure
469
+ @writing_threads_mutex.synchronize { @writing_threads.delete(Thread.current) }
470
+ end
471
+
472
+ def enqueue_with_retry(producer, topic, record_buf, message_key, partition, headers, time)
473
+ attempt = 0
474
+ actual_topic = topic
475
+
476
+ loop do
477
+ begin
478
+ @enqueue_rate.raise_if_limit_exceeded(record_buf.bytesize) if @enqueue_rate
479
+ return producer.produce(topic: actual_topic, payload: record_buf, key: message_key, partition: partition, headers: headers, timestamp: @use_event_time ? Time.at(time) : nil)
480
+ rescue EnqueueRate::LimitExceeded => e
481
+ @enqueue_rate.revert if @enqueue_rate
482
+ duration = e.next_retry_clock - Fluent::Clock.now
483
+ sleep(duration) if duration > 0.0
484
+ rescue Exception => e
485
+ @enqueue_rate.revert if @enqueue_rate
486
+
487
+ if !e.respond_to?(:code)
488
+ raise e
489
+ end
490
+
491
+ case e.code
492
+ when :queue_full
493
+ if attempt <= @max_enqueue_retries
494
+ log.warn "Failed to enqueue message; attempting retry #{attempt} of #{@max_enqueue_retries} after #{@enqueue_retry_backoff}s"
495
+ sleep @enqueue_retry_backoff
496
+ attempt += 1
497
+ else
498
+ raise "Failed to enqueue message although tried retry #{@max_enqueue_retries} times"
499
+ end
500
+ # https://github.com/confluentinc/librdkafka/blob/c282ba2423b2694052393c8edb0399a5ef471b3f/src/rdkafka.h#LL309C9-L309C41
501
+ # RD_KAFKA_RESP_ERR__UNKNOWN_TOPIC
502
+ when :unknown_topic
503
+ if @use_default_for_unknown_topic && actual_topic != @default_topic
504
+ log.debug "'#{actual_topic}' topic not found. Retry with '#{@default_topic}' topic"
505
+ actual_topic = @default_topic
506
+ retry
507
+ end
508
+ raise e
509
+ # https://github.com/confluentinc/librdkafka/blob/c282ba2423b2694052393c8edb0399a5ef471b3f/src/rdkafka.h#L305
510
+ # RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION
511
+ when :unknown_partition
512
+ if @use_default_for_unknown_partition_error && actual_topic != @default_topic
513
+ log.debug "failed writing to topic '#{actual_topic}' with error '#{e.to_s}'. Writing message to topic '#{@default_topic}'"
514
+ actual_topic = @default_topic
515
+ retry
516
+ end
517
+
518
+ raise e
519
+ else
520
+ raise e
521
+ end
522
+ end
523
+ end
524
+ end
525
+ end
526
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'test/unit/rr'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'fluent/test'
16
+ unless ENV.has_key?('VERBOSE')
17
+ nulllogger = Object.new
18
+ nulllogger.instance_eval {|obj|
19
+ def method_missing(method, *args)
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/plugin/out_kafka'
26
+ require 'fluent/plugin/out_kafka_buffered'
27
+ require 'fluent/plugin/out_kafka2'
28
+ require 'fluent/plugin/in_kafka'
29
+ require 'fluent/plugin/in_kafka_group'
30
+
31
+ require "fluent/test/driver/output"
32
+
33
+ class Test::Unit::TestCase
34
+ end
@@ -0,0 +1,66 @@
1
+ require 'helper'
2
+ require 'fluent/test/driver/input'
3
+ require 'securerandom'
4
+
5
+ class KafkaInputTest < Test::Unit::TestCase
6
+ def setup
7
+ Fluent::Test.setup
8
+ end
9
+
10
+ TOPIC_NAME = "kafka-input-#{SecureRandom.uuid}"
11
+
12
+ CONFIG = %[
13
+ @type kafka
14
+ brokers localhost:9092
15
+ format text
16
+ @label @kafka
17
+ topics #{TOPIC_NAME}
18
+ ]
19
+
20
+ def create_driver(conf = CONFIG)
21
+ Fluent::Test::Driver::Input.new(Fluent::KafkaInput).configure(conf)
22
+ end
23
+
24
+
25
+ def test_configure
26
+ d = create_driver
27
+ assert_equal TOPIC_NAME, d.instance.topics
28
+ assert_equal 'text', d.instance.format
29
+ assert_equal 'localhost:9092', d.instance.brokers
30
+ end
31
+
32
+ def test_multi_worker_support
33
+ d = create_driver
34
+ assert_false d.instance.multi_workers_ready?
35
+ end
36
+
37
+ class ConsumeTest < self
38
+ def setup
39
+ @kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
40
+ @producer = @kafka.producer
41
+ end
42
+
43
+ def teardown
44
+ @kafka.delete_topic(TOPIC_NAME)
45
+ @kafka.close
46
+ end
47
+
48
+ def test_consume
49
+ conf = %[
50
+ @type kafka
51
+ brokers localhost:9092
52
+ format text
53
+ @label @kafka
54
+ topics #{TOPIC_NAME}
55
+ ]
56
+ d = create_driver
57
+
58
+ d.run(expect_records: 1, timeout: 10) do
59
+ @producer.produce("Hello, fluent-plugin-kafka!", topic: TOPIC_NAME)
60
+ @producer.deliver_messages
61
+ end
62
+ expected = {'message' => 'Hello, fluent-plugin-kafka!'}
63
+ assert_equal expected, d.events[0][2]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ require 'helper'
2
+ require 'fluent/test/driver/input'
3
+ require 'securerandom'
4
+
5
+ class KafkaGroupInputTest < Test::Unit::TestCase
6
+ def setup
7
+ Fluent::Test.setup
8
+ end
9
+
10
+ TOPIC_NAME = "kafka-input-#{SecureRandom.uuid}"
11
+
12
+ CONFIG = %[
13
+ @type kafka
14
+ brokers localhost:9092
15
+ consumer_group fluentd
16
+ format text
17
+ refresh_topic_interval 0
18
+ @label @kafka
19
+ topics #{TOPIC_NAME}
20
+ ]
21
+
22
+ def create_driver(conf = CONFIG)
23
+ Fluent::Test::Driver::Input.new(Fluent::KafkaGroupInput).configure(conf)
24
+ end
25
+
26
+
27
+ def test_configure
28
+ d = create_driver
29
+ assert_equal [TOPIC_NAME], d.instance.topics
30
+ assert_equal 'text', d.instance.format
31
+ assert_equal 'localhost:9092', d.instance.brokers
32
+ end
33
+
34
+ def test_multi_worker_support
35
+ d = create_driver
36
+ assert_true d.instance.multi_workers_ready?
37
+ end
38
+
39
+ class ConsumeTest < self
40
+ def setup
41
+ @kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
42
+ @producer = @kafka.producer
43
+ end
44
+
45
+ def teardown
46
+ @kafka.delete_topic(TOPIC_NAME)
47
+ @kafka.close
48
+ end
49
+
50
+ def test_consume
51
+ conf = %[
52
+ @type kafka
53
+ brokers localhost:9092
54
+ format text
55
+ @label @kafka
56
+ refresh_topic_interval 0
57
+ topics #{TOPIC_NAME}
58
+ ]
59
+ d = create_driver
60
+
61
+ d.run(expect_records: 1, timeout: 10) do
62
+ @producer.produce("Hello, fluent-plugin-kafka!", topic: TOPIC_NAME)
63
+ @producer.deliver_messages
64
+ end
65
+ expected = {'message' => 'Hello, fluent-plugin-kafka!'}
66
+ assert_equal expected, d.events[0][2]
67
+ end
68
+ end
69
+ end