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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +17 -0
- data/.github/workflows/linux.yml +2 -2
- data/.github/workflows/stale-actions.yml +1 -1
- data/ChangeLog +53 -0
- data/README.md +13 -0
- 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 +15 -3
- 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 +198 -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 +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3e5a24c5a5c936b9d502631d6382faa9315a54f0813e5b636cf025ecb64212c
|
|
4
|
+
data.tar.gz: 43c1b088840c92c330c70c456d92e3849d9de326b6b772c55bae63affea9f3e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 608800269cb2f306911e4a1a6a6aeea8b7e0e69fde4b34b5a165f67d4a306e82226de890151ffff813a9ba91ef4399d87aeb531243cb63cdf625a023cf92d3e7
|
|
7
|
+
data.tar.gz: e818390d3f8946d0449a80d8c8ddd612e4a96632b2d6765fdcbf3beeb394c0cdc7c95e0cdce8c0780a05e66e39812328ddb48cd32135476cf5a85dac244485ef
|
data/.github/dependabot.yml
CHANGED
|
@@ -4,3 +4,20 @@ updates:
|
|
|
4
4
|
directory: '/'
|
|
5
5
|
schedule:
|
|
6
6
|
interval: 'monthly'
|
|
7
|
+
groups:
|
|
8
|
+
# PR: "Security update [package] from [old] to [new]"
|
|
9
|
+
# This PR should be merged in hurry
|
|
10
|
+
security-updates:
|
|
11
|
+
applies-to: security-updates
|
|
12
|
+
patterns:
|
|
13
|
+
- '*'
|
|
14
|
+
|
|
15
|
+
# PR: "Bump [package] from [old] to [new]"
|
|
16
|
+
# No need to be merged this PR in hurry. It is enough to merge
|
|
17
|
+
# once in a month.
|
|
18
|
+
monthly-updates:
|
|
19
|
+
applies-to: version-updates
|
|
20
|
+
patterns:
|
|
21
|
+
- '*'
|
|
22
|
+
# Allow to create PR both of security and normal updates.
|
|
23
|
+
open-pull-requests-limit: 1
|
data/.github/workflows/linux.yml
CHANGED
|
@@ -46,8 +46,8 @@ jobs:
|
|
|
46
46
|
rdkafka_versions: { min: '>= 0.12.0', max: '= 0.15.2' }
|
|
47
47
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }} with rdkafka gem version (min ${{ matrix.rdkafka_versions.min }} max ${{ matrix.rdkafka_versions.max }})
|
|
48
48
|
steps:
|
|
49
|
-
- uses: actions/checkout@
|
|
50
|
-
- uses: ruby/setup-ruby@
|
|
49
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
50
|
+
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
51
51
|
with:
|
|
52
52
|
ruby-version: ${{ matrix.ruby }}
|
|
53
53
|
- name: Install confluent-kafka
|
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
issues: write
|
|
11
11
|
pull-requests: write
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/stale@
|
|
13
|
+
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
|
14
14
|
with:
|
|
15
15
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
16
16
|
days-before-stale: 90
|
data/ChangeLog
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
Release 0.19.8 - 2026/09/04
|
|
2
|
+
* in_kafka: Emit each record with its own tag when `tag_source record` is
|
|
3
|
+
set. The whole fetch batch used to be emitted with the tag of the
|
|
4
|
+
last parsed record, so `tag_source record` never routed anything.
|
|
5
|
+
A record whose tag field is not a non-empty String is now skipped
|
|
6
|
+
with a warning, and the offset advances even when every message of
|
|
7
|
+
a batch is skipped. (#581)
|
|
8
|
+
* in_rdkafka_group: Apply SSL and SASL parameters to the consumer.
|
|
9
|
+
`ssl_ca_cert`, `ssl_client_cert`, `username`, `password`,
|
|
10
|
+
`scram_mechanism` and `principal` were accepted and then discarded,
|
|
11
|
+
so a consumer configured for SSL connected in the clear. NOTE: these
|
|
12
|
+
consumers start negotiating TLS after this change, and passing SASL
|
|
13
|
+
credentials through `kafka_configs` with `security.protocol
|
|
14
|
+
SASL_PLAINTEXT` is now rejected at startup unless `sasl_over_ssl
|
|
15
|
+
false` is set. (#580)
|
|
16
|
+
* out_rdkafka2: Apply `unrecoverable_error_codes` to delivery report
|
|
17
|
+
errors. The check only ran for errors raised by `produce`, so even the
|
|
18
|
+
default `msg_size_too_large` kept the chunk retrying until
|
|
19
|
+
`retry_timeout`. NOTE: such a chunk is now passed to the secondary
|
|
20
|
+
output, or written to the backup directory. (#579)
|
|
21
|
+
* out_rdkafka2: Fix the rdkafka patch version dispatch. The 0_16_0 patch
|
|
22
|
+
had been unreachable since it was added, and rdkafka-ruby 0.12.1
|
|
23
|
+
through 0.13.x got no patch at all, where `producer.close(10)` raises
|
|
24
|
+
ArgumentError on shutdown. Add a patch for 0.13.x. (#578)
|
|
25
|
+
* out_kafka2, out_rdkafka2: Skip an event whose `partition` is not a
|
|
26
|
+
decimal integer within the int32 range, and coerce `message_key` with
|
|
27
|
+
`to_s`. Such an event used to raise outside the per-event guard and
|
|
28
|
+
Fluentd retried the same chunk forever, stalling every source feeding
|
|
29
|
+
the output. NOTE: `partition` is now read as base 10, so "010" means
|
|
30
|
+
10 rather than 8. (#576, #577)
|
|
31
|
+
* out_rdkafka2: Fix `scram_mechanism` being ignored. SCRAM was silently
|
|
32
|
+
downgraded to PLAIN. `scram_mechanism` is now an enum of the
|
|
33
|
+
mechanisms ruby-kafka accepts, so an unknown value is rejected at
|
|
34
|
+
startup instead of falling back. (#574, #575)
|
|
35
|
+
* out_rdkafka, out_rdkafka2: Fix `ssl_verify_hostname` being ignored, and
|
|
36
|
+
set the SSL flag for any of `ssl_ca_cert`, `ssl_ca_certs_from_system`,
|
|
37
|
+
`ssl_client_cert` and `ssl_client_cert_key`. A mutual TLS setup
|
|
38
|
+
without `ssl_ca_cert` used to connect in the clear. NOTE: these
|
|
39
|
+
configurations start negotiating TLS after this change. (#572, #573)
|
|
40
|
+
* Validate that `ssl_client_cert`, `ssl_client_cert_key`,
|
|
41
|
+
`ssl_client_cert_chain` and `ssl_client_cert_key_password` are set
|
|
42
|
+
together, in every plugin that accepts them. librdkafka used to accept
|
|
43
|
+
a certificate without a key and connect with no client identity.
|
|
44
|
+
NOTE: such a configuration is now rejected at startup. (#571)
|
|
45
|
+
* out_rdkafka2: Add `sasl_over_ssl` parameter and reject SASL credentials
|
|
46
|
+
over a plaintext connection. NOTE: a configuration that sends
|
|
47
|
+
`username` and `password` over `PLAINTEXT` or `SASL_PLAINTEXT` now
|
|
48
|
+
fails at startup. Set `sasl_over_ssl false` only if you deliberately
|
|
49
|
+
keep sending them in the clear. (#570)
|
|
50
|
+
* Replace yajl-ruby with the json gem for JSON handling, and keep
|
|
51
|
+
accepting duplicate keys so that json 3.0 does not raise
|
|
52
|
+
`JSON::ParserError`. (#566, #568)
|
|
53
|
+
|
|
1
54
|
Release 0.19.7 - 2026/06/02
|
|
2
55
|
* in_rdkafka_group: support regexp pattern in topics (#541)
|
|
3
56
|
|
data/README.md
CHANGED
|
@@ -166,6 +166,12 @@ With the introduction of the rdkafka-ruby based input plugin we hope to support
|
|
|
166
166
|
time_source <source for message timestamp (now|kafka|record)> :default => now
|
|
167
167
|
time_format <string (Optional when use_record_time is used)>
|
|
168
168
|
|
|
169
|
+
# The SSL and SASL parameters in "Common parameters" are mapped to librdkafka
|
|
170
|
+
# properties. Keys given in kafka_configs take precedence over the mapped values.
|
|
171
|
+
# ssl_ca_cert uses only its first entry, and ssl_client_cert_chain is not mapped.
|
|
172
|
+
# When true, SASL authentication requires an SSL connection
|
|
173
|
+
sasl_over_ssl (bool) :default => true
|
|
174
|
+
|
|
169
175
|
# kafka consumer options
|
|
170
176
|
max_wait_time_ms 500
|
|
171
177
|
max_batch_size 10000
|
|
@@ -523,6 +529,8 @@ You need to install rdkafka gem.
|
|
|
523
529
|
discard_kafka_delivery_failed (bool) :default => false (No discard)
|
|
524
530
|
discard_kafka_delivery_failed_regex (regexp) :default => nil (No discard)
|
|
525
531
|
use_event_time (bool) :default => false
|
|
532
|
+
# When true, SASL authentication requires an SSL connection
|
|
533
|
+
sasl_over_ssl (bool) :default => true
|
|
526
534
|
|
|
527
535
|
# same with kafka2
|
|
528
536
|
headers (hash) :default => {}
|
|
@@ -569,6 +577,11 @@ You need to install rdkafka gem.
|
|
|
569
577
|
`rdkafka2` supports `discard_kafka_delivery_failed_regex` parameter:
|
|
570
578
|
- `discard_kafka_delivery_failed_regex` - default: nil - discard the record where the Kafka::DeliveryFailed occurred and the emitted message matches the given regex pattern, such as `/unknown_topic/`.
|
|
571
579
|
|
|
580
|
+
`rdkafka2` supports `unrecoverable_error_codes` parameter:
|
|
581
|
+
- `unrecoverable_error_codes` - default: `["topic_authorization_failed", "msg_size_too_large"]` - librdkafka error codes to treat as unrecoverable errors. When a listed code is returned, `Fluent::UnrecoverableError` is raised, so Fluentd hands the whole chunk to the secondary output or to the backup directory instead of retrying it until `retry_timeout` expires. Both the produce call and the delivery report are covered, the latter only while `rdkafka_delivery_handle_poll_timeout` is not 0. Set the parameter to an empty value keeping retrying every error, as this plugin did before v0.19.x.
|
|
582
|
+
- A code is spelled the way `Rdkafka::RdkafkaError#code` reports it, that is the librdkafka name lowercased with the leading underscore removed, so `RD_KAFKA_RESP_ERR__UNKNOWN_PARTITION` is written as `unknown_partition`. A name that matches no error code is accepted and never matches anything.
|
|
583
|
+
- `discard_kafka_delivery_failed` and `discard_kafka_delivery_failed_regex` take precedence over this parameter. The regex is matched against the librdkafka message, such as `Broker: Message size too large (msg_size_too_large)`.
|
|
584
|
+
|
|
572
585
|
If you use v0.12, use `rdkafka` instead.
|
|
573
586
|
|
|
574
587
|
<match kafka.**>
|
data/fluent-plugin-kafka.gemspec
CHANGED
|
@@ -13,7 +13,7 @@ Gem::Specification.new do |gem|
|
|
|
13
13
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
14
|
gem.name = "fluent-plugin-kafka"
|
|
15
15
|
gem.require_paths = ["lib"]
|
|
16
|
-
gem.version = '0.19.
|
|
16
|
+
gem.version = '0.19.8'
|
|
17
17
|
gem.required_ruby_version = ">= 2.1.0"
|
|
18
18
|
|
|
19
19
|
gem.add_dependency "fluentd", [">= 0.10.58", "< 2"]
|
|
@@ -143,9 +143,9 @@ class Fluent::KafkaInput < Fluent::Input
|
|
|
143
143
|
r
|
|
144
144
|
}
|
|
145
145
|
rescue LoadError
|
|
146
|
-
require '
|
|
146
|
+
require 'json'
|
|
147
147
|
Proc.new { |msg, te|
|
|
148
|
-
r =
|
|
148
|
+
r = JSON.parse(msg.value, allow_duplicate_key: true)
|
|
149
149
|
add_offset_in_hash(r, te, msg.offset) if @add_offset_in_record
|
|
150
150
|
r
|
|
151
151
|
}
|
|
@@ -302,18 +302,24 @@ class Fluent::KafkaInput < Fluent::Input
|
|
|
302
302
|
|
|
303
303
|
return if messages.size.zero?
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
event_streams = {}
|
|
306
|
+
topic_tag = @topic_entry.topic
|
|
307
|
+
topic_tag = @add_prefix + "." + topic_tag if @add_prefix
|
|
308
|
+
topic_tag = topic_tag + "." + @add_suffix if @add_suffix
|
|
309
309
|
|
|
310
310
|
messages.each { |msg|
|
|
311
311
|
begin
|
|
312
312
|
record = @parser.call(msg, @topic_entry)
|
|
313
313
|
if @tag_source == :record
|
|
314
314
|
tag = record[@record_tag_key]
|
|
315
|
+
unless tag.is_a?(String) && !tag.empty?
|
|
316
|
+
$log.warn "skipped record with invalid tag in #{@topic_entry.topic}/#{@topic_entry.partition}", :key => @record_tag_key, :value => tag, :offset => msg.offset
|
|
317
|
+
next
|
|
318
|
+
end
|
|
315
319
|
tag = @add_prefix + "." + tag if @add_prefix
|
|
316
320
|
tag = tag + "." + @add_suffix if @add_suffix
|
|
321
|
+
else
|
|
322
|
+
tag = topic_tag
|
|
317
323
|
end
|
|
318
324
|
case @time_source
|
|
319
325
|
when :kafka
|
|
@@ -332,7 +338,8 @@ class Fluent::KafkaInput < Fluent::Input
|
|
|
332
338
|
if @kafka_message_key
|
|
333
339
|
record[@kafka_message_key] = msg.key
|
|
334
340
|
end
|
|
335
|
-
|
|
341
|
+
event_streams[tag] ||= Fluent::MultiEventStream.new
|
|
342
|
+
event_streams[tag].add(record_time, record)
|
|
336
343
|
rescue => e
|
|
337
344
|
$log.warn "parser error in #{@topic_entry.topic}/#{@topic_entry.partition}", :error => e.to_s, :value => msg.value, :offset => msg.offset
|
|
338
345
|
$log.debug_backtrace
|
|
@@ -340,14 +347,14 @@ class Fluent::KafkaInput < Fluent::Input
|
|
|
340
347
|
}
|
|
341
348
|
offset = messages.last.offset + 1
|
|
342
349
|
|
|
343
|
-
|
|
350
|
+
event_streams.each { |tag, es|
|
|
344
351
|
@router.emit_stream(tag, es)
|
|
352
|
+
}
|
|
345
353
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
end
|
|
349
|
-
@next_offset = offset
|
|
354
|
+
if @offset_manager
|
|
355
|
+
@offset_manager.save_offset(offset)
|
|
350
356
|
end
|
|
357
|
+
@next_offset = offset
|
|
351
358
|
end
|
|
352
359
|
end
|
|
353
360
|
|
|
@@ -160,8 +160,8 @@ class Fluent::KafkaGroupInput < Fluent::Input
|
|
|
160
160
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
161
161
|
Proc.new { |msg| Oj.load(msg.value) }
|
|
162
162
|
rescue LoadError
|
|
163
|
-
require '
|
|
164
|
-
Proc.new { |msg|
|
|
163
|
+
require 'json'
|
|
164
|
+
Proc.new { |msg| JSON.parse(msg.value, allow_duplicate_key: true) }
|
|
165
165
|
end
|
|
166
166
|
when 'ltsv'
|
|
167
167
|
require 'ltsv'
|
|
@@ -265,7 +265,7 @@ class Fluent::KafkaGroupInput < Fluent::Input
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
def process_batch_with_record_tag(batch)
|
|
268
|
-
es = {}
|
|
268
|
+
es = {}
|
|
269
269
|
batch.messages.each { |msg|
|
|
270
270
|
begin
|
|
271
271
|
record = @parser_proc.call(msg)
|
|
@@ -361,7 +361,7 @@ class Fluent::KafkaGroupInput < Fluent::Input
|
|
|
361
361
|
if @tag_source == :record
|
|
362
362
|
process_batch_with_record_tag(batch)
|
|
363
363
|
else
|
|
364
|
-
process_batch(batch)
|
|
364
|
+
process_batch(batch)
|
|
365
365
|
end
|
|
366
366
|
}
|
|
367
367
|
rescue ForShutdown
|
|
@@ -56,6 +56,7 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
56
56
|
|
|
57
57
|
include Fluent::KafkaPluginUtil::SSLSettings
|
|
58
58
|
include Fluent::KafkaPluginUtil::SaslSettings
|
|
59
|
+
include Fluent::KafkaPluginUtil::RdkafkaSecuritySettings
|
|
59
60
|
|
|
60
61
|
class ForShutdown < StandardError
|
|
61
62
|
end
|
|
@@ -126,6 +127,8 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
126
127
|
if @time_source == :record and @time_format
|
|
127
128
|
@time_parser = Fluent::TimeParser.new(@time_format)
|
|
128
129
|
end
|
|
130
|
+
|
|
131
|
+
@rdkafka_config = build_config
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def setup_parser(parser_conf)
|
|
@@ -137,8 +140,8 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
137
140
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
138
141
|
Proc.new { |msg| Oj.load(msg.payload) }
|
|
139
142
|
rescue LoadError
|
|
140
|
-
require '
|
|
141
|
-
Proc.new { |msg|
|
|
143
|
+
require 'json'
|
|
144
|
+
Proc.new { |msg| JSON.parse(msg.payload, allow_duplicate_key: true) }
|
|
142
145
|
end
|
|
143
146
|
when 'ltsv'
|
|
144
147
|
require 'ltsv'
|
|
@@ -177,8 +180,17 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
177
180
|
super
|
|
178
181
|
end
|
|
179
182
|
|
|
183
|
+
def build_config
|
|
184
|
+
config = rdkafka_security_config
|
|
185
|
+
@kafka_configs.each { |k, v|
|
|
186
|
+
config[k.to_sym] = v
|
|
187
|
+
}
|
|
188
|
+
validate_sasl_over_ssl(config)
|
|
189
|
+
config
|
|
190
|
+
end
|
|
191
|
+
|
|
180
192
|
def setup_consumer
|
|
181
|
-
consumer = Rdkafka::Config.new(@
|
|
193
|
+
consumer = Rdkafka::Config.new(@rdkafka_config).consumer
|
|
182
194
|
consumer.subscribe(*@topics)
|
|
183
195
|
consumer
|
|
184
196
|
end
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
module Fluent
|
|
2
2
|
module KafkaPluginUtil
|
|
3
|
+
module PartitionSettings
|
|
4
|
+
# -1 is the unassigned partition; the Kafka protocol takes the index as int32
|
|
5
|
+
PARTITION_RANGE = -1..(2**31 - 1)
|
|
6
|
+
|
|
7
|
+
def coerce_partition(partition)
|
|
8
|
+
partition = case partition
|
|
9
|
+
when Integer
|
|
10
|
+
partition
|
|
11
|
+
when String
|
|
12
|
+
Integer(partition, 10)
|
|
13
|
+
else
|
|
14
|
+
raise TypeError, "partition must be an Integer or a decimal String, got #{partition.class}"
|
|
15
|
+
end
|
|
16
|
+
raise RangeError, "partition #{partition} is out of range" unless PARTITION_RANGE.cover?(partition)
|
|
17
|
+
|
|
18
|
+
partition
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
3
22
|
module AwsIamSettings
|
|
4
23
|
def self.included(klass)
|
|
5
24
|
klass.instance_eval do
|
|
@@ -36,6 +55,20 @@ module Fluent
|
|
|
36
55
|
|
|
37
56
|
DummyFormatter = Object.new
|
|
38
57
|
|
|
58
|
+
def configure(conf)
|
|
59
|
+
super
|
|
60
|
+
|
|
61
|
+
if @ssl_client_cert && !@ssl_client_cert_key
|
|
62
|
+
raise Fluent::ConfigError, "ssl_client_cert is set but ssl_client_cert_key is not. Please provide both."
|
|
63
|
+
elsif !@ssl_client_cert && @ssl_client_cert_key
|
|
64
|
+
raise Fluent::ConfigError, "ssl_client_cert_key is set but ssl_client_cert is not. Please provide both."
|
|
65
|
+
elsif @ssl_client_cert_chain && !@ssl_client_cert
|
|
66
|
+
raise Fluent::ConfigError, "ssl_client_cert_chain is set but ssl_client_cert is not. Please provide cert, key and chain."
|
|
67
|
+
elsif @ssl_client_cert_key_password && !@ssl_client_cert_key
|
|
68
|
+
raise Fluent::ConfigError, "ssl_client_cert_key_password is set but ssl_client_cert_key is not. Please provide both."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
39
72
|
def start
|
|
40
73
|
super
|
|
41
74
|
|
|
@@ -75,10 +108,82 @@ module Fluent
|
|
|
75
108
|
:desc => "a username when using PLAIN/SCRAM SASL authentication"
|
|
76
109
|
config_param :password, :string, :default => nil, secret: true,
|
|
77
110
|
:desc => "a password when using PLAIN/SCRAM SASL authentication"
|
|
78
|
-
config_param :scram_mechanism, :
|
|
111
|
+
config_param :scram_mechanism, :enum, :list => [:sha256, :sha512], :default => nil,
|
|
79
112
|
:desc => "if set, use SCRAM authentication with specified mechanism. When unset, default to PLAIN authentication"
|
|
80
113
|
}
|
|
81
114
|
end
|
|
115
|
+
|
|
116
|
+
def configure(conf)
|
|
117
|
+
super
|
|
118
|
+
|
|
119
|
+
@scram_mechanism = @scram_mechanism.to_s if @scram_mechanism
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
module RdkafkaSecuritySettings
|
|
124
|
+
SCRAM_MECHANISMS = {
|
|
125
|
+
"sha256" => "SCRAM-SHA-256",
|
|
126
|
+
"sha512" => "SCRAM-SHA-512",
|
|
127
|
+
}.freeze
|
|
128
|
+
|
|
129
|
+
def self.included(klass)
|
|
130
|
+
klass.instance_eval {
|
|
131
|
+
config_param :service_name, :string, :default => nil, :desc => 'Used for sasl.kerberos.service.name'
|
|
132
|
+
config_param :sasl_over_ssl, :bool, :default => true,
|
|
133
|
+
:desc => 'When true, SASL authentication requires an SSL connection'
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def rdkafka_security_config
|
|
138
|
+
config = {}
|
|
139
|
+
|
|
140
|
+
if (@ssl_ca_cert && @ssl_ca_cert[0]) || @ssl_ca_certs_from_system || @ssl_client_cert || @ssl_client_cert_key
|
|
141
|
+
ssl = true
|
|
142
|
+
config[:"ssl.ca.location"] = @ssl_ca_cert[0] if @ssl_ca_cert && @ssl_ca_cert[0]
|
|
143
|
+
config[:"ssl.certificate.location"] = @ssl_client_cert if @ssl_client_cert
|
|
144
|
+
config[:"ssl.key.location"] = @ssl_client_cert_key if @ssl_client_cert_key
|
|
145
|
+
config[:"ssl.key.password"] = @ssl_client_cert_key_password if @ssl_client_cert_key_password
|
|
146
|
+
config[:"ssl.endpoint.identification.algorithm"] = @ssl_verify_hostname ? "https" : "none"
|
|
147
|
+
config[:"enable.ssl.certificate.verification"] = true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if @principal
|
|
151
|
+
sasl = true
|
|
152
|
+
config[:"sasl.mechanisms"] = "GSSAPI"
|
|
153
|
+
config[:"sasl.kerberos.principal"] = @principal
|
|
154
|
+
config[:"sasl.kerberos.service.name"] = @service_name if @service_name
|
|
155
|
+
config[:"sasl.kerberos.keytab"] = @keytab if @keytab
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if @username && @password
|
|
159
|
+
sasl = true
|
|
160
|
+
config[:"sasl.mechanisms"] = SCRAM_MECHANISMS.fetch(@scram_mechanism, 'PLAIN')
|
|
161
|
+
elsif @scram_mechanism
|
|
162
|
+
log.warn "scram_mechanism is ignored because username and password are not set"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if ssl && sasl
|
|
166
|
+
security_protocol = "SASL_SSL"
|
|
167
|
+
elsif ssl && !sasl
|
|
168
|
+
security_protocol = "SSL"
|
|
169
|
+
elsif !ssl && sasl
|
|
170
|
+
security_protocol = "SASL_PLAINTEXT"
|
|
171
|
+
else
|
|
172
|
+
security_protocol = "PLAINTEXT"
|
|
173
|
+
end
|
|
174
|
+
config[:"security.protocol"] = security_protocol
|
|
175
|
+
|
|
176
|
+
config[:"sasl.username"] = @username if @username
|
|
177
|
+
config[:"sasl.password"] = @password if @password
|
|
178
|
+
|
|
179
|
+
config
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_sasl_over_ssl(config)
|
|
183
|
+
if @sasl_over_ssl && config[:"sasl.password"] && config[:"security.protocol"].to_s.upcase == "SASL_PLAINTEXT"
|
|
184
|
+
raise Fluent::ConfigError, "SASL authentication requires that SSL is configured. Set 'sasl_over_ssl false' to send SASL credentials over a plaintext connection"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
82
187
|
end
|
|
83
188
|
end
|
|
84
189
|
end
|
|
@@ -96,7 +96,7 @@ DESC
|
|
|
96
96
|
@seed_brokers = []
|
|
97
97
|
z = Zookeeper.new(@zookeeper)
|
|
98
98
|
z.get_children(:path => @zookeeper_path)[:children].each do |id|
|
|
99
|
-
broker =
|
|
99
|
+
broker = JSON.parse(z.get(:path => @zookeeper_path + "/#{id}")[:data], allow_duplicate_key: true)
|
|
100
100
|
if @ssl_client_cert
|
|
101
101
|
@seed_brokers.push(pickup_ssl_endpoint(broker))
|
|
102
102
|
else
|
|
@@ -192,8 +192,8 @@ DESC
|
|
|
192
192
|
|
|
193
193
|
def setup_formatter(conf)
|
|
194
194
|
if @output_data_type == 'json'
|
|
195
|
-
require '
|
|
196
|
-
Proc.new { |tag, time, record|
|
|
195
|
+
require 'json'
|
|
196
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
197
197
|
elsif @output_data_type == 'ltsv'
|
|
198
198
|
require 'ltsv'
|
|
199
199
|
Proc.new { |tag, time, record| LTSV.dump(record) }
|
|
@@ -98,6 +98,7 @@ DESC
|
|
|
98
98
|
include Fluent::KafkaPluginUtil::AwsIamSettings
|
|
99
99
|
include Fluent::KafkaPluginUtil::SSLSettings
|
|
100
100
|
include Fluent::KafkaPluginUtil::SaslSettings
|
|
101
|
+
include Fluent::KafkaPluginUtil::PartitionSettings
|
|
101
102
|
|
|
102
103
|
def initialize
|
|
103
104
|
super
|
|
@@ -314,8 +315,8 @@ DESC
|
|
|
314
315
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
315
316
|
Proc.new { |tag, time, record| Oj.dump(record) }
|
|
316
317
|
rescue LoadError
|
|
317
|
-
require '
|
|
318
|
-
Proc.new { |tag, time, record|
|
|
318
|
+
require 'json'
|
|
319
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
319
320
|
end
|
|
320
321
|
when 'ltsv'
|
|
321
322
|
require 'ltsv'
|
|
@@ -351,6 +352,7 @@ DESC
|
|
|
351
352
|
partition_key = (@exclude_partition_key ? record.delete(@partition_key_key) : record[@partition_key_key]) || @default_partition_key
|
|
352
353
|
partition = (@exclude_partition ? record.delete(@partition_key) : record[@partition_key]) || @default_partition
|
|
353
354
|
message_key = (@exclude_message_key ? record.delete(@message_key_key) : record[@message_key_key]) || @default_message_key
|
|
355
|
+
partition = coerce_partition(partition) unless partition.nil?
|
|
354
356
|
|
|
355
357
|
if mutate_headers
|
|
356
358
|
headers = base_headers.clone
|
|
@@ -119,7 +119,7 @@ DESC
|
|
|
119
119
|
@seed_brokers = []
|
|
120
120
|
z = Zookeeper.new(@zookeeper)
|
|
121
121
|
z.get_children(:path => @zookeeper_path)[:children].each do |id|
|
|
122
|
-
broker =
|
|
122
|
+
broker = JSON.parse(z.get(:path => @zookeeper_path + "/#{id}")[:data], allow_duplicate_key: true)
|
|
123
123
|
if @ssl_client_cert
|
|
124
124
|
@seed_brokers.push(pickup_ssl_endpoint(broker))
|
|
125
125
|
else
|
|
@@ -259,8 +259,8 @@ DESC
|
|
|
259
259
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
260
260
|
Proc.new { |tag, time, record| Oj.dump(record) }
|
|
261
261
|
rescue LoadError
|
|
262
|
-
require '
|
|
263
|
-
Proc.new { |tag, time, record|
|
|
262
|
+
require 'json'
|
|
263
|
+
Proc.new { |tag, time, record| JSON.generate(record) }
|
|
264
264
|
end
|
|
265
265
|
elsif @output_data_type == 'ltsv'
|
|
266
266
|
require 'ltsv'
|
|
@@ -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 '
|
|
232
|
-
Proc.new { |tag, time, 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
|
|
11
|
-
require_relative 'rdkafka_patch/
|
|
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.
|
|
17
|
-
require_relative 'rdkafka_patch/
|
|
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
|
-
|
|
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
|