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

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