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.
- 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
|