fluent-plugin-kafka 0.19.7 → 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.
@@ -37,6 +37,80 @@ class Rdkafka2OutputTest < Test::Unit::TestCase
37
37
  Fluent::Test::Driver::Output.new(Fluent::Rdkafka2Output).configure(conf)
38
38
  end
39
39
 
40
+ class DummyDeliveryHandle
41
+ def initialize(error = nil)
42
+ @error = error
43
+ end
44
+
45
+ def wait(**)
46
+ raise @error if @error
47
+ end
48
+ end
49
+
50
+ class DummyProducer
51
+ attr_reader :produced
52
+
53
+ def initialize(produce_error: nil, delivery_error: nil)
54
+ @produced = []
55
+ @produce_error = produce_error
56
+ @delivery_error = delivery_error
57
+ end
58
+
59
+ # Raises what the rdkafka FFI binding raises: the partition must be an
60
+ # int32 and the key must respond to #bytesize
61
+ def produce(topic:, payload:, key:, partition:, headers:, timestamp:)
62
+ unless partition.nil?
63
+ raise TypeError, "no implicit conversion of #{partition.class} into Integer" unless partition.is_a?(Integer)
64
+ raise RangeError, "integer #{partition} too big to convert to 'int'" unless (-2**31..2**31 - 1).cover?(partition)
65
+ end
66
+ key.bytesize unless key.nil?
67
+ raise @produce_error if @produce_error
68
+
69
+ @produced << {payload: payload, key: key, partition: partition}
70
+ DummyDeliveryHandle.new(@delivery_error)
71
+ end
72
+ end
73
+
74
+ RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION = -190
75
+ RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE = 10
76
+
77
+ class DummyErrorWithCode < StandardError
78
+ def code
79
+ :msg_size_too_large
80
+ end
81
+ end
82
+
83
+ def create_chunk(records, tag: 'test')
84
+ metadata = Fluent::Plugin::Buffer::Metadata.new(nil, tag, nil)
85
+ chunk = Fluent::Plugin::Buffer::MemoryChunk.new(metadata)
86
+ chunk.extend(Fluent::ChunkMessagePackEventStreamer)
87
+ chunk.append(records.map { |record| [event_time, record].to_msgpack })
88
+ chunk
89
+ end
90
+
91
+ def test_rdkafka_patch_is_applied
92
+ version = Gem::Version.create(Rdkafka::VERSION)
93
+ expected = if version >= Gem::Version.create('0.16.0')
94
+ '0_16_0.rb'
95
+ elsif version >= Gem::Version.create('0.14.0')
96
+ '0_14_0.rb'
97
+ elsif version >= Gem::Version.create('0.13.0')
98
+ '0_13_0.rb'
99
+ elsif version >= Gem::Version.create('0.12.0')
100
+ '0_12_0.rb'
101
+ else
102
+ '0_11_0.rb'
103
+ end
104
+
105
+ assert_equal expected, File.basename(Rdkafka::Producer.instance_method(:close).source_location.first)
106
+ end
107
+
108
+ def test_producer_close_accepts_timeout
109
+ producer = Rdkafka::Config.new("bootstrap.servers" => "localhost:9092").producer
110
+
111
+ assert_true producer.close(10)
112
+ end
113
+
40
114
  def test_configure
41
115
  assert_nothing_raised(Fluent::ConfigError) {
42
116
  create_driver(base_config)
@@ -55,8 +129,30 @@ class Rdkafka2OutputTest < Test::Unit::TestCase
55
129
  assert_equal 'localhost:9092', d.instance.brokers
56
130
  end
57
131
 
58
- def test_configure_sasl_plain
132
+ def test_configure_sasl_plain_over_ssl
133
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
134
+ "ssl_ca_cert" => "/path/to/ca_cert.pem"}, [])
135
+ d = create_driver(conf)
136
+
137
+ config = d.instance.build_config
138
+
139
+ assert_equal 'PLAIN', config[:"sasl.mechanisms"]
140
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
141
+ assert_equal 'testuser', config[:"sasl.username"]
142
+ assert_equal 'testpass', config[:"sasl.password"]
143
+ end
144
+
145
+ def test_configure_sasl_plain_without_ssl
59
146
  conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass"}, [])
147
+
148
+ assert_raise(Fluent::ConfigError) {
149
+ create_driver(conf)
150
+ }
151
+ end
152
+
153
+ def test_configure_sasl_plain_without_ssl_allowed_by_sasl_over_ssl
154
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
155
+ "sasl_over_ssl" => "false"}, [])
60
156
  d = create_driver(conf)
