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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +18 -1
- data/.github/workflows/add-to-project.yml +9 -2
- data/.github/workflows/linux.yml +14 -6
- data/.github/workflows/stale-actions.yml +4 -1
- data/ChangeLog +56 -0
- data/README.md +17 -0
- data/ci/prepare-kafka-server.sh +13 -1
- data/fluent-plugin-kafka.gemspec +1 -1
- data/lib/fluent/plugin/in_kafka.rb +19 -12
- data/lib/fluent/plugin/in_kafka_group.rb +4 -4
- data/lib/fluent/plugin/in_rdkafka_group.rb +35 -6
- data/lib/fluent/plugin/kafka_plugin_util.rb +106 -1
- data/lib/fluent/plugin/out_kafka.rb +3 -3
- data/lib/fluent/plugin/out_kafka2.rb +4 -2
- data/lib/fluent/plugin/out_kafka_buffered.rb +3 -3
- data/lib/fluent/plugin/out_rdkafka.rb +6 -4
- data/lib/fluent/plugin/out_rdkafka2.rb +21 -48
- data/lib/fluent/plugin/rdkafka_patch/0_13_0.rb +43 -0
- data/lib/fluent/plugin/rdkafka_patch/0_16_0.rb +3 -2
- data/test/plugin/test_in_kafka.rb +127 -0
- data/test/plugin/test_in_rdkafka_group.rb +326 -0
- data/test/plugin/test_kafka_plugin_util.rb +26 -0
- data/test/plugin/test_out_kafka2.rb +99 -0
- data/test/plugin/test_out_rdkafka.rb +105 -0
- data/test/plugin/test_out_rdkafka2.rb +341 -1
- metadata +7 -2
|
@@ -6,6 +6,7 @@ class KafkaPluginUtilTest < Test::Unit::TestCase
|
|
|
6
6
|
def self.config_param(name, type, options)
|
|
7
7
|
end
|
|
8
8
|
include Fluent::KafkaPluginUtil::SSLSettings
|
|
9
|
+
include Fluent::KafkaPluginUtil::PartitionSettings
|
|
9
10
|
|
|
10
11
|
def config_param
|
|
11
12
|
end
|
|
@@ -13,6 +14,31 @@ class KafkaPluginUtilTest < Test::Unit::TestCase
|
|
|
13
14
|
Fluent::Test.setup
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
data("integer" => [3, 3],
|
|
18
|
+
"decimal string" => ["3", 3],
|
|
19
|
+
"zero padded string" => ["010", 10],
|
|
20
|
+
"unassigned partition" => [-1, -1],
|
|
21
|
+
"max int32" => [2**31 - 1, 2**31 - 1])
|
|
22
|
+
def test_coerce_partition(data)
|
|
23
|
+
given, expected = data
|
|
24
|
+
assert_equal(expected, coerce_partition(given))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
data("non numeric string" => ["not-a-number", ArgumentError],
|
|
28
|
+
"empty string" => ["", ArgumentError],
|
|
29
|
+
"hexadecimal string" => ["0x10", ArgumentError],
|
|
30
|
+
"float string" => ["3.5", ArgumentError],
|
|
31
|
+
"float" => [3.9, TypeError],
|
|
32
|
+
"nil" => [nil, TypeError],
|
|
33
|
+
"negative partition" => [-2, RangeError],
|
|
34
|
+
"too big for int32" => [2**31, RangeError])
|
|
35
|
+
def test_coerce_partition_rejects_invalid_value(data)
|
|
36
|
+
given, expected = data
|
|
37
|
+
assert_raise(expected) do
|
|
38
|
+
coerce_partition(given)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
16
42
|
def test_read_ssl_file_when_nil
|
|
17
43
|
stub(File).read(anything) do |path|
|
|
18
44
|
path
|
|
@@ -27,6 +27,32 @@ class Kafka2OutputTest < Test::Unit::TestCase
|
|
|
27
27
|
Fluent::Test::Driver::Output.new(Fluent::Kafka2Output).configure(conf)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
class DummyProducer
|
|
31
|
+
attr_reader :produced
|
|
32
|
+
|
|
33
|
+
# Coerces like ruby-kafka's Producer#produce, which is where an invalid
|
|
34
|
+
# partition raises today
|
|
35
|
+
def produce(value, key:, partition_key:, partition:, headers:, create_time:, topic:)
|
|
36
|
+
@produced ||= []
|
|
37
|
+
@produced << {value: value && value.to_s, key: key && key.to_s,
|
|
38
|
+
partition: partition && Integer(partition)}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def deliver_messages
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear_buffer
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_chunk(records, tag: 'test')
|
|
49
|
+
metadata = Fluent::Plugin::Buffer::Metadata.new(nil, tag, nil)
|
|
50
|
+
chunk = Fluent::Plugin::Buffer::MemoryChunk.new(metadata)
|
|
51
|
+
chunk.extend(Fluent::ChunkMessagePackEventStreamer)
|
|
52
|
+
chunk.append(records.map { |record| [event_time, record].to_msgpack })
|
|
53
|
+
chunk
|
|
54
|
+
end
|
|
55
|
+
|
|
30
56
|
def test_configure
|
|
31
57
|
assert_nothing_raised(Fluent::ConfigError) {
|
|
32
58
|
create_driver(base_config)
|
|
@@ -45,6 +71,79 @@ class Kafka2OutputTest < Test::Unit::TestCase
|
|
|
45
71
|
assert_equal ['localhost:9092'], d.instance.brokers
|
|
46
72
|
end
|
|
47
73
|
|
|
74
|
+
data("cert without key" => {"ssl_client_cert" => "/path/to/cert.pem"},
|
|
75
|
+
"key without cert" => {"ssl_client_cert_key" => "/path/to/key.pem"},
|
|
76
|
+
"chain without cert" => {"ssl_client_cert_chain" => "/path/to/chain.pem"},
|
|
77
|
+
"key password without key" => {"ssl_client_cert_key_password" => "secret"})
|
|
78
|
+
def test_configure_incomplete_ssl_client_cert(params)
|
|
79
|
+
conf = config + config_element('ROOT', '', params, [])
|
|
80
|
+
|
|
81
|
+
assert_raise(Fluent::ConfigError) {
|
|
82
|
+
create_driver(conf)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_configure_ssl_client_cert_with_key
|
|
87
|
+
conf = config + config_element('ROOT', '', {"ssl_client_cert" => "/path/to/cert.pem",
|
|
88
|
+
"ssl_client_cert_key" => "/path/to/key.pem"}, [])
|
|
89
|
+
|
|
90
|
+
assert_nothing_raised(Fluent::ConfigError) {
|
|
91
|
+
create_driver(conf)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
data("sha256" => "sha256",
|
|
96
|
+
"sha512" => "sha512")
|
|
97
|
+
def test_configure_scram_mechanism(mechanism)
|
|
98
|
+
conf = config + config_element('ROOT', '', {"username" => "testuser",
|
|
99
|
+
"password" => "testpass",
|
|
100
|
+
"scram_mechanism" => mechanism,
|
|
101
|
+
"ssl_ca_certs_from_system" => "true"}, [])
|
|
102
|
+
d = create_driver(conf)
|
|
103
|
+
|
|
104
|
+
assert_equal mechanism, d.instance.scram_mechanism
|
|
105
|
+
|
|
106
|
+
assert_nothing_raised {
|
|
107
|
+
d.instance.refresh_client
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_configure_unsupported_scram_mechanism
|
|
112
|
+
conf = config + config_element('ROOT', '', {"username" => "testuser",
|
|
113
|
+
"password" => "testpass",
|
|
114
|
+
"scram_mechanism" => "sha1"}, [])
|
|
115
|
+
|
|
116
|
+
assert_raise(Fluent::ConfigError) {
|
|
117
|
+
create_driver(conf)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
data("non numeric partition" => "not-a-number",
|
|
122
|
+
"hash partition" => {"x" => 1},
|
|
123
|
+
"negative partition" => -2,
|
|
124
|
+
"out of int32 range" => 2**31)
|
|
125
|
+
def test_write_skips_event_with_invalid_partition(partition)
|
|
126
|
+
d = create_driver
|
|
127
|
+
producer = DummyProducer.new
|
|
128
|
+
stub(d.instance).get_producer { producer }
|
|
129
|
+
|
|
130
|
+
assert_nothing_raised {
|
|
131
|
+
d.instance.write(create_chunk([{"a" => "b"}, {"a" => "c", "partition" => partition}, {"a" => "d"}]))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
assert_equal ['{"a":"b"}', '{"a":"d"}'], producer.produced.collect { |message| message[:value] }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_write_keeps_event_with_zero_padded_partition
|
|
138
|
+
d = create_driver
|
|
139
|
+
producer = DummyProducer.new
|
|
140
|
+
stub(d.instance).get_producer { producer }
|
|
141
|
+
|
|
142
|
+
d.instance.write(create_chunk([{"a" => "b", "partition" => "010"}]))
|
|
143
|
+
|
|
144
|
+
assert_equal [10], producer.produced.collect { |message| message[:partition] }
|
|
145
|
+
end
|
|
146
|
+
|
|
48
147
|
data("crc32" => "crc32",
|
|
49
148
|
"murmur2" => "murmur2")
|
|
50
149
|
def test_partitioner_hash_function(data)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
require 'fluent/test/helpers'
|
|
3
|
+
require 'fluent/test/driver/output'
|
|
4
|
+
|
|
5
|
+
class RdkafkaOutputTest < Test::Unit::TestCase
|
|
6
|
+
include Fluent::Test::Helpers
|
|
7
|
+
|
|
8
|
+
def have_rdkafka
|
|
9
|
+
begin
|
|
10
|
+
require 'fluent/plugin/out_rdkafka'
|
|
11
|
+
true
|
|
12
|
+
rescue LoadError
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def setup
|
|
18
|
+
omit_unless(have_rdkafka, "rdkafka isn't installed")
|
|
19
|
+
Fluent::Test.setup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base_config(params = {})
|
|
23
|
+
config_element('ROOT', '', {"@type" => "rdkafka",
|
|
24
|
+
"brokers" => "localhost:9092"}.merge(params), [])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_driver(conf = base_config)
|
|
28
|
+
Fluent::Test::Driver::Output.new(Fluent::KafkaOutputBuffered2).configure(conf)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_configure
|
|
32
|
+
d = create_driver
|
|
33
|
+
|
|
34
|
+
assert_equal 'localhost:9092', d.instance.brokers
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_configure_ssl_ca_cert
|
|
38
|
+
d = create_driver(base_config("ssl_ca_cert" => "/path/to/ca_cert.pem"))
|
|
39
|
+
|
|
40
|
+
config = d.instance.build_config
|
|
41
|
+
|
|
42
|
+
assert_equal 'SSL', config[:"security.protocol"]
|
|
43
|
+
assert_equal '/path/to/ca_cert.pem', config[:"ssl.ca.location"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_configure_ssl_ca_certs_from_system
|
|
47
|
+
d = create_driver(base_config("ssl_ca_certs_from_system" => "true"))
|
|
48
|
+
|
|
49
|
+
config = d.instance.build_config
|
|
50
|
+
|
|
51
|
+
assert_equal 'SSL', config[:"security.protocol"]
|
|
52
|
+
assert_nil config[:"ssl.ca.location"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_configure_ssl_client_cert_without_ca_cert
|
|
56
|
+
d = create_driver(base_config("ssl_client_cert" => "/path/to/cert.pem",
|
|
57
|
+
"ssl_client_cert_key" => "/path/to/key.pem"))
|
|
58
|
+
|
|
59
|
+
config = d.instance.build_config
|
|
60
|
+
|
|
61
|
+
assert_equal 'SSL', config[:"security.protocol"]
|
|
62
|
+
assert_equal '/path/to/cert.pem', config[:"ssl.certificate.location"]
|
|
63
|
+
assert_equal '/path/to/key.pem', config[:"ssl.key.location"]
|
|
64
|
+
assert_nil config[:"ssl.ca.location"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_configure_ssl_verify_hostname_default
|
|
68
|
+
d = create_driver(base_config("ssl_ca_cert" => "/path/to/ca_cert.pem"))
|
|
69
|
+
|
|
70
|
+
config = d.instance.build_config
|
|
71
|
+
|
|
72
|
+
assert_equal 'SSL', config[:"security.protocol"]
|
|
73
|
+
assert_equal 'https', config[:"ssl.endpoint.identification.algorithm"]
|
|
74
|
+
assert_equal true, config[:"enable.ssl.certificate.verification"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_configure_ssl_verify_hostname_false
|
|
78
|
+
d = create_driver(base_config("ssl_ca_cert" => "/path/to/ca_cert.pem",
|
|
79
|
+
"ssl_verify_hostname" => "false"))
|
|
80
|
+
|
|
81
|
+
config = d.instance.build_config
|
|
82
|
+
|
|
83
|
+
assert_equal 'none', config[:"ssl.endpoint.identification.algorithm"]
|
|
84
|
+
assert_equal true, config[:"enable.ssl.certificate.verification"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_configure_without_ssl_has_no_endpoint_identification
|
|
88
|
+
config = create_driver.instance.build_config
|
|
89
|
+
|
|
90
|
+
assert_equal 'PLAINTEXT', config[:"security.protocol"]
|
|
91
|
+
assert_nil config[:"ssl.endpoint.identification.algorithm"]
|
|
92
|
+
assert_nil config[:"enable.ssl.certificate.verification"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_configure_sasl_gssapi_over_ssl
|
|
96
|
+
d = create_driver(base_config("principal" => "testuser@EXAMPLE.COM",
|
|
97
|
+
"ssl_client_cert" => "/path/to/cert.pem",
|
|
98
|
+
"ssl_client_cert_key" => "/path/to/key.pem"))
|
|
99
|
+
|
|
100
|
+
config = d.instance.build_config
|
|
101
|
+
|
|
102
|
+
assert_equal 'SASL_SSL', config[:"security.protocol"]
|
|
103
|
+
assert_equal 'GSSAPI', config[:"sasl.mechanisms"]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -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
|
|
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.
|
|
4
|
+
version: 0.19.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hidemasa Togashi
|
|
@@ -187,15 +187,18 @@ 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
|
|
193
194
|
- test/plugin/test_in_kafka.rb
|
|
194
195
|
- test/plugin/test_in_kafka_group.rb
|
|
196
|
+
- test/plugin/test_in_rdkafka_group.rb
|
|
195
197
|
- test/plugin/test_kafka_plugin_util.rb
|
|
196
198
|
- test/plugin/test_out_kafka.rb
|
|
197
199
|
- test/plugin/test_out_kafka2.rb
|
|
198
200
|
- test/plugin/test_out_kafka_buffered.rb
|
|
201
|
+
- test/plugin/test_out_rdkafka.rb
|
|
199
202
|
- test/plugin/test_out_rdkafka2.rb
|
|
200
203
|
homepage: https://github.com/fluent/fluent-plugin-kafka
|
|
201
204
|
licenses:
|
|
@@ -215,15 +218,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
215
218
|
- !ruby/object:Gem::Version
|
|
216
219
|
version: '0'
|
|
217
220
|
requirements: []
|
|
218
|
-
rubygems_version: 4.0.
|
|
221
|
+
rubygems_version: 4.0.18
|
|
219
222
|
specification_version: 4
|
|
220
223
|
summary: Fluentd plugin for Apache Kafka > 0.8
|
|
221
224
|
test_files:
|
|
222
225
|
- test/helper.rb
|
|
223
226
|
- test/plugin/test_in_kafka.rb
|
|
224
227
|
- test/plugin/test_in_kafka_group.rb
|
|
228
|
+
- test/plugin/test_in_rdkafka_group.rb
|
|
225
229
|
- test/plugin/test_kafka_plugin_util.rb
|
|
226
230
|
- test/plugin/test_out_kafka.rb
|
|
227
231
|
- test/plugin/test_out_kafka2.rb
|
|
228
232
|
- test/plugin/test_out_kafka_buffered.rb
|
|
233
|
+
- test/plugin/test_out_rdkafka.rb
|
|
229
234
|
- test/plugin/test_out_rdkafka2.rb
|