fluent-plugin-kafka 0.19.6 → 0.19.8

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.
@@ -130,12 +130,14 @@ DESC
130
130
  :"bootstrap.servers" => @brokers,
131
131
  }
132
132
 
133
- if @ssl_ca_cert && @ssl_ca_cert[0]
133
+ if (@ssl_ca_cert && @ssl_ca_cert[0]) || @ssl_ca_certs_from_system || @ssl_client_cert || @ssl_client_cert_key
134
134
  ssl = true
135
- config[:"ssl.ca.location"] = @ssl_ca_cert[0]
135
+ config[:"ssl.ca.location"] = @ssl_ca_cert[0] if @ssl_ca_cert && @ssl_ca_cert[0]
136
136
  config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
137
137
  config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
138
138
  config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
139
+ config[:"ssl.endpoint.identification.algorithm"] = @ssl_verify_hostname ? "https" : "none"
140
+ config[:"enable.ssl.certificate.verification"] = true
139
141
  end
140
142
 
141
143
  if @principal
@@ -228,8 +230,8 @@ DESC
228
230
  Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
229
231
  Proc.new { |tag, time, record| Oj.dump(record) }
230
232
  rescue LoadError
231
- require 'yajl'
232
- Proc.new { |tag, time, record| Yajl::Encoder.encode(record) }
233
+ require 'json'
234
+ Proc.new { |tag, time, record| JSON.generate(record) }
233
235
  end
234
236
  elsif @output_data_type == 'ltsv'
235
237
  require 'ltsv'
@@ -7,14 +7,16 @@ require 'rdkafka'
7
7
 
8
8
  begin
9
9
  rdkafka_version = Gem::Version::create(Rdkafka::VERSION)
10
- if rdkafka_version < Gem::Version.create('0.12.0')
11
- require_relative 'rdkafka_patch/0_11_0'
12
- elsif rdkafka_version == Gem::Version.create('0.12.0')
13
- require_relative 'rdkafka_patch/0_12_0'
10
+ if rdkafka_version >= Gem::Version.create('0.16.0')
11
+ require_relative 'rdkafka_patch/0_16_0'
14
12
  elsif rdkafka_version >= Gem::Version.create('0.14.0')
15
13
  require_relative 'rdkafka_patch/0_14_0'
16
- elsif rdkafka_version >= Gem::Version.create('0.16.0')
17
- require_relative 'rdkafka_patch/0_16_0'
14
+ elsif rdkafka_version >= Gem::Version.create('0.13.0')
15
+ require_relative 'rdkafka_patch/0_13_0'
16
+ elsif rdkafka_version >= Gem::Version.create('0.12.0')
17
+ require_relative 'rdkafka_patch/0_12_0'
18
+ else
19
+ require_relative 'rdkafka_patch/0_11_0'
18
20
  end
19
21
  rescue LoadError, NameError
20
22
  raise "unable to patch rdkafka."
@@ -97,7 +99,6 @@ DESC
97
99
  config_param :enqueue_retry_backoff, :integer, :default => 3
98
100
  config_param :max_enqueue_bytes_per_second, :size, :default => nil, :desc => 'The maximum number of enqueueing bytes per second'
99
101
 
100
- config_param :service_name, :string, :default => nil, :desc => 'Used for sasl.kerberos.service.name'
101
102
  config_param :unrecoverable_error_codes, :array, :default => ["topic_authorization_failed", "msg_size_too_large"],
102
103
  :desc => 'Handle some of the error codes should be unrecoverable if specified'
103
104
 
@@ -111,6 +112,8 @@ DESC
111
112
 
112
113
  include Fluent::KafkaPluginUtil::SSLSettings
113
114
  include Fluent::KafkaPluginUtil::SaslSettings
115
+ include Fluent::KafkaPluginUtil::PartitionSettings
116
+ include Fluent::KafkaPluginUtil::RdkafkaSecuritySettings
114
117
 
115
118
  class EnqueueRate
116
119
  class LimitExceeded < StandardError
@@ -250,39 +253,7 @@ DESC
250
253
  end
251
254
 
252
255
  def build_config