61
157
 
62
158
  config = d.instance.build_config
@@ -67,6 +163,250 @@ class Rdkafka2OutputTest < Test::Unit::TestCase
67
163
  assert_equal 'testpass', config[:"sasl.password"]
68
164
  end
69
165
 
166
+ def test_configure_sasl_plain_with_security_protocol_from_rdkafka_options
167
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
168
+ "rdkafka_options" => '{"security.protocol": "SASL_SSL"}'}, [])
169
+ d = create_driver(conf)
170
+
171
+ config = d.instance.build_config
172
+
173
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
174
+ end
175
+
176
+ def test_configure_sasl_plain_with_lowercase_security_protocol_from_rdkafka_options
177
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
178
+ "rdkafka_options" => '{"security.protocol": "sasl_plaintext"}'}, [])
179
+
180
+ assert_raise(Fluent::ConfigError) {
181
+ create_driver(conf)
182
+ }
183
+ end
184
+
185
+ data("sha256" => ["sha256", "SCRAM-SHA-256"],
186
+ "sha512" => ["sha512", "SCRAM-SHA-512"])
187
+ def test_configure_sasl_scram(data)
188
+ mechanism, expected = data
189
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
190
+ "scram_mechanism" => mechanism,
191
+ "ssl_ca_cert" => "/path/to/ca_cert.pem"}, [])
192
+ d = create_driver(conf)
193
+
194
+ config = d.instance.build_config
195
+
196
+ assert_equal expected, config[:"sasl.mechanisms"]
197
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
198
+ assert_equal 'testuser', config[:"sasl.username"]
199
+ assert_equal 'testpass', config[:"sasl.password"]
200
+ end
201
+
202
+ def test_configure_ssl_ca_certs_from_system
203
+ conf = base_config + config_element('ROOT', '', {"ssl_ca_certs_from_system" => "true"}, [])
204
+ d = create_driver(conf)
205
+
206
+ config = d.instance.build_config
207
+
208
+ assert_equal 'SSL', config[:"security.protocol"]
209
+ assert_nil config[:"ssl.ca.location"]
210
+ end
211
+
212
+ def test_configure_ssl_client_cert_without_ca_cert
213
+ conf = base_config + config_element('ROOT', '', {"ssl_client_cert" => "/path/to/cert.pem",
214
+ "ssl_client_cert_key" => "/path/to/key.pem"}, [])
215
+ d = create_driver(conf)
216
+
217
+ config = d.instance.build_config
218
+
219
+ assert_equal 'SSL', config[:"security.protocol"]
220
+ assert_equal '/path/to/cert.pem', config[:"ssl.certificate.location"]
221
+ assert_equal '/path/to/key.pem', config[:"ssl.key.location"]
222
+ assert_nil config[:"ssl.ca.location"]
223
+ end
224
+
225
+ def test_configure_sasl_plain_over_ssl_ca_certs_from_system
226
+ conf = base_config + config_element('ROOT', '', {"username" => "testuser", "password" => "testpass",
227
+ "ssl_ca_certs_from_system" => "true"}, [])
228
+ d = create_driver(conf)
229
+
230
+ config = d.instance.build_config
231
+
232
+ assert_equal 'SASL_SSL', config[:"security.protocol"]
233
+ assert_equal 'testpass', config[:"sasl.password"]
234
+ end
235
+
236
+ def test_configure_ssl_verify_hostname_default
237
+ conf = base_config + config_element('ROOT', '', {"ssl_ca_cert" => "/path/to/ca_cert.pem"}, [])
238
+ d = create_driver(conf)
239
+
240
+ config = d.instance.build_config
241
+
242
+ assert_equal 'SSL', config[:"security.protocol"]
243
+ assert_equal 'https', config[:"ssl.endpoint.identification.algorithm"]
244
+ assert_equal true, config[:"enable.ssl.certificate.verification"]
245
+ end
246
+
247
+ def test_configure_ssl_verify_hostname_false
248
+ conf = base_config + config_element('ROOT', '', {"ssl_ca_cert" => "/path/to/ca_cert.pem",
249
+ "ssl_verify_hostname" => "false"}, [])
250
+ d = create_driver(conf)
251
+
252
+ config = d.instance.build_config
253
+
254
+ assert_equal 'none', config[:"ssl.endpoint.identification.algorithm"]
255
+ assert_equal true, config[:"enable.ssl.certificate.verification"]
256
+ end
257
+
258
+ def test_configure_without_ssl_has_no_endpoint_identification
259
+ d = create_driver
260
+
261
+ config = d.instance.build_config
262
+
263
+ assert_equal 'PLAINTEXT', config[:"security.protocol"]
264
+ assert_nil config[:"ssl.endpoint.identification.algorithm"]
265
+ assert_nil config[:"enable.ssl.certificate.verification"]
266
+ end
267
+
268
+ data("non numeric partition" => "not-a-number",
269
+ "hexadecimal partition" => "0x10",
270
+ "float partition" => 3.9,
271
+ "negative partition" => -2,
272
+ "out of int32 range" => 2**31)
273
+ def test_write_skips_event_with_invalid_partition(partition)
274
+ d = create_driver
275
+ producer = DummyProducer.new
276
+ stub(d.instance).get_producer { producer }
277
+
278
+ assert_nothing_raised {
279
+ d.instance.write(create_chunk([{"a" => "b"}, {"a" => "c", "partition" => partition}, {"a" => "d"}]))
280
+ }
281
+
282
+ assert_equal ['{"a":"b"}', '{"a":"d"}'], producer.produced.collect { |message| message[:payload] }
283
+ end
284
+
285
+ def test_write_keeps_event_with_zero_padded_partition
286
+ d = create_driver
287
+ producer = DummyProducer.new
288
+ stub(d.instance).get_producer { producer }
289
+
290
+ d.instance.write(create_chunk([{"a" => "b", "partition" => "010"}]))
291
+
292
+ assert_equal [10], producer.produced.collect { |message| message[:partition] }
293
+ end
294
+
295
+ def test_write_converts_non_string_message_key
296
+ d = create_driver
297
+ producer = DummyProducer.new
298
+ stub(d.instance).get_producer { producer }
299
+
300
+ d.instance.write(create_chunk([{"a" => "b", "message_key" => 12345}]))
301
+
302
+ assert_equal ["12345"], producer.produced.collect { |message| message[:key] }
303
+ end
304
+
305
+ def test_rdkafka_error_codes_under_test
306
+ assert_equal [:unknown_partition, :msg_size_too_large],
307
+ [Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION).code,
308
+ Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE).code]
309
+ end
310
+
311
+ data("produce" => :produce_error,
312
+ "delivery report" => :delivery_error)
313
+ def test_write_raises_unrecoverable_error_for_listed_error_code(path)
314
+ d = create_driver
315
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE))
316
+ stub(d.instance).get_producer { producer }
317
+
318
+ assert_raise(Fluent::UnrecoverableError) {
319
+ d.instance.write(create_chunk([{"a" => "b"}]))
320
+ }
321
+ end
322
+
323
+ data("produce" => :produce_error,
324
+ "delivery report" => :delivery_error)
325
+ def test_write_reraises_unlisted_error_code(path)
326
+ d = create_driver
327
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION))
328
+ stub(d.instance).get_producer { producer }
329
+
330
+ assert_raise(Rdkafka::RdkafkaError) {
331
+ d.instance.write(create_chunk([{"a" => "b"}]))
332
+ }
333
+ end
334
+
335
+ data("produce" => :produce_error,
336
+ "delivery report" => :delivery_error)
337
+ def test_write_raises_unrecoverable_error_for_unknown_partition_when_listed(path)
338
+ d = create_driver(config + config_element('ROOT', '', {"unrecoverable_error_codes" => "unknown_partition"}))
339
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION))
340
+ stub(d.instance).get_producer { producer }
341
+
342
+ assert_raise(Fluent::UnrecoverableError) {
343
+ d.instance.write(create_chunk([{"a" => "b"}]))
344
+ }
345
+ end
346
+
347
+ data("produce" => :produce_error,
348
+ "delivery report" => :delivery_error)
349
+ def test_write_discards_listed_error_code_when_discard_kafka_delivery_failed(path)
350
+ d = create_driver(config + config_element('ROOT', '', {"discard_kafka_delivery_failed" => "true"}))
351
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE))
352
+ stub(d.instance).get_producer { producer }
353
+
354
+ assert_nothing_raised {
355
+ d.instance.write(create_chunk([{"a" => "b"}]))
356
+ }
357
+ assert_true d.logs.any? { |log| log.include?("Delivery failed. Discard events") }
358
+ end
359
+
360
+ data("produce" => :produce_error,
361
+ "delivery report" => :delivery_error)
362
+ def test_write_reraises_error_that_is_not_a_kafka_error(path)
363
+ d = create_driver
364
+ producer = DummyProducer.new(path => DummyErrorWithCode.new)
365
+ stub(d.instance).get_producer { producer }
366
+
367
+ assert_raise(DummyErrorWithCode) {
368
+ d.instance.write(create_chunk([{"a" => "b"}]))
369
+ }
370
+ end
371
+
372
+ data("produce" => :produce_error,
373
+ "delivery report" => :delivery_error)
374
+ def test_write_discards_listed_error_code_matching_discard_kafka_delivery_failed_regex(path)
375
+ d = create_driver(config + config_element('ROOT', '', {"discard_kafka_delivery_failed_regex" => "/msg_size_too_large/"}))
376
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE))
377
+ stub(d.instance).get_producer { producer }
378
+
379
+ assert_nothing_raised {
380
+ d.instance.write(create_chunk([{"a" => "b"}]))
381
+ }
382
+ assert_true d.logs.any? { |log| log.include?("Delivery failed and matched regexp pattern") }
383
+ end
384
+
385
+ def test_write_produces_every_record_before_raising_unrecoverable_error
386
+ d = create_driver
387
+ producer = DummyProducer.new(delivery_error: Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE))
388
+ stub(d.instance).get_producer { producer }
389
+
390
+ assert_raise(Fluent::UnrecoverableError) {
391
+ d.instance.write(create_chunk([{"a" => "b"}, {"a" => "c"}, {"a" => "d"}]))
392
+ }
393
+
394
+ assert_equal ['{"a":"b"}', '{"a":"c"}', '{"a":"d"}'], producer.produced.collect { |message| message[:payload] }
395
+ end
396
+
397
+ data("produce" => :produce_error,
398
+ "delivery report" => :delivery_error)
399
+ def test_write_reraises_listed_error_code_when_unrecoverable_error_codes_is_empty(path)
400
+ d = create_driver(config + config_element('ROOT', '', {"unrecoverable_error_codes" => ""}))
401
+ producer = DummyProducer.new(path => Rdkafka::RdkafkaError.new(RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE))
402
+ stub(d.instance).get_producer { producer }
403
+
404
+ assert_equal [], d.instance.unrecoverable_error_codes
405
+ assert_raise(Rdkafka::RdkafkaError) {
406
+ d.instance.write(create_chunk([{"a" => "b"}]))
407
+ }
408
+ end
409
+
70
410
  def test_mutli_worker_support
