fluent-plugin-kafka-custom-ruby-version 0.9.3

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.
@@ -0,0 +1,257 @@
1
+ require 'fluent/plugin/output'
2
+ require 'fluent/plugin/kafka_plugin_util'
3
+
4
+ require 'kafka'
5
+ require 'fluent/plugin/kafka_producer_ext'
6
+
7
+ module Fluent::Plugin
8
+ class Fluent::Kafka2Output < Output
9
+ Fluent::Plugin.register_output('kafka2', self)
10
+
11
+ helpers :inject, :formatter, :event_emitter
12
+
13
+ config_param :brokers, :array, :value_type => :string, :default => ['localhost:9092'],
14
+ :desc => <<-DESC
15
+ Set brokers directly:
16
+ <broker1_host>:<broker1_port>,<broker2_host>:<broker2_port>,..
17
+ DESC
18
+ config_param :topic_key, :string, :default => 'topic', :desc => "Field for kafka topic"
19
+ config_param :default_topic, :string, :default => nil,
20
+ :desc => "Default output topic when record doesn't have topic field"
21
+ config_param :message_key_key, :string, :default => 'message_key', :desc => "Field for kafka message key"
22
+ config_param :default_message_key, :string, :default => nil
23
+ config_param :partition_key_key, :string, :default => 'partition_key', :desc => "Field for kafka partition key"
24
+ config_param :default_partition_key, :string, :default => nil
25
+ config_param :partition_key, :string, :default => 'partition', :desc => "Field for kafka partition"
26
+ config_param :default_partition, :integer, :default => nil
27
+ config_param :use_default_for_unknown_topic, :bool, :default => false, :desc => "If true, default_topic is used when topic not found"
28
+ config_param :client_id, :string, :default => 'fluentd'
29
+ config_param :idempotent, :bool, :default => false, :desc => 'Enable idempotent producer'
30
+ config_param :sasl_over_ssl, :bool, :default => true,
31
+ :desc => <<-DESC
32
+ Set to false to prevent SSL strict mode when using SASL authentication
33
+ DESC
34
+ config_param :exclude_partition_key, :bool, :default => false,
35
+ :desc => 'Set true to remove partition key from data'
36
+ config_param :exclude_partition, :bool, :default => false,
37
+ :desc => 'Set true to remove partition from data'
38
+ config_param :exclude_message_key, :bool, :default => false,
39
+ :desc => 'Set true to remove partition key from data'
40
+ config_param :exclude_topic_key, :bool, :default => false,
41
+ :desc => 'Set true to remove topic name key from data'
42
+
43
+ config_param :get_kafka_client_log, :bool, :default => false
44
+
45
+ config_param :ignore_exceptions, :array, :default => [], value_type: :string, :desc => "Ignorable exception list"
46
+ config_param :exception_backup, :bool, :default => true, :desc => "Chunk backup flag when ignore exception occured"
47
+
48
+ # ruby-kafka producer options
49
+ config_param :max_send_retries, :integer, :default => 2,
50
+ :desc => "Number of times to retry sending of messages to a leader."
51
+ config_param :required_acks, :integer, :default => -1,
52
+ :desc => "The number of acks required per request."
53
+ config_param :ack_timeout, :time, :default => nil,
54
+ :desc => "How long the producer waits for acks."
55
+ config_param :compression_codec, :string, :default => nil,
56
+ :desc => <<-DESC
57
+ The codec the producer uses to compress messages.
58
+ Supported codecs: (gzip|snappy)
59
+ DESC
60
+
61
+ config_param :active_support_notification_regex, :string, :default => nil,
62
+ :desc => <<-DESC
63
+ Add a regular expression to capture ActiveSupport notifications from the Kafka client
64
+ requires activesupport gem - records will be generated under fluent_kafka_stats.**
65
+ DESC
66
+
67
+ config_section :buffer do
68
+ config_set_default :chunk_keys, ["topic"]
69
+ end
70
+ config_section :format do
71
+ config_set_default :@type, 'json'
72
+ end
73
+
74
+ include Fluent::KafkaPluginUtil::SSLSettings
75
+ include Fluent::KafkaPluginUtil::SaslSettings
76
+
77
+ def initialize
78
+ super
79
+
80
+ @kafka = nil
81
+ end
82
+
83
+ def refresh_client(raise_error = true)
84
+ begin
85
+ logger = @get_kafka_client_log ? log : nil
86
+ if @scram_mechanism != nil && @username != nil && @password != nil
87
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
88
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_client_cert_chain: read_ssl_file(@ssl_client_cert_chain),
89
+ ssl_ca_certs_from_system: @ssl_ca_certs_from_system, sasl_scram_username: @username, sasl_scram_password: @password,
90
+ sasl_scram_mechanism: @scram_mechanism, sasl_over_ssl: @sasl_over_ssl)
91
+ elsif @username != nil && @password != nil
92
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
93
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_client_cert_chain: read_ssl_file(@ssl_client_cert_chain),
94
+ ssl_ca_certs_from_system: @ssl_ca_certs_from_system, sasl_plain_username: @username, sasl_plain_password: @password, sasl_over_ssl: @sasl_over_ssl)
95
+ else
96
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
97
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_client_cert_chain: read_ssl_file(@ssl_client_cert_chain),
98
+ ssl_ca_certs_from_system: @ssl_ca_certs_from_system, sasl_gssapi_principal: @principal, sasl_gssapi_keytab: @keytab)
99
+ end
100
+ log.info "initialized kafka producer: #{@client_id}"
101
+ rescue Exception => e
102
+ if raise_error # During startup, error should be reported to engine and stop its phase for safety.
103
+ raise e
104
+ else
105
+ log.error e
106
+ end
107
+ end
108
+ end
109
+
110
+ def configure(conf)
111
+ super
112
+
113
+ if @brokers.size > 0
114
+ @seed_brokers = @brokers
115
+ log.info "brokers has been set: #{@seed_brokers}"
116
+ else
117
+ raise Fluent::Config, 'No brokers specified. Need one broker at least.'
118
+ end
119
+
120
+ formatter_conf = conf.elements('format').first
121
+ unless formatter_conf
122
+ raise Fluent::ConfigError, "<format> section is required."
123
+ end
124
+ unless formatter_conf["@type"]
125
+ raise Fluent::ConfigError, "format/@type is required."
126
+ end
127
+ @formatter_proc = setup_formatter(formatter_conf)
128
+
129
+ if @default_topic.nil?
130
+ if @use_default_for_unknown_topic
131
+ raise Fluent::ConfigError, "default_topic must be set when use_default_for_unknown_topic is true"
132
+ end
133
+ if @chunk_keys.include?('topic') && !@chunk_key_tag
134
+ log.warn "Use 'topic' field of event record for topic but no fallback. Recommend to set default_topic or set 'tag' in buffer chunk keys like <buffer topic,tag>"
135
+ end
136
+ else
137
+ if @chunk_key_tag
138
+ log.warn "default_topic is set. Fluentd's event tag is not used for topic"
139
+ end
140
+ end
141
+
142
+ @producer_opts = {max_retries: @max_send_retries, required_acks: @required_acks, idempotent: @idempotent}
143
+ @producer_opts[:ack_timeout] = @ack_timeout if @ack_timeout
144
+ @producer_opts[:compression_codec] = @compression_codec.to_sym if @compression_codec
145
+ if @active_support_notification_regex
146
+ require 'active_support/notifications'
147
+ require 'active_support/core_ext/hash/keys'
148
+ ActiveSupport::Notifications.subscribe(Regexp.new(@active_support_notification_regex)) do |*args|
149
+ event = ActiveSupport::Notifications::Event.new(*args)
150
+ message = event.payload.respond_to?(:stringify_keys) ? event.payload.stringify_keys : event.payload
151
+ @router.emit("fluent_kafka_stats.#{event.name}", Time.now.to_i, message)
152
+ end
153
+ end
154
+
155
+ @topic_key_sym = @topic_key.to_sym
156
+ end
157
+
158
+ def multi_workers_ready?
159
+ true
160
+ end
161
+
162
+ def start
163
+ super
164
+ refresh_client
165
+ end
166
+
167
+ def close
168
+ super
169
+ @kafka.close if @kafka
170
+ end
171
+
172
+ def terminate
173
+ super
174
+ @kafka = nil
175
+ end
176
+
177
+ def setup_formatter(conf)
178
+ type = conf['@type']
179
+ case type
180
+ when 'json'
181
+ begin
182
+ require 'oj'
183
+ Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
184
+ Proc.new { |tag, time, record| Oj.dump(record) }
185
+ rescue LoadError
186
+ require 'yajl'
187
+ Proc.new { |tag, time, record| Yajl::Encoder.encode(record) }
188
+ end
189
+ when 'ltsv'
190
+ require 'ltsv'
191
+ Proc.new { |tag, time, record| LTSV.dump(record) }
192
+ else
193
+ @formatter = formatter_create(usage: 'kafka-plugin', conf: conf)
194
+ @formatter.method(:format)
195
+ end
196
+ end
197
+
198
+ # TODO: optimize write performance
199
+ def write(chunk)
200
+ tag = chunk.metadata.tag
201
+ topic = chunk.metadata.variables[@topic_key_sym] || @default_topic || tag
202
+
203
+ messages = 0
204
+ record_buf = nil
205
+
206
+ begin
207
+ producer = @kafka.topic_producer(topic, @producer_opts)
208
+ chunk.msgpack_each { |time, record|
209
+ begin
210
+ record = inject_values_to_record(tag, time, record)
211
+ record.delete(@topic_key) if @exclude_topic_key
212
+ partition_key = (@exclude_partition_key ? record.delete(@partition_key_key) : record[@partition_key_key]) || @default_partition_key
213
+ partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
214
+ message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
215
+
216
+ record_buf = @formatter_proc.call(tag, time, record)
217
+ rescue StandardError => e
218
+ log.warn "unexpected error during format record. Skip broken event:", :error => e.to_s, :error_class => e.class.to_s, :time => time, :record => record
219
+ next
220
+ end
221
+
222
+ log.trace { "message will send to #{topic} with partition_key: #{partition_key}, partition: #{partition}, message_key: #{message_key} and value: #{record_buf}." }
223
+ messages += 1
224
+
225
+ producer.produce(record_buf, key: message_key, partition_key: partition_key, partition: partition)
226
+ }
227
+
228
+ if messages > 0
229
+ log.debug { "#{messages} messages send." }
230
+ producer.deliver_messages
231
+ end
232
+ rescue Kafka::UnknownTopicOrPartition
233
+ if @use_default_for_unknown_topic && topic != @default_topic
234
+ producer.shutdown if producer
235
+ log.warn "'#{topic}' topic not found. Retry with '#{default_topic}' topic"
236
+ topic = @default_topic
237
+ retry
238
+ end
239
+ raise
240
+ end
241
+ rescue Exception => e
242
+ ignore = @ignore_exceptions.include?(e.class.name)
243
+
244
+ log.warn "Send exception occurred: #{e}"
245
+ log.warn "Exception Backtrace : #{e.backtrace.join("\n")}"
246
+ log.warn "Exception ignored in tag : #{tag}" if ignore
247
+ # For safety, refresh client and its producers
248
+ refresh_client(false)
249
+ # raise UnrecoverableError for backup ignored exception chunk
250
+ raise Fluent::UnrecoverableError if ignore && exception_backup
251
+ # Raise exception to retry sendind messages
252
+ raise e unless ignore
253
+ ensure
254
+ producer.shutdown if producer
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,362 @@
1
+ require 'thread'
2
+ require 'fluent/output'
3
+ require 'fluent/plugin/kafka_plugin_util'
4
+
5
+ class Fluent::KafkaOutputBuffered < Fluent::BufferedOutput
6
+ Fluent::Plugin.register_output('kafka_buffered', self)
7
+
8
+ config_param :brokers, :string, :default => 'localhost:9092',
9
+ :desc => <<-DESC
10
+ Set brokers directly:
11
+ <broker1_host>:<broker1_port>,<broker2_host>:<broker2_port>,..
12
+ Brokers: you can choose to use either brokers or zookeeper.
13
+ DESC
14
+ config_param :zookeeper, :string, :default => nil,
15
+ :desc => <<-DESC
16
+ Set brokers via Zookeeper:
17
+ <zookeeper_host>:<zookeeper_port>
18
+ DESC
19
+ config_param :zookeeper_path, :string, :default => '/brokers/ids', :desc => "Path in path for Broker id. Default to /brokers/ids"
20
+
21
+ config_param :topic_key, :string, :default => 'topic', :desc => "Field for kafka topic"
22
+ config_param :default_topic, :string, :default => nil, :desc => "Default output topic when record doesn't have topic field"
23
+ config_param :message_key_key, :string, :default => 'message_key', :desc => "Field for kafka message key"
24
+ config_param :default_message_key, :string, :default => nil
25
+ config_param :partition_key_key, :string, :default => 'partition_key', :desc => "Field for kafka partition key"
26
+ config_param :default_partition_key, :string, :default => nil
27
+ config_param :partition_key, :string, :default => 'partition', :desc => "Field for kafka partition"
28
+ config_param :default_partition, :integer, :default => nil
29
+ config_param :client_id, :string, :default => 'kafka'
30
+ config_param :idempotent, :bool, :default => false, :desc => 'Enable idempotent producer'
31
+ config_param :sasl_over_ssl, :bool, :default => true,
32
+ :desc => <<-DESC
33
+ Set to false to prevent SSL strict mode when using SASL authentication
34
+ DESC
35
+ config_param :output_data_type, :string, :default => 'json',
36
+ :desc => <<-DESC
37
+ Supported format: (json|ltsv|msgpack|attr:<record name>|<formatter name>)
38
+ DESC
39
+ config_param :output_include_tag, :bool, :default => false
40
+ config_param :output_include_time, :bool, :default => false
41
+ config_param :exclude_partition_key, :bool, :default => false,
42
+ :desc => <<-DESC
43
+ Set true to remove partition key from data
44
+ DESC
45
+ config_param :exclude_partition, :bool, :default => false,
46
+ :desc => <<-DESC
47
+ Set true to remove partition from data
48
+ DESC
49
+ config_param :exclude_message_key, :bool, :default => false,
50
+ :desc => <<-DESC
51
+ Set true to remove message key from data
52
+ DESC
53
+ config_param :exclude_topic_key, :bool, :default => false,
54
+ :desc => <<-DESC
55
+ Set true to remove topic name key from data
56
+ DESC
57
+
58
+ config_param :kafka_agg_max_bytes, :size, :default => 4*1024 #4k
59
+ config_param :kafka_agg_max_messages, :integer, :default => nil
60
+ config_param :get_kafka_client_log, :bool, :default => false
61
+
62
+ # ruby-kafka producer options
63
+ config_param :max_send_retries, :integer, :default => 2,
64
+ :desc => "Number of times to retry sending of messages to a leader."
65
+ config_param :required_acks, :integer, :default => -1,
66
+ :desc => "The number of acks required per request."
67
+ config_param :ack_timeout, :time, :default => nil,
68
+ :desc => "How long the producer waits for acks."
69
+ config_param :compression_codec, :string, :default => nil,
70
+ :desc => <<-DESC
71
+ The codec the producer uses to compress messages.
72
+ Supported codecs: (gzip|snappy)
73
+ DESC
74
+ config_param :max_send_limit_bytes, :size, :default => nil
75
+ config_param :discard_kafka_delivery_failed, :bool, :default => false
76
+
77
+ config_param :time_format, :string, :default => nil
78
+
79
+ config_param :active_support_notification_regex, :string, :default => nil,
80
+ :desc => <<-DESC
81
+ Add a regular expression to capture ActiveSupport notifications from the Kafka client
82
+ requires activesupport gem - records will be generated under fluent_kafka_stats.**
83
+ DESC
84
+
85
+ config_param :monitoring_list, :array, :default => [],
86
+ :desc => "library to be used to monitor. statsd and datadog are supported"
87
+
88
+ include Fluent::KafkaPluginUtil::SSLSettings
89
+ include Fluent::KafkaPluginUtil::SaslSettings
90
+
91
+ attr_accessor :output_data_type
92
+ attr_accessor :field_separator
93
+
94
+ unless method_defined?(:log)
95
+ define_method("log") { $log }
96
+ end
97
+
98
+ def initialize
99
+ super
100
+
101
+ require 'kafka'
102
+ require 'fluent/plugin/kafka_producer_ext'
103
+
104
+ @kafka = nil
105
+ @producers = {}
106
+ @producers_mutex = Mutex.new
107
+ end
108
+
109
+ def multi_workers_ready?
110
+ true
111
+ end
112
+
113
+ def refresh_client(raise_error = true)
114
+ if @zookeeper
115
+ @seed_brokers = []
116
+ z = Zookeeper.new(@zookeeper)
117
+ z.get_children(:path => @zookeeper_path)[:children].each do |id|
118
+ broker = Yajl.load(z.get(:path => @zookeeper_path + "/#{id}")[:data])
119
+ if @ssl_client_cert
120
+ @seed_brokers.push(pickup_ssl_endpoint(broker))
121
+ else
122
+ @seed_brokers.push("#{broker['host']}:#{broker['port']}")
123
+ end
124
+ end
125
+ z.close
126
+ log.info "brokers has been refreshed via Zookeeper: #{@seed_brokers}"
127
+ end
128
+ begin
129
+ if @seed_brokers.length > 0
130
+ logger = @get_kafka_client_log ? log : nil
131
+ if @scram_mechanism != nil && @username != nil && @password != nil
132
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
133
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_ca_certs_from_system: @ssl_ca_certs_from_system,
134
+ sasl_scram_username: @username, sasl_scram_password: @password, sasl_scram_mechanism: @scram_mechanism, sasl_over_ssl: @sasl_over_ssl)
135
+ elsif @username != nil && @password != nil
136
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
137
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_ca_certs_from_system: @ssl_ca_certs_from_system,
138
+ sasl_plain_username: @username, sasl_plain_password: @password, sasl_over_ssl: @sasl_over_ssl)
139
+ else
140
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, logger: logger, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
141
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key), ssl_ca_certs_from_system: @ssl_ca_certs_from_system,
142
+ sasl_gssapi_principal: @principal, sasl_gssapi_keytab: @keytab)
143
+ end
144
+ log.info "initialized kafka producer: #{@client_id}"
145
+ else
146
+ log.warn "No brokers found on Zookeeper"
147
+ end
148
+ rescue Exception => e
149
+ if raise_error # During startup, error should be reported to engine and stop its phase for safety.
150
+ raise e
151
+ else
152
+ log.error e
153
+ end
154
+ end
155
+ end
156
+
157
+ def configure(conf)
158
+ super
159
+
160
+ if @zookeeper
161
+ require 'zookeeper'
162
+ else
163
+ @seed_brokers = @brokers.split(",")
164
+ log.info "brokers has been set directly: #{@seed_brokers}"
165
+ end
166
+
167
+ if conf['ack_timeout_ms']
168
+ log.warn "'ack_timeout_ms' parameter is deprecated. Use second unit 'ack_timeout' instead"
169
+ @ack_timeout = conf['ack_timeout_ms'].to_i / 1000
170
+ end
171
+
172
+ @f_separator = case @field_separator
173
+ when /SPACE/i then ' '
174
+ when /COMMA/i then ','
175
+ when /SOH/i then "\x01"
176
+ else "\t"
177
+ end
178
+
179
+ @formatter_proc = setup_formatter(conf)
180
+
181
+ @producer_opts = {max_retries: @max_send_retries, required_acks: @required_acks, idempotent: @idempotent}
182
+ @producer_opts[:ack_timeout] = @ack_timeout if @ack_timeout
183
+ @producer_opts[:compression_codec] = @compression_codec.to_sym if @compression_codec
184
+
185
+ if @discard_kafka_delivery_failed
186
+ log.warn "'discard_kafka_delivery_failed' option discards events which cause delivery failure, e.g. invalid topic or something."
187
+ log.warn "If this is unexpected, you need to check your configuration or data."
188
+ end
189
+
190
+ if @active_support_notification_regex
191
+ require 'active_support/notifications'
192
+ require 'active_support/core_ext/hash/keys'
193
+ ActiveSupport::Notifications.subscribe(Regexp.new(@active_support_notification_regex)) do |*args|
194
+ event = ActiveSupport::Notifications::Event.new(*args)
195
+ message = event.payload.respond_to?(:stringify_keys) ? event.payload.stringify_keys : event.payload
196
+ @router.emit("fluent_kafka_stats.#{event.name}", Time.now.to_i, message)
197
+ end
198
+ end
199
+
200
+ @monitoring_list.each { |m|
201
+ require "kafka/#{m}"
202
+ log.info "#{m} monitoring started"
203
+ }
204
+ end
205
+
206
+ def start
207
+ super
208
+ refresh_client
209
+ end
210
+
211
+ def shutdown
212
+ super
213
+ shutdown_producers
214
+ @kafka = nil
215
+ end
216
+
217
+ def emit(tag, es, chain)
218
+ super(tag, es, chain, tag)
219
+ end
220
+
221
+ def format_stream(tag, es)
222
+ es.to_msgpack_stream
223
+ end
224
+
225
+ def shutdown_producers
226
+ @producers_mutex.synchronize {
227
+ @producers.each { |key, producer|
228
+ producer.shutdown
229
+ }
230
+ @producers = {}
231
+ }
232
+ end
233
+
234
+ def get_producer
235
+ @producers_mutex.synchronize {
236
+ producer = @producers[Thread.current.object_id]
237
+ unless producer
238
+ producer = @kafka.producer(@producer_opts)
239
+ @producers[Thread.current.object_id] = producer
240
+ end
241
+ producer
242
+ }
243
+ end
244
+
245
+ def setup_formatter(conf)
246
+ if @output_data_type == 'json'
247
+ begin
248
+ require 'oj'
249
+ Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
250
+ Proc.new { |tag, time, record| Oj.dump(record) }
251
+ rescue LoadError
252
+ require 'yajl'
253
+ Proc.new { |tag, time, record| Yajl::Encoder.encode(record) }
254
+ end
255
+ elsif @output_data_type == 'ltsv'
256
+ require 'ltsv'
257
+ Proc.new { |tag, time, record| LTSV.dump(record) }
258
+ elsif @output_data_type == 'msgpack'
259
+ require 'msgpack'
260
+ Proc.new { |tag, time, record| record.to_msgpack }
261
+ elsif @output_data_type =~ /^attr:(.*)$/
262
+ @custom_attributes = $1.split(',').map(&:strip).reject(&:empty?)
263
+ @custom_attributes.unshift('time') if @output_include_time
264
+ @custom_attributes.unshift('tag') if @output_include_tag
265
+ Proc.new { |tag, time, record|
266
+ @custom_attributes.map { |attr|
267
+ record[attr].nil? ? '' : record[attr].to_s
268
+ }.join(@f_separator)
269
+ }
270
+ else
271
+ @formatter = Fluent::Plugin.new_formatter(@output_data_type)
272
+ @formatter.configure(conf)
273
+ @formatter.method(:format)
274
+ end
275
+ end
276
+
277
+ def deliver_messages(producer, tag)
278
+ if @discard_kafka_delivery_failed
279
+ begin
280
+ producer.deliver_messages
281
+ rescue Kafka::DeliveryFailed => e
282
+ log.warn "DeliveryFailed occurred. Discard broken event:", :error => e.to_s, :error_class => e.class.to_s, :tag => tag
283
+ producer.clear_buffer
284
+ end
285
+ else
286
+ producer.deliver_messages
287
+ end
288
+ end
289
+
290
+ def write(chunk)
291
+ tag = chunk.key
292
+ def_topic = @default_topic || tag
293
+ producer = get_producer
294
+
295
+ records_by_topic = {}
296
+ bytes_by_topic = {}
297
+ messages = 0
298
+ messages_bytes = 0
299
+ record_buf = nil
300
+ record_buf_bytes = nil
301
+
302
+ begin
303
+ chunk.msgpack_each { |time, record|
304
+ begin
305
+ if @output_include_time
306
+ if @time_format
307
+ record['time'.freeze] = Time.at(time).strftime(@time_format)
308
+ else
309
+ record['time'.freeze] = time
310
+ end
311
+ end
312
+
313
+ record['tag'] = tag if @output_include_tag
314
+ topic = (@exclude_topic_key ? record.delete(@topic_key) : record[@topic_key]) || def_topic
315
+ partition_key = (@exclude_partition_key ? record.delete(@partition_key_key) : record[@partition_key_key]) || @default_partition_key
316
+ partition = (@exclude_partition ? record.delete(@partition) : record[@partition]) || @default_partition
317
+ message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
318
+
319
+ records_by_topic[topic] ||= 0
320
+ bytes_by_topic[topic] ||= 0
321
+
322
+ record_buf = @formatter_proc.call(tag, time, record)
323
+ record_buf_bytes = record_buf.bytesize
324
+ if @max_send_limit_bytes && record_buf_bytes > @max_send_limit_bytes
325
+ log.warn "record size exceeds max_send_limit_bytes. Skip event:", :time => time, :record => record
326
+ next
327
+ end
328
+ rescue StandardError => e
329
+ log.warn "unexpected error during format record. Skip broken event:", :error => e.to_s, :error_class => e.class.to_s, :time => time, :record => record
330
+ next
331
+ end
332
+
333
+ if (messages > 0) and (messages_bytes + record_buf_bytes > @kafka_agg_max_bytes) or (@kafka_agg_max_messages && messages >= @kafka_agg_max_messages)
334
+ log.debug { "#{messages} messages send because reaches the limit of batch transmission." }
335
+ deliver_messages(producer, tag)
336
+ messages = 0
337
+ messages_bytes = 0
338
+ end
339
+ log.trace { "message will send to #{topic} with partition_key: #{partition_key}, partition: #{partition}, message_key: #{message_key} and value: #{record_buf}." }
340
+ messages += 1
341
+ producer.produce_for_buffered(record_buf, topic: topic, key: message_key, partition_key: partition_key, partition: partition)
342
+ messages_bytes += record_buf_bytes
343
+
344
+ records_by_topic[topic] += 1
345
+ bytes_by_topic[topic] += record_buf_bytes
346
+ }
347
+ if messages > 0
348
+ log.debug { "#{messages} messages send." }
349
+ deliver_messages(producer, tag)
350
+ end
351
+ log.debug { "(records|bytes) (#{records_by_topic}|#{bytes_by_topic})" }
352
+ end
353
+ rescue Exception => e
354
+ log.warn "Send exception occurred: #{e}"
355
+ log.warn "Exception Backtrace : #{e.backtrace.join("\n")}"
356
+ # For safety, refresh client and its producers
357
+ shutdown_producers
358
+ refresh_client(false)
359
+ # Raise exception to retry sendind messages
360
+ raise e
361
+ end
362
+ end