253
- config = {:"bootstrap.servers" => @brokers}
254
-
255
- if @ssl_ca_cert && @ssl_ca_cert[0]
256
- ssl = true
257
- config[:"ssl.ca.location"] = @ssl_ca_cert[0]
258
- config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
259
- config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
260
- config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
261
- end
262
-
263
- if @principal
264
- sasl = true
265
- config[:"sasl.mechanisms"] = "GSSAPI"
266
- config[:"sasl.kerberos.principal"] = @principal
267
- config[:"sasl.kerberos.service.name"] = @service_name if @service_name
268
- config[:"sasl.kerberos.keytab"] = @keytab if @keytab
269
- end
270
-
271
- if @username && @password
272
- sasl = true
273
- config[:"sasl.mechanisms"] = 'PLAIN'
274
- end
275
-
276
- if ssl && sasl
277
- security_protocol = "SASL_SSL"
278
- elsif ssl && !sasl
279
- security_protocol = "SSL"
280
- elsif !ssl && sasl
281
- security_protocol = "SASL_PLAINTEXT"
282
- else
283
- security_protocol = "PLAINTEXT"
284
- end
285
- config[:"security.protocol"] = security_protocol
256
+ config = {:"bootstrap.servers" => @brokers}.merge(rdkafka_security_config)
286
257
 
287
258
  config[:"compression.codec"] = @compression_codec if @compression_codec
288
259
  config[:"message.send.max.retries"] = @max_send_retries if @max_send_retries
@@ -292,14 +263,14 @@ DESC
292
263
  config[:"queue.buffering.max.messages"] = @rdkafka_buffering_max_messages if @rdkafka_buffering_max_messages
293
264
  config[:"message.max.bytes"] = @rdkafka_message_max_bytes if @rdkafka_message_max_bytes
294
265
  config[:"batch.num.messages"] = @rdkafka_message_max_num if @rdkafka_message_max_num
295
- config[:"sasl.username"] = @username if @username
296
- config[:"sasl.password"] = @password if @password
297
266
  config[:"enable.idempotence"] = @idempotent if @idempotent
298
267
 
299
268
  @rdkafka_options.each { |k, v|
300
269
  config[k.to_sym] = v
301
270
  }
302
271
 
272
+ validate_sasl_over_ssl(config)
273
+
303
274
  config
304
275
  end
305
276
 
@@ -406,6 +377,8 @@ DESC
406
377
  record.delete(@topic_key) if @exclude_topic_key
407
378
  partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
408
379
  message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
380
+ partition = coerce_partition(partition) unless partition.nil?
381
+ message_key = message_key.to_s unless message_key.nil?
409
382
 
410
383
  @headers_from_record_accessors.each do |key, header_accessor|
411
384
  headers[key] = header_accessor.call(record)
@@ -451,6 +424,7 @@ DESC
451
424
  log.warn "Delivery failed and matched regexp pattern #{@discard_kafka_delivery_failed_regex}. Discard events:", :error => e.to_s, :error_class => e.class.to_s, :tag => tag
452
425
  else
453
426
  log.warn "Send exception occurred: #{e} at #{e.backtrace.first}"
427
+ raise Fluent::UnrecoverableError, "Rejected due to #{e}" if unrecoverable_error?(e)
454
428
  # Raise exception to retry sendind messages
455
429
  raise e
456
430
  end
@@ -507,15 +481,14 @@ DESC
507
481
 
508
482
  raise e
509
483
  else
510
- if unrecoverable_error_codes.include?(e.code.to_s)
511
- # some of the errors should be handled as an unrecoverable error
512
- raise Fluent::UnrecoverableError, "Rejected due to #{e}"
513
- else
514
- raise e
515
- end
484
+ raise e
516
485
  end
517
486
  end
518
487
  end
519
488
  end
489
+
490
+ def unrecoverable_error?(e)
491
+ e.is_a?(Rdkafka::RdkafkaError) && unrecoverable_error_codes.include?(e.code.to_s)
492
+ end
520
493
  end
521
494
  end