71
411
  d = create_driver
72
412
  assert_equal true, d.instance.multi_workers_ready?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.7
4
+ version: 0.19.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hidemasa Togashi
@@ -187,6 +187,7 @@ files:
187
187
  - lib/fluent/plugin/out_rdkafka2.rb
188
188
  - lib/fluent/plugin/rdkafka_patch/0_11_0.rb
189
189
  - lib/fluent/plugin/rdkafka_patch/0_12_0.rb
190
+ - lib/fluent/plugin/rdkafka_patch/0_13_0.rb
190
191
  - lib/fluent/plugin/rdkafka_patch/0_14_0.rb
191
192
  - lib/fluent/plugin/rdkafka_patch/0_16_0.rb
192
193
  - test/helper.rb
@@ -197,6 +198,7 @@ files:
197
198
  - test/plugin/test_out_kafka.rb
198
199
  - test/plugin/test_out_kafka2.rb
199
200
  - test/plugin/test_out_kafka_buffered.rb
201
+ - test/plugin/test_out_rdkafka.rb
200
202
  - test/plugin/test_out_rdkafka2.rb
201
203
  homepage: https://github.com/fluent/fluent-plugin-kafka
202
204
  licenses:
@@ -216,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
218
  - !ruby/object:Gem::Version
217
219
  version: '0'
218
220
  requirements: []
219
- rubygems_version: 4.0.10
221
+ rubygems_version: 4.0.18
220
222
  specification_version: 4
221
223
  summary: Fluentd plugin for Apache Kafka > 0.8
222
224
  test_files:
@@ -228,4 +230,5 @@ test_files:
228
230
  - test/plugin/test_out_kafka.rb
229
231
  - test/plugin/test_out_kafka2.rb
230
232
  - test/plugin/test_out_kafka_buffered.rb
233
+ - test/plugin/test_out_rdkafka.rb
231
234
  - test/plugin/test_out_rdkafka2.rb