roched-fluent-plugin-kafka 0.6.5

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,223 @@
1
+ require "set"
2
+ require "kafka/partitioner"
3
+ require "kafka/message_buffer"
4
+ require "kafka/produce_operation"
5
+ require "kafka/pending_message_queue"
6
+ require "kafka/pending_message"
7
+ require "kafka/compressor"
8
+ require 'kafka/producer'
9
+
10
+ # for out_kafka_buffered
11
+ module Kafka
12
+ class Producer
13
+ def produce2(value, key: nil, topic:, partition: nil, partition_key: nil)
14
+ create_time = Time.now
15
+
16
+ message = PendingMessage.new(
17
+ value,
18
+ key,
19
+ topic,
20
+ partition,
21
+ partition_key,
22
+ create_time
23
+ )
24
+
25
+ @target_topics.add(topic)
26
+ @pending_message_queue.write(message)
27
+
28
+ nil
29
+ end
30
+ end
31
+ end
32
+
33
+ # for out_kafka2
34
+ module Kafka
35
+ class Client
36
+ def topic_producer(topic, compression_codec: nil, compression_threshold: 1, ack_timeout: 5, required_acks: :all, max_retries: 2, retry_backoff: 1, max_buffer_size: 1000, max_buffer_bytesize: 10_000_000)
37
+ compressor = Compressor.new(
38
+ codec_name: compression_codec,
39
+ threshold: compression_threshold,
40
+ instrumenter: @instrumenter,
41
+ )
42
+
43
+ TopicProducer.new(topic,
44
+ cluster: initialize_cluster,
45
+ logger: @logger,
46
+ instrumenter: @instrumenter,
47
+ compressor: compressor,
48
+ ack_timeout: ack_timeout,
49
+ required_acks: required_acks,
50
+ max_retries: max_retries,
51
+ retry_backoff: retry_backoff,
52
+ max_buffer_size: max_buffer_size,
53
+ max_buffer_bytesize: max_buffer_bytesize,
54
+ )
55
+ end
56
+ end
57
+
58
+ class TopicProducer
59
+ def initialize(topic, cluster:, logger:, instrumenter:, compressor:, ack_timeout:, required_acks:, max_retries:, retry_backoff:, max_buffer_size:, max_buffer_bytesize:)
60
+ @cluster = cluster
61
+ @logger = logger
62
+ @instrumenter = instrumenter
63
+ @required_acks = required_acks == :all ? -1 : required_acks
64
+ @ack_timeout = ack_timeout
65
+ @max_retries = max_retries
66
+ @retry_backoff = retry_backoff
67
+ @max_buffer_size = max_buffer_size
68
+ @max_buffer_bytesize = max_buffer_bytesize
69
+ @compressor = compressor
70
+
71
+ @topic = topic
72
+ @cluster.add_target_topics(Set.new([topic]))
73
+
74
+ # A buffer organized by topic/partition.
75
+ @buffer = MessageBuffer.new
76
+
77
+ # Messages added by `#produce` but not yet assigned a partition.
78
+ @pending_message_queue = PendingMessageQueue.new
79
+ end
80
+
81
+ def produce(value, key, partition, partition_key)
82
+ create_time = Time.now
83
+
84
+ message = PendingMessage.new(
85
+ value,
86
+ key,
87
+ @topic,
88
+ partition,
89
+ partition_key,
90
+ create_time
91
+ )
92
+
93
+ @pending_message_queue.write(message)
94
+
95
+ nil
96
+ end
97
+
98
+ def deliver_messages
99
+ # There's no need to do anything if the buffer is empty.
100
+ return if buffer_size == 0
101
+
102
+ deliver_messages_with_retries
103
+ end
104
+
105
+ # Returns the number of messages currently held in the buffer.
106
+ #
107
+ # @return [Integer] buffer size.
108
+ def buffer_size
109
+ @pending_message_queue.size + @buffer.size
110
+ end
111
+
112
+ def buffer_bytesize
113
+ @pending_message_queue.bytesize + @buffer.bytesize
114
+ end
115
+
116
+ # Deletes all buffered messages.
117
+ #
118
+ # @return [nil]
119
+ def clear_buffer
120
+ @buffer.clear
121
+ @pending_message_queue.clear
122
+ end
123
+
124
+ # Closes all connections to the brokers.
125
+ #
126
+ # @return [nil]
127
+ def shutdown
128
+ @cluster.disconnect
129
+ end
130
+
131
+ private
132
+
133
+ def deliver_messages_with_retries
134
+ attempt = 0
135
+
136
+ #@cluster.add_target_topics(@target_topics)
137
+
138
+ operation = ProduceOperation.new(
139
+ cluster: @cluster,
140
+ buffer: @buffer,
141
+ required_acks: @required_acks,
142
+ ack_timeout: @ack_timeout,
143
+ compressor: @compressor,
144
+ logger: @logger,
145
+ instrumenter: @instrumenter,
146
+ )
147
+
148
+ loop do
149
+ attempt += 1
150
+
151
+ @cluster.refresh_metadata_if_necessary!
152
+
153
+ assign_partitions!
154
+ operation.execute
155
+
156
+ if @required_acks.zero?
157
+ # No response is returned by the brokers, so we can't know which messages
158
+ # have been successfully written. Our only option is to assume that they all
159
+ # have.
160
+ @buffer.clear
161
+ end
162
+
163
+ if buffer_size.zero?
164
+ break
165
+ elsif attempt <= @max_retries
166
+ @logger.warn "Failed to send all messages; attempting retry #{attempt} of #{@max_retries} after #{@retry_backoff}s"
167
+
168
+ sleep @retry_backoff
169
+ else
170
+ @logger.error "Failed to send all messages; keeping remaining messages in buffer"
171
+ break
172
+ end
173
+ end
174
+
175
+ unless @pending_message_queue.empty?
176
+ # Mark the cluster as stale in order to force a cluster metadata refresh.
177
+ @cluster.mark_as_stale!
178
+ raise DeliveryFailed, "Failed to assign partitions to #{@pending_message_queue.size} messages"
179
+ end
180
+
181
+ unless @buffer.empty?
182
+ partitions = @buffer.map {|topic, partition, _| "#{topic}/#{partition}" }.join(", ")
183
+
184
+ raise DeliveryFailed, "Failed to send messages to #{partitions}"
185
+ end
186
+ end
187
+
188
+ def assign_partitions!
189
+ failed_messages = []
190
+ partition_count = @cluster.partitions_for(@topic).count
191
+
192
+ @pending_message_queue.each do |message|
193
+ partition = message.partition
194
+
195
+ begin
196
+ if partition.nil?
197
+ partition = Partitioner.partition_for_key(partition_count, message)
198
+ end
199
+
200
+ @buffer.write(
201
+ value: message.value,
202
+ key: message.key,
203
+ topic: message.topic,
204
+ partition: partition,
205
+ create_time: message.create_time,
206
+ )
207
+ rescue Kafka::Error => e
208
+ failed_messages << message
209
+ end
210
+ end
211
+
212
+ if failed_messages.any?
213
+ failed_messages.group_by(&:topic).each do |topic, messages|
214
+ @logger.error "Failed to assign partitions to #{messages.count} messages in #{topic}"
215
+ end
216
+
217
+ @cluster.mark_as_stale!
218
+ end
219
+
220
+ @pending_message_queue.replace(failed_messages)
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,242 @@
1
+ require 'fluent/output'
2
+ require 'fluent/plugin/kafka_plugin_util'
3
+
4
+ class Fluent::KafkaOutput < Fluent::Output
5
+ Fluent::Plugin.register_output('kafka', self)
6
+
7
+ config_param :brokers, :string, :default => 'localhost:9092',
8
+ :desc => <<-DESC
9
+ Set brokers directly
10
+ <broker1_host>:<broker1_port>,<broker2_host>:<broker2_port>,..
11
+ Note that you can choose to use either brokers or zookeeper.
12
+ DESC
13
+ config_param :zookeeper, :string, :default => nil,
14
+ :desc => "Set brokers via Zookeeper: <zookeeper_host>:<zookeeper_port>"
15
+ config_param :zookeeper_path, :string, :default => '/brokers/ids',
16
+ :desc => "Path in path for Broker id. Default to /brokers/ids"
17
+ config_param :default_topic, :string, :default => nil,
18
+ :desc => "Output topic."
19
+ config_param :default_message_key, :string, :default => nil
20
+ config_param :default_partition_key, :string, :default => nil
21
+ config_param :default_partition, :integer, :default => nil
22
+ config_param :client_id, :string, :default => 'kafka'
23
+ config_param :output_data_type, :string, :default => 'json',
24
+ :desc => "Supported format: (json|ltsv|msgpack|attr:<record name>|<formatter name>)"
25
+ config_param :output_include_tag, :bool, :default => false
26
+ config_param :output_include_time, :bool, :default => false
27
+ config_param :exclude_partition_key, :bool, :default => false,
28
+ :desc => <<-DESC
29
+ Set true to remove partition key from data
30
+ DESC
31
+ config_param :exclude_partition, :bool, :default => false,
32
+ :desc => <<-DESC
33
+ Set true to remove partition from data
34
+ DESC
35
+
36
+ config_param :exclude_message_key, :bool, :default => false,
37
+ :desc => <<-DESC
38
+ Set true to remove message key from data
39
+ DESC
40
+ config_param :exclude_topic_key, :bool, :default => false,
41
+ :desc => <<-DESC
42
+ Set true to remove topic name key from data
43
+ DESC
44
+
45
+ # ruby-kafka producer options
46
+ config_param :max_send_retries, :integer, :default => 2,
47
+ :desc => "Number of times to retry sending of messages to a leader."
48
+ config_param :required_acks, :integer, :default => -1,
49
+ :desc => "The number of acks required per request."
50
+ config_param :ack_timeout, :integer, :default => nil,
51
+ :desc => "How long the producer waits for acks."
52
+ config_param :compression_codec, :string, :default => nil,
53
+ :desc => "The codec the producer uses to compress messages."
54
+
55
+ config_param :time_format, :string, :default => nil
56
+
57
+ config_param :max_buffer_size, :integer, :default => nil,
58
+ :desc => "Number of messages to be buffered by the kafka producer."
59
+
60
+ config_param :max_buffer_bytesize, :integer, :default => nil,
61
+ :desc => "Maximum size in bytes to be buffered."
62
+
63
+ config_param :active_support_notification_regex, :string, :default => nil,
64
+ :desc => <<-DESC
65
+ Add a regular expression to capture ActiveSupport notifications from the Kafka client
66
+ requires activesupport gem - records will be generated under fluent_kafka_stats.**
67
+ DESC
68
+
69
+ include Fluent::KafkaPluginUtil::SSLSettings
70
+ include Fluent::KafkaPluginUtil::SaslSettings
71
+
72
+ attr_accessor :output_data_type
73
+ attr_accessor :field_separator
74
+
75
+ unless method_defined?(:log)
76
+ define_method("log") { $log }
77
+ end
78
+
79
+ def initialize
80
+ super
81
+
82
+ require 'kafka'
83
+
84
+ @kafka = nil
85
+ end
86
+
87
+ def refresh_client
88
+ if @zookeeper
89
+ @seed_brokers = []
90
+ z = Zookeeper.new(@zookeeper)
91
+ z.get_children(:path => @zookeeper_path)[:children].each do |id|
92
+ broker = Yajl.load(z.get(:path => @zookeeper_path + "/#{id}")[:data])
93
+ @seed_brokers.push("#{broker['host']}:#{broker['port']}")
94
+ end
95
+ z.close
96
+ log.info "brokers has been refreshed via Zookeeper: #{@seed_brokers}"
97
+ end
98
+ begin
99
+ if @seed_brokers.length > 0
100
+ if @scram_mechanism && @username && @password
101
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
102
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
103
+ sasl_scram_username: @username, sasl_scram_password: @password, sasl_scram_mechanism: @scram_mechanism)
104
+ elseif @username && @password
105
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
106
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
107
+ sasl_plain_usernam: @username, sasl_plain_password: @password)
108
+ else
109
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
110
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
111
+ sasl_gssapi_principal: @principal, sasl_gssapi_keytab: @keytab)
112
+ end
113
+ log.info "initialized kafka producer: #{@client_id}"
114
+ else
115
+ log.warn "No brokers found on Zookeeper"
116
+ end
117
+ rescue Exception => e
118
+ log.error e
119
+ end
120
+ end
121
+
122
+ def configure(conf)
123
+ super
124
+
125
+ if @zookeeper
126
+ require 'zookeeper'
127
+ else
128
+ @seed_brokers = @brokers.match(",").nil? ? [@brokers] : @brokers.split(",")
129
+ log.info "brokers has been set directly: #{@seed_brokers}"
130
+ end
131
+
132
+ if conf['ack_timeout_ms']
133
+ log.warn "'ack_timeout_ms' parameter is deprecated. Use second unit 'ack_timeout' instead"
134
+ @ack_timeout = conf['ack_timeout_ms'].to_i / 1000
135
+ end
136
+
137
+ @f_separator = case @field_separator
138
+ when /SPACE/i then ' '
139
+ when /COMMA/i then ','
140
+ when /SOH/i then "\x01"
141
+ else "\t"
142
+ end
143
+
144
+ @formatter_proc = setup_formatter(conf)
145
+
146
+ @producer_opts = {max_retries: @max_send_retries, required_acks: @required_acks}
147
+ @producer_opts[:ack_timeout] = @ack_timeout if @ack_timeout
148
+ @producer_opts[:compression_codec] = @compression_codec.to_sym if @compression_codec
149
+ @producer_opts[:max_buffer_size] = @max_buffer_size if @max_buffer_size
150
+ @producer_opts[:max_buffer_bytesize] = @max_buffer_bytesize if @max_buffer_bytesize
151
+ if @active_support_notification_regex
152
+ require 'active_support/notifications'
153
+ require 'active_support/core_ext/hash/keys'
154
+ ActiveSupport::Notifications.subscribe(Regexp.new(@active_support_notification_regex)) do |*args|
155
+ event = ActiveSupport::Notifications::Event.new(*args)
156
+ message = event.payload.respond_to?(:stringify_keys) ? event.payload.stringify_keys : event.payload
157
+ @router.emit("fluent_kafka_stats.#{event.name}", Time.now.to_i, message)
158
+ end
159
+ end
160
+ end
161
+
162
+ def start
163
+ super
164
+ refresh_client
165
+ end
166
+
167
+ def shutdown
168
+ super
169
+ @kafka = nil
170
+ end
171
+
172
+ def setup_formatter(conf)
173
+ if @output_data_type == 'json'
174
+ require 'yajl'
175
+ Proc.new { |tag, time, record| Yajl::Encoder.encode(record) }
176
+ elsif @output_data_type == 'ltsv'
177
+ require 'ltsv'
178
+ Proc.new { |tag, time, record| LTSV.dump(record) }
179
+ elsif @output_data_type == 'msgpack'
180
+ require 'msgpack'
181
+ Proc.new { |tag, time, record| record.to_msgpack }
182
+ elsif @output_data_type =~ /^attr:(.*)$/
183
+ @custom_attributes = $1.split(',').map(&:strip).reject(&:empty?)
184
+ @custom_attributes.unshift('time') if @output_include_time
185
+ @custom_attributes.unshift('tag') if @output_include_tag
186
+ Proc.new { |tag, time, record|
187
+ @custom_attributes.map { |attr|
188
+ record[attr].nil? ? '' : record[attr].to_s
189
+ }.join(@f_separator)
190
+ }
191
+ else
192
+ @formatter = Fluent::Plugin.new_formatter(@output_data_type)
193
+ @formatter.configure(conf)
194
+ @formatter.method(:format)
195
+ end
196
+ end
197
+
198
+ def emit(tag, es, chain)
199
+ begin
200
+ chain.next
201
+
202
+ # out_kafka is mainly for testing so don't need the performance unlike out_kafka_buffered.
203
+ producer = @kafka.producer(@producer_opts)
204
+
205
+ es.each do |time, record|
206
+ if @output_include_time
207
+ if @time_format
208
+ record['time'] = Time.at(time).strftime(@time_format)
209
+ else
210
+ record['time'] = time
211
+ end
212
+ end
213
+ record['tag'] = tag if @output_include_tag
214
+ topic = (@exclude_topic_key ? record.delete('topic') : record['topic']) || @default_topic || tag
215
+ partition_key = (@exclude_partition_key ? record.delete('partition_key') : record['partition_key']) || @default_partition_key
216
+ partition = (@exclude_partition ? record.delete('partition'.freeze) : record['partition'.freeze]) || @default_partition
217
+ message_key = (@exclude_message_key ? record.delete('message_key') : record['message_key']) || @default_message_key
218
+
219
+ value = @formatter_proc.call(tag, time, record)
220
+
221
+ log.trace { "message will send to #{topic} with partition_key: #{partition_key}, partition: #{partition}, message_key: #{message_key} and value: #{value}." }
222
+ begin
223
+ producer.produce(value, topic: topic, key: message_key, partition: partition, partition_key: partition_key)
224
+ rescue Kafka::BufferOverflow => e
225
+ log.warn "BufferOverflow occurred: #{e}"
226
+ log.info "Trying to deliver the messages to prevent the buffer from overflowing again."
227
+ producer.deliver_messages
228
+ log.info "Recovered from BufferOverflow successfully`"
229
+ end
230
+ end
231
+
232
+ producer.deliver_messages
233
+ producer.shutdown
234
+ rescue Exception => e
235
+ log.warn "Send exception occurred: #{e}"
236
+ producer.shutdown if producer
237
+ refresh_client
238
+ raise e
239
+ end
240
+ end
241
+
242
+ end
@@ -0,0 +1,224 @@
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
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 :default_topic, :string, :default => nil,
19
+ :desc => "Default output topic when record doesn't have topic field"
20
+ config_param :default_message_key, :string, :default => nil
21
+ config_param :default_partition_key, :string, :default => nil
22
+ config_param :default_partition, :integer, :default => nil
23
+ config_param :client_id, :string, :default => 'fluentd'
24
+ config_param :exclude_partition_key, :bool, :default => false,
25
+ :desc => 'Set true to remove partition key from data'
26
+ config_param :exclude_partition, :bool, :default => false,
27
+ :desc => 'Set true to remove partition from data'
28
+ config_param :exclude_message_key, :bool, :default => false,
29
+ :desc => 'Set true to remove partition key from data'
30
+ config_param :exclude_topic_key, :bool, :default => false,
31
+ :desc => 'Set true to remove topic name key from data'
32
+
33
+ config_param :get_kafka_client_log, :bool, :default => false
34
+
35
+ # ruby-kafka producer options
36
+ config_param :max_send_retries, :integer, :default => 2,
37
+ :desc => "Number of times to retry sending of messages to a leader."
38
+ config_param :required_acks, :integer, :default => -1,
39
+ :desc => "The number of acks required per request."
40
+ config_param :ack_timeout, :time, :default => nil,
41
+ :desc => "How long the producer waits for acks."
42
+ config_param :compression_codec, :string, :default => nil,
43
+ :desc => <<-DESC
44
+ The codec the producer uses to compress messages.
45
+ Supported codecs: (gzip|snappy)
46
+ DESC
47
+
48
+ config_param :active_support_notification_regex, :string, :default => nil,
49
+ :desc => <<-DESC
50
+ Add a regular expression to capture ActiveSupport notifications from the Kafka client
51
+ requires activesupport gem - records will be generated under fluent_kafka_stats.**
52
+ DESC
53
+
54
+ config_section :buffer do
55
+ config_set_default :chunk_keys, ["topic"]
56
+ end
57
+ config_section :format do
58
+ config_set_default :@type, 'json'
59
+ end
60
+
61
+ include Fluent::KafkaPluginUtil::SSLSettings
62
+ include Fluent::KafkaPluginUtil::SaslSettings
63
+
64
+ def initialize
65
+ super
66
+
67
+ @kafka = nil
68
+ end
69
+
70
+ def refresh_client(raise_error = true)
71
+ begin
72
+ logger = @get_kafka_client_log ? log : nil
73
+ if @scram_mechanism && @username && @password
74
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
75
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
76
+ sasl_scram_username: @username, sasl_scram_password: @password, sasl_scram_mechanism: @scram_mechanism)
77
+ elseif @username && @password
78
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
79
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
80
+ sasl_plain_username: @username, sasl_plain_password: @password)
81
+ else
82
+ @kafka = Kafka.new(seed_brokers: @seed_brokers, client_id: @client_id, ssl_ca_cert: read_ssl_file(@ssl_ca_cert),
83
+ ssl_client_cert: read_ssl_file(@ssl_client_cert), ssl_client_cert_key: read_ssl_file(@ssl_client_cert_key),
84
+ sasl_gssapi_principal: @principal, sasl_gssapi_keytab: @keytab)
85
+ end
86
+ log.info "initialized kafka producer: #{@client_id}"
87
+ rescue Exception => e
88
+ if raise_error # During startup, error should be reported to engine and stop its phase for safety.
89
+ raise e
90
+ else
91
+ log.error e
92
+ end
93
+ end
94
+ end
95
+
96
+ def configure(conf)
97
+ super
98
+
99
+ if @brokers.size > 0
100
+ log.info "brokers has been set: #{@brokers}"
101
+ else
102
+ raise Fluent::Config, 'No brokers specified. Need one broker at least.'
103
+ end
104
+
105
+ formatter_conf = conf.elements('format').first
106
+ unless formatter_conf
107
+ raise Fluent::ConfigError, "<format> section is required."
108
+ end
109
+ unless formatter_conf["@type"]
110
+ raise Fluent::ConfigError, "format/@type is required."
111
+ end
112
+ @formatter_proc = setup_formatter(formatter_conf)
113
+
114
+ if @default_topic.nil?
115
+ if @chunk_keys.include?('topic') && !@chunk_keys.include?('tag')
116
+ 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>"
117
+ end
118
+ else
119
+ if @chunk_keys.include?('tag')
120
+ log.warn "default_topic is set. Fluentd's event tag is not used for topic"
121
+ end
122
+ end
123
+
124
+ @producer_opts = {max_retries: @max_send_retries, required_acks: @required_acks}
125
+ @producer_opts[:ack_timeout] = @ack_timeout if @ack_timeout
126
+ @producer_opts[:compression_codec] = @compression_codec.to_sym if @compression_codec
127
+ if @active_support_notification_regex
128
+ require 'active_support/notifications'
129
+ require 'active_support/core_ext/hash/keys'
130
+ ActiveSupport::Notifications.subscribe(Regexp.new(@active_support_notification_regex)) do |*args|
131
+ event = ActiveSupport::Notifications::Event.new(*args)
132
+ message = event.payload.respond_to?(:stringify_keys) ? event.payload.stringify_keys : event.payload
133
+ @router.emit("fluent_kafka_stats.#{event.name}", Time.now.to_i, message)
134
+ end
135
+ end
136
+ end
137
+
138
+ def multi_workers_ready?
139
+ true
140
+ end
141
+
142
+ def start
143
+ super
144
+ refresh_client
145
+ end
146
+
147
+ def close
148
+ super
149
+ @kafka.close if @kafka
150
+ end
151
+
152
+ def terminate
153
+ super
154
+ @kafka = nil
155
+ end
156
+
157
+ def setup_formatter(conf)
158
+ type = conf['@type']
159
+ case type
160
+ when 'json'
161
+ begin
162
+ require 'oj'
163
+ Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
164
+ Proc.new { |tag, time, record| Oj.dump(record) }
165
+ rescue LoadError
166
+ require 'yajl'
167
+ Proc.new { |tag, time, record| Yajl::Encoder.encode(record) }
168
+ end
169
+ when 'ltsv'
170
+ require 'ltsv'
171
+ Proc.new { |tag, time, record| LTSV.dump(record) }
172
+ else
173
+ @formatter = formatter_create(usage: 'kafka-plugin', conf: conf)
174
+ @formatter.method(:format)
175
+ end
176
+ end
177
+
178
+ # TODO: optimize write performance
179
+ def write(chunk)
180
+ tag = chunk.metadata.tag
181
+ topic = chunk.metadata.variables[:topic] || @default_topic || tag
182
+ producer = @kafka.topic_producer(topic, @producer_opts)
183
+
184
+ messages = 0
185
+ record_buf = nil
186
+
187
+ begin
188
+ chunk.msgpack_each { |time, record|
189
+ begin
190
+ record = inject_values_to_record(tag, time, record)
191
+ record.delete('topic'.freeze) if @exclude_topic_key
192
+ partition_key = (@exclude_partition_key ? record.delete('partition_key'.freeze) : record['partition_key'.freeze]) || @default_partition_key
193
+ partition = (@exclude_partition ? record.delete('partition'.freeze) : record['partition'.freeze]) || @default_partition
194
+ message_key = (@exclude_message_key ? record.delete('message_key'.freeze) : record['message_key'.freeze]) || @default_message_key
195
+
196
+ record_buf = @formatter_proc.call(tag, time, record)
197
+ rescue StandardError => e
198
+ log.warn "unexpected error during format record. Skip broken event:", :error => e.to_s, :error_class => e.class.to_s, :time => time, :record => record
199
+ next
200
+ end
201
+
202
+ log.trace { "message will send to #{topic} with partition_key: #{partition_key}, partition: #{partition}, message_key: #{message_key} and value: #{record_buf}." }
203
+ messages += 1
204
+
205
+ producer.produce(record_buf, message_key, partition, partition_key)
206
+ }
207
+
208
+ if messages > 0
209
+ log.debug { "#{messages} messages send." }
210
+ producer.deliver_messages
211
+ end
212
+ end
213
+ rescue Exception => e
214
+ log.warn "Send exception occurred: #{e}"
215
+ log.warn "Exception Backtrace : #{e.backtrace.join("\n")}"
216
+ # For safety, refresh client and its producers
217
+ refresh_client(false)
218
+ # Raise exception to retry sendind messages
219
+ raise e
220
+ ensure
221
+ producer.shutdown if producer
222
+ end
223
+ end
224
+ end