@@ -0,0 +1,43 @@
1
+ class Rdkafka::NativeKafka
2
+ # return false if producer is forcefully closed, otherwise return true
3
+ def close(timeout=nil, object_id=nil)
4
+ return true if closed?
5
+
6
+ synchronize do
7
+ # Indicate to the outside world that we are closing
8
+ @closing = true
9
+
10
+ thread_status = :unknown
11
+ if @polling_thread
12
+ # Indicate to polling thread that we're closing
13
+ @polling_thread[:closing] = true
14
+
15
+ # Wait for the polling thread to finish up,
16
+ # this can be aborted in practice if this
17
+ # code runs from a finalizer.
18
+ thread_status = @polling_thread.join(timeout)
19
+ end
20
+
21
+ # Destroy the client after locking both mutexes
22
+ @poll_mutex.lock
23
+
24
+ # This check prevents a race condition, where we would enter the close in two threads
25
+ # and after unlocking the primary one that hold the lock but finished, ours would be unlocked
26
+ # and would continue to run, trying to destroy inner twice
27
+ if @inner
28
+ Rdkafka::Bindings.rd_kafka_destroy(@inner)
29
+ @inner = nil
30
+ end
31
+
32
+ !thread_status.nil?
33
+ end
34
+ end
35
+ end
36
+
37
+ class Rdkafka::Producer
38
+ def close(timeout = nil)
39
+ return true if closed?
40
+ ObjectSpace.undefine_finalizer(self)
41
+ @native_kafka.close(timeout)
42
+ end
43
+ end
@@ -24,7 +24,7 @@ class Rdkafka::NativeKafka
24
24
  # This check prevents a race condition, where we would enter the close in two threads
25
25
  # and after unlocking the primary one that hold the lock but finished, ours would be unlocked
26
26
  # and would continue to run, trying to destroy inner twice
27
- retun unless @inner
27
+ return unless @inner
28
28
 
29
29
  Rdkafka::Bindings.rd_kafka_destroy(@inner)
30
30
  @inner = nil
@@ -40,7 +40,7 @@ class Rdkafka::Producer
40
40
  return true if closed?
41
41
  ObjectSpace.undefine_finalizer(self)
42
42
 
43
- @native_kafka.close(timeout) do
43
+ closed = @native_kafka.close(timeout) do
44
44
  # We need to remove the topics references objects before we destroy the producer,
45
45
  # otherwise they would leak out
46
46
  @topics_refs_map.each_value do |refs|
@@ -51,5 +51,6 @@ class Rdkafka::Producer
51
51
  end
52
52
 
53
53
  @topics_refs_map.clear
54
+ closed
54
55
  end
55
56
  end
@@ -1,6 +1,7 @@
1
1
  require 'helper'
2
2
  require 'fluent/test/driver/input'
3
3
  require 'securerandom'
4
+ require 'json'
4
5
 
5
6
  class KafkaInputTest < Test::Unit::TestCase
6
7
  def setup
@@ -34,6 +35,132 @@ class KafkaInputTest < Test::Unit::TestCase
34
35
  assert_false d.instance.multi_workers_ready?
35
36
  end
36
37
 
