fluent-plugin-kafka-xst 0.19.1

Sign up to get free protection for your applications and to get access to all the features.
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