fluent-plugin-kafka-xst 0.19.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
- data/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yaml +39 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/linux.yml +45 -0
- data/.github/workflows/stale-actions.yml +24 -0
- data/.gitignore +2 -0
- data/ChangeLog +344 -0
- data/Gemfile +6 -0
- data/LICENSE +14 -0
- data/README.md +594 -0
- data/Rakefile +12 -0
- data/ci/prepare-kafka-server.sh +33 -0
- data/examples/README.md +3 -0
- data/examples/out_kafka2/dynamic_topic_based_on_tag.conf +32 -0
- data/examples/out_kafka2/protobuf-formatter.conf +23 -0
- data/examples/out_kafka2/record_key.conf +31 -0
- data/fluent-plugin-kafka.gemspec +27 -0
- data/lib/fluent/plugin/in_kafka.rb +388 -0
- data/lib/fluent/plugin/in_kafka_group.rb +394 -0
- data/lib/fluent/plugin/in_rdkafka_group.rb +305 -0
- data/lib/fluent/plugin/kafka_plugin_util.rb +84 -0
- data/lib/fluent/plugin/kafka_producer_ext.rb +308 -0
- data/lib/fluent/plugin/out_kafka.rb +268 -0
- data/lib/fluent/plugin/out_kafka2.rb +427 -0
- data/lib/fluent/plugin/out_kafka_buffered.rb +374 -0
- data/lib/fluent/plugin/out_rdkafka.rb +324 -0
- data/lib/fluent/plugin/out_rdkafka2.rb +526 -0
- data/test/helper.rb +34 -0
- data/test/plugin/test_in_kafka.rb +66 -0
- data/test/plugin/test_in_kafka_group.rb +69 -0
- data/test/plugin/test_kafka_plugin_util.rb +44 -0
- data/test/plugin/test_out_kafka.rb +68 -0
- data/test/plugin/test_out_kafka2.rb +138 -0
- data/test/plugin/test_out_kafka_buffered.rb +68 -0
- data/test/plugin/test_out_rdkafka2.rb +182 -0
- 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
|