38
+ class TopicWatcherTest < self
39
+ FakeMessage = Struct.new(:value, :key, :offset, :create_time)
40
+
41
+ class FakeRouter
42
+ attr_reader :emitted
43
+
44
+ def initialize
45
+ @emitted = []
46
+ end
47
+
48
+ def emit_stream(tag, es)
49
+ records = []
50
+ es.each { |time, record| records << record }
51
+ @emitted << [tag, records]
52
+ end
53
+ end
54
+
55
+ def create_topic_watcher(messages, router, tag_source: :record, add_prefix: nil, add_suffix: nil)
56
+ kafka = Object.new
57
+ kafka.define_singleton_method(:fetch_messages) { |**args| messages }
58
+ parser = Proc.new { |msg, te| JSON.parse(msg.value) }
59
+
60
+ Fluent::KafkaInput::TopicWatcher.new(
61
+ Fluent::KafkaInput::TopicEntry.new(TOPIC_NAME, 0, 0),
62
+ kafka,
63
+ 1,
64
+ parser,
65
+ add_prefix,
66
+ add_suffix,
67
+ nil,
68
+ router,
69
+ nil,
70
+ :now,
71
+ 'time',
72
+ tag_source,
73
+ 'tag')
74
+ end
75
+
76
+ def tagged_message(tag, message, offset)
77
+ FakeMessage.new({'tag' => tag, 'message' => message}.to_json, nil, offset, Time.now)
78
+ end
79
+
80
+ def test_consume_with_tag_source_record_emits_stream_per_tag
81
+ messages = [
82
+ tagged_message('app.trusted', 'record 1', 0),
83
+ tagged_message('app.trusted', 'record 2', 1),
84
+ tagged_message('attacker.controlled', 'record 3', 2),
85
+ ]
86
+ router = FakeRouter.new
87
+ create_topic_watcher(messages, router).consume
88
+
89
+ assert_equal([['app.trusted', ['record 1', 'record 2']],
90
+ ['attacker.controlled', ['record 3']]],
91
+ router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
92
+ end
93
+
94
+ def test_consume_with_tag_source_record_applies_prefix_and_suffix_per_tag
95
+ messages = [
96
+ tagged_message('app.trusted', 'record 1', 0),
97
+ tagged_message('attacker.controlled', 'record 2', 1),
98
+ ]
99
+ router = FakeRouter.new
100
+ create_topic_watcher(messages, router, add_prefix: 'prefix', add_suffix: 'suffix').consume
101
+
102
+ assert_equal(['prefix.app.trusted.suffix', 'prefix.attacker.controlled.suffix'],
103
+ router.emitted.map(&:first))
104
+ end
105
+
106
+ def test_consume_with_tag_source_record_skips_unparsable_message
107
+ messages = [
108
+ tagged_message('app.trusted', 'record 1', 0),
109
+ FakeMessage.new('this is not json', nil, 1, Time.now),
110
+ tagged_message('attacker.controlled', 'record 2', 2),
111
+ ]
112
+ router = FakeRouter.new
113
+ create_topic_watcher(messages, router).consume
114
+
115
+ assert_equal([['app.trusted', ['record 1']],
116
+ ['attacker.controlled', ['record 2']]],
117
+ router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
118
+ end
119
+
120
+ def test_consume_with_tag_source_record_skips_invalid_tag
121
+ messages = [
122
+ tagged_message('app.trusted', 'record 1', 0),
123
+ tagged_message(7, 'record 2', 1),
124
+ FakeMessage.new({'message' => 'record 3'}.to_json, nil, 2, Time.now),
125
+ tagged_message('', 'record 4', 3),
126
+ tagged_message('attacker.controlled', 'record 5', 4),
127
+ ]
128
+ router = FakeRouter.new
129
+ watcher = create_topic_watcher(messages, router)
130
+ watcher.consume
131
+
132
+ assert_equal([['app.trusted', ['record 1']],
133
+ ['attacker.controlled', ['record 5']]],
134
+ router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
135
+ assert_equal(5, watcher.instance_variable_get(:@next_offset))
136
+ end
137
+
138
+ def test_consume_advances_offset_when_every_message_is_skipped
139
+ messages = [
140
+ FakeMessage.new('this is not json', nil, 0, Time.now),
141
+ tagged_message(nil, 'record 1', 1),
142
+ ]
143
+ router = FakeRouter.new
144
+ watcher = create_topic_watcher(messages, router)
145
+ watcher.consume
146
+
147
+ assert_equal([], router.emitted)
148
+ assert_equal(2, watcher.instance_variable_get(:@next_offset))
149
+ end
150
+
151
+ def test_consume_with_tag_source_topic_emits_single_stream
152
+ messages = [
153
+ tagged_message('app.trusted', 'record 1', 0),
154
+ tagged_message('attacker.controlled', 'record 2', 1),
155
+ ]
156
+ router = FakeRouter.new
157
+ create_topic_watcher(messages, router, tag_source: :topic).consume
158
+
159
+ assert_equal([[TOPIC_NAME, ['record 1', 'record 2']]],
160
+ router.emitted.map { |tag, records| [tag, records.map { |r| r['message'] }] })
161
+ end
162
+ end
163
+
37
164
  class ConsumeTest < self
38
165
  def setup
39
166
  @kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
@@ -0,0 +1,326 @@
1
+ require 'helper'
2
+ require 'fluent/test/driver/input'
3
+ require 'securerandom'
4
+
5
+ class RdkafkaGroupInputTest < Test::Unit::TestCase
6
+
7
+ def have_rdkafka
8
+ begin
9
+ require 'fluent/plugin/in_rdkafka_group'
10
+ true
11
+ rescue LoadError
12
+ false
13
+ end
14
+ end
15
+
16
+ def setup
17
+ omit_unless(have_rdkafka, "rdkafka isn't installed")
18
+ Fluent::Test.setup
19
+ end
20
+
21
+ TOPIC_NAME = "kafka-input-#{SecureRandom.uuid}"
22
+
23
+ CONFIG = %[
24
+ topics #{TOPIC_NAME}
25
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group"}
26
+ <parse>
27
+ @type none
28
+ </parse>
29
+ ]
30
+
31
+ def create_driver(conf = CONFIG)
32
+ Fluent::Test::Driver::Input.new(Fluent::Plugin::RdKafkaGroupInput).configure(conf)
33
+ end
34
+
35
+
36
+ def test_configure
37
+ d = create_driver
38
+ assert_equal [TOPIC_NAME], d.instance.topics
39
+ assert_equal 'localhost:9092', d.instance.kafka_configs['bootstrap.servers']
40
+ end
41
+
42
+ def test_multi_worker_support
43
+ d = create_driver
44
+ assert_true d.instance.multi_workers_ready?
45
+ end
46
+
47
+ def test_build_config_without_security_parameters
48
+ d = create_driver
49
+ config = d.instance.build_config
50
+
51
+ assert_equal 'localhost:9092', config[:"bootstrap.servers"]
52
+ assert_equal 'test_group', config[:"group.id"]
53
+ assert_equal 'PLAINTEXT', config[:"security.protocol"]
54
+ assert_nil config[:"ssl.endpoint.identification.algorithm"]
55
+ assert_nil config[:"sasl.mechanisms"]
56
+ end
57
+
58
+ def test_build_config_ssl_ca_certs_from_system
59
+ d = create_driver(CONFIG + %[
60
+ ssl_ca_certs_from_system true
61
+ ])
62
+ config = d.instance.build_config
63
+
64
+ assert_equal 'SSL', config[:"security.protocol"]
65
+ assert_equal 'https', config[:"ssl.endpoint.identification.algorithm"]
66
+ assert_equal true, config[:"enable.ssl.certificate.verification"]
67
+ assert_nil config[:"ssl.ca.location"]
68
+ end
69
+
70
+ def test_build_config_ssl_client_cert
71
+ d = create_driver(CONFIG + %[
72
+ ssl_ca_cert /path/to/ca_cert.pem
73
+ ssl_client_cert /path/to/cert.pem
74
+ ssl_client_cert_key /path/to/key.pem
75
+ ssl_client_cert_key_password secret
76
+ ])
77
+ config = d.instance.build_config
78
+
79
+ assert_equal 'SSL', config[:"security.protocol"]
80
+ assert_equal '/path/to/ca_cert.pem', config[:"ssl.ca.location"]
81
+ assert_equal '/path/to/cert.pem', config[:"ssl.certificate.location"]
82
+ assert_equal '/path/to/key.pem', config[:"ssl.key.location"]
83
+ assert_equal 'secret', config[:"ssl.key.password"]
84
+ end
85
+
86
+ def test_build_config_ssl_verify_hostname_false
87
+ d = create_driver(CONFIG + %[
88
+ ssl_ca_certs_from_system true
89
+ ssl_verify_hostname false
90
+ ])
91
+ config = d.instance.build_config
92
+
93
+ assert_equal 'none', config[:"ssl.endpoint.identification.algorithm"]
94
+ assert_equal true, config[:"enable.ssl.certificate.verification"]
95
+ end
96
+
97
+ def test_build_config_sasl_plain_over_ssl
98
+ d = create_driver(CONFIG + %[
99
+ username testuser
100
+ password testpass
101
+ ssl_ca_certs_from_system true
102
+ ])
103
+ config = d.instance.build_config
104
+
105
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
106
+ assert_equal 'PLAIN', config[:"sasl.mechanisms"]
107
+ assert_equal 'testuser', config[:"sasl.username"]
108
+ assert_equal 'testpass', config[:"sasl.password"]
109
+ end
110
+
111
+ def test_configure_sasl_plain_without_ssl_raises
112
+ assert_raise(Fluent::ConfigError) {
113
+ create_driver(CONFIG + %[
114
+ username testuser
115
+ password testpass
116
+ ])
117
+ }
118
+ end
119
+
120
+ def test_build_config_sasl_plain_without_ssl_allowed_by_sasl_over_ssl
121
+ d = create_driver(CONFIG + %[
122
+ username testuser
123
+ password testpass
124
+ sasl_over_ssl false
125
+ ])
126
+ config = d.instance.build_config
127
+
128
+ assert_equal 'SASL_PLAINTEXT', config[:"security.protocol"]
129
+ assert_equal 'testpass', config[:"sasl.password"]
130
+ end
131
+
132
+ data("sha256" => ["sha256", "SCRAM-SHA-256"],
133
+ "sha512" => ["sha512", "SCRAM-SHA-512"])
134
+ def test_build_config_sasl_scram(data)
135
+ mechanism, expected = data
136
+ d = create_driver(CONFIG + %[
137
+ username testuser
138
+ password testpass
139
+ scram_mechanism #{mechanism}
140
+ ssl_ca_certs_from_system true
141
+ ])
142
+ config = d.instance.build_config
143
+
144
+ assert_equal expected, config[:"sasl.mechanisms"]
145
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
146
+ end
147
+
148
+ def test_build_config_sasl_scram_without_credentials_warns
149
+ d = create_driver(CONFIG + %[
150
+ scram_mechanism sha256
151
+ ssl_ca_certs_from_system true
152
+ ])
153
+ config = d.instance.build_config
154
+
155
+ assert_equal 'SSL', config[:"security.protocol"]
156
+ assert_nil config[:"sasl.mechanisms"]
157
+ assert_true d.logs.any? { |log| log.include?("scram_mechanism is ignored") }
158
+ end
159
+
160
+ def test_build_config_sasl_gssapi
161
+ d = create_driver(CONFIG + %[
162
+ principal kafka/host@REALM
163
+ keytab /path/to/kafka.keytab
164
+ service_name kafka
165
+ ssl_ca_certs_from_system true
166
+ ])
167
+ config = d.instance.build_config
168
+
169
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
170
+ assert_equal 'GSSAPI', config[:"sasl.mechanisms"]
171
+ assert_equal 'kafka/host@REALM', config[:"sasl.kerberos.principal"]
172
+ assert_equal '/path/to/kafka.keytab', config[:"sasl.kerberos.keytab"]
173
+ assert_equal 'kafka', config[:"sasl.kerberos.service.name"]
174
+ end
175
+
176
+ def test_build_config_kafka_configs_take_precedence
177
+ conf = %[
178
+ topics #{TOPIC_NAME}
179
+ ssl_ca_certs_from_system true
180
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group", "security.protocol": "SASL_SSL", "ssl.endpoint.identification.algorithm": "none"}
181
+ <parse>
182
+ @type none
183
+ </parse>
184
+ ]
185
+ d = create_driver(conf)
186
+ config = d.instance.build_config
187
+
188
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
189
+ assert_equal 'none', config[:"ssl.endpoint.identification.algorithm"]
190
+ assert_nil config["security.protocol"]
191
+ end
192
+
193
+ def test_configure_sasl_plaintext_in_kafka_configs_raises
194
+ conf = %[
195
+ topics #{TOPIC_NAME}
196
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group", "security.protocol": "SASL_PLAINTEXT", "sasl.mechanisms": "PLAIN", "sasl.username": "testuser", "sasl.password": "testpass"}
197
+ <parse>
198
+ @type none
199
+ </parse>
200
+ ]
201
+
202
+ assert_raise(Fluent::ConfigError) {
203
+ create_driver(conf)
204
+ }
205
+ assert_nothing_raised {
206
+ create_driver(conf + "sasl_over_ssl false\n")
207
+ }
208
+ end
209
+
210
+ data("uppercase" => "SASL_PLAINTEXT",
211
+ "lowercase" => "sasl_plaintext")
212
+ def test_configure_sasl_plaintext_in_kafka_configs_raises_regardless_of_case(protocol)
213
+ conf = %[
214
+ topics #{TOPIC_NAME}
215
+ username testuser
216
+ password testpass
217
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group", "security.protocol": "#{protocol}"}
218
+ <parse>
219
+ @type none
220
+ </parse>
221
+ ]
222
+
223
+ assert_raise(Fluent::ConfigError) {
224
+ create_driver(conf)
225
+ }
226
+ end
227
+
228
+ def test_setup_consumer_uses_build_config
229
+ d = create_driver(CONFIG + %[
230
+ ssl_ca_certs_from_system true
231
+ ])
232
+ consumer = Object.new
233
+ stub(consumer).subscribe
234
+ rdkafka_config = Object.new
235
+ stub(rdkafka_config).consumer { consumer }
236
+ passed = nil
237
+ stub(Rdkafka::Config).new { |config| passed = config; rdkafka_config }
238
+
239
+ d.instance.setup_consumer
240
+
241
+ assert_equal 'localhost:9092', passed[:"bootstrap.servers"]
242
+ assert_equal 'SSL', passed[:"security.protocol"]
243
+ end
244
+
245
+ class ConsumeTest < self
246
+ TOPIC_NAME = "kafka-input-#{SecureRandom.uuid}"
247
+
248
+ def setup
249
+ @kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
250
+ @producer = @kafka.producer
251
+ @kafka.create_topic(TOPIC_NAME)
252
+ end
253
+
254
+ def teardown
255
+ @kafka.delete_topic(TOPIC_NAME)
256
+ @kafka.close
257
+ end
258
+
259
+ def test_consume
260
+ conf = %[
261
+ topics #{TOPIC_NAME}
262
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group"}
263
+ <parse>
264
+ @type none
265
+ </parse>
266
+ ]
267
+
268
+ d = create_driver(conf)
269
+
270
+ d.run(expect_records: 1, timeout: 10) do
271
+ sleep 0.1
272
+ @producer.produce("Hello, fluent-plugin-kafka!", topic: TOPIC_NAME)
273
+ @producer.deliver_messages
274
+ end
275
+
276
+ expected = {'message' => 'Hello, fluent-plugin-kafka!'}
277
+ assert_equal expected, d.events[0][2]
278
+ end
279
+ end
280
+
281
+ class ConsumeTopicWithRegexpTest < self
282
+ TOPIC_NAME1 = "kafka-input-1-#{SecureRandom.uuid}"
283
+ TOPIC_NAME2 = "kafka-input-22-#{SecureRandom.uuid}"
284
+ UNMATCHED_TOPIC = "kafka-input-333-#{SecureRandom.uuid}"
285
+
286
+ TOPIC_NAME_REGEXP = "/kafka-input-[0-9]{1,2}-.*/"
287
+
288
+ def setup
289
+ @kafka = Kafka.new(["localhost:9092"], client_id: 'kafka')
290
+ @producer = @kafka.producer
291
+ @kafka.create_topic(TOPIC_NAME1)
292
+ @kafka.create_topic(TOPIC_NAME2)
293
+ @kafka.create_topic(UNMATCHED_TOPIC)
294
+ end
295
+
296
+ def teardown
297
+ @kafka.delete_topic(TOPIC_NAME1)
298
+ @kafka.delete_topic(TOPIC_NAME2)
299
+ @kafka.delete_topic(UNMATCHED_TOPIC)
300
+ @kafka.close
301
+ end
302
+
303
+ def test_consume_with_regexp
304
+ conf = %[
305
+ topics #{TOPIC_NAME_REGEXP}
306
+ kafka_configs {"bootstrap.servers": "localhost:9092", "group.id": "test_group"}
307
+ <parse>
308
+ @type none
309
+ </parse>
310
+ ]
311
+ d = create_driver(conf)
312
+
313
+ d.run(expect_records: 2, timeout: 10) do
314
+ sleep 0.1
315
+ @producer.produce("Hello, fluent-plugin-kafka! in topic 1", topic: TOPIC_NAME1)
316
+ @producer.produce("Hello, fluent-plugin-kafka! in topic 2", topic: TOPIC_NAME2)
317
+ @producer.produce("Should be ignored", topic: UNMATCHED_TOPIC)
318
+ @producer.deliver_messages
319
+ end
320
+ expected_message_pattern = /Hello, fluent-plugin-kafka! in topic [12]/
321
+ assert_equal 2, d.events.size
322
+ assert_match(expected_message_pattern, d.events[0][2]['message'])
323
+ assert_match(expected_message_pattern, d.events[1][2]['message'])
324
+ end
325
+ end
326
+ end