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
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
|
@@ -3,4 +3,21 @@ updates:
|
|
|
3
3
|
- package-ecosystem: 'github-actions'
|
|
4
4
|
directory: '/'
|
|
5
5
|
schedule:
|
|
6
|
-
interval: '
|
|
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
|
|
@@ -10,8 +10,15 @@ jobs:
|
|
|
10
10
|
name: Add issue to project
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
-
|
|
13
|
+
- name: Generate token
|
|
14
|
+
id: generate_token
|
|
15
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
16
|
+
with:
|
|
17
|
+
app-id: ${{ secrets.PROJECT_APP_ID }}
|
|
18
|
+
private-key: ${{ secrets.PROJECT_APP_PRIVATE_KEY }}
|
|
19
|
+
owner: fluent
|
|
20
|
+
- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
|
|
14
21
|
with:
|
|
15
22
|
project-url: https://github.com/orgs/fluent/projects/4
|
|
16
|
-
github-token: ${{
|
|
23
|
+
github-token: ${{ steps.generate_token.outputs.token }}
|
|
17
24
|
labeled: waiting-for-triage
|
data/.github/workflows/linux.yml
CHANGED
|
@@ -4,15 +4,23 @@ on:
|
|
|
4
4
|
branches: [master]
|
|
5
5
|
pull_request:
|
|
6
6
|
branches: [master]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: '0 0 1 * *'
|
|
7
9
|
jobs:
|
|
10
|
+
ruby-versions:
|
|
11
|
+
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
|
|
12
|
+
with:
|
|
13
|
+
engine: cruby
|
|
14
|
+
min_version: 3.0
|
|
8
15
|
build:
|
|
16
|
+
needs: ruby-versions
|
|
9
17
|
runs-on: ${{ matrix.os }}
|
|
10
18
|
env:
|
|
11
19
|
USE_RDKAFKA: 1
|
|
12
20
|
strategy:
|
|
13
21
|
fail-fast: false
|
|
14
22
|
matrix:
|
|
15
|
-
ruby:
|
|
23
|
+
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
|
|
16
24
|
os:
|
|
17
25
|
- ubuntu-latest
|
|
18
26
|
rdkafka_versions:
|
|
@@ -23,6 +31,7 @@ jobs:
|
|
|
23
31
|
- { min: '>= 0.25.0', max: '>= 0.25.0' }
|
|
24
32
|
# rdkafka 0.25.0 supports Ruby 3.2+, so we exclude it for Ruby 3.0 and 3.1
|
|
25
33
|
exclude:
|
|
34
|
+
- ruby: head
|
|
26
35
|
- ruby: '3.1'
|
|
27
36
|
rdkafka_versions: { min: '>= 0.25.0', max: '>= 0.25.0' }
|
|
28
37
|
- ruby: '3.0'
|
|
@@ -37,17 +46,17 @@ jobs:
|
|
|
37
46
|
rdkafka_versions: { min: '>= 0.12.0', max: '= 0.15.2' }
|
|
38
47
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }} with rdkafka gem version (min ${{ matrix.rdkafka_versions.min }} max ${{ matrix.rdkafka_versions.max }})
|
|
39
48
|
steps:
|
|
40
|
-
- uses: actions/checkout@
|
|
41
|
-
- uses: ruby/setup-ruby@v1
|
|
49
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
50
|
+
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
42
51
|
with:
|
|
43
52
|
ruby-version: ${{ matrix.ruby }}
|
|
44
53
|
- name: Install confluent-kafka
|
|
45
54
|
run: |
|
|
46
55
|
sudo apt install -V -y gnupg2 wget
|
|
47
|
-
wget https://packages.confluent.io/deb/
|
|
56
|
+
wget https://packages.confluent.io/deb/7.9/archive.key
|
|
48
57
|
sudo gpg2 --homedir /tmp --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/confluent-archive-keyring.gpg --import archive.key
|
|
49
58
|
sudo chmod 644 /usr/share/keyrings/confluent-archive-keyring.gpg
|
|
50
|
-
sudo sh -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/confluent-archive-keyring.gpg] https://packages.confluent.io/deb/
|
|
59
|
+
sudo sh -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/confluent-archive-keyring.gpg] https://packages.confluent.io/deb/7.9 stable main" > /etc/apt/sources.list.d/confluent.list'
|
|
51
60
|
sudo apt update
|
|
52
61
|
sudo apt install -y confluent-community-2.13 openjdk-11-jre netcat-openbsd
|
|
53
62
|
- name: unit testing
|
|
@@ -57,6 +66,5 @@ jobs:
|
|
|
57
66
|
RDKAFKA_VERSION_MAX_RANGE: ${{ matrix.rdkafka_versions.max }}
|
|
58
67
|
run: |
|
|
59
68
|
sudo ./ci/prepare-kafka-server.sh
|
|
60
|
-
gem install rake
|
|
61
69
|
bundle install --jobs 4 --retry 3
|
|
62
70
|
bundle exec rake test
|
|
@@ -6,8 +6,11 @@ on:
|
|
|
6
6
|
jobs:
|
|
7
7
|
stale:
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
issues: write
|
|
11
|
+
pull-requests: write
|
|
9
12
|
steps:
|
|
10
|
-
- uses: actions/stale@
|
|
13
|
+
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
|
11
14
|
with:
|
|
12
15
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
13
16
|
days-before-stale: 90
|
data/ChangeLog
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
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
|
+
|
|
54
|
+
Release 0.19.7 - 2026/06/02
|
|
55
|
+
* in_rdkafka_group: support regexp pattern in topics (#541)
|
|
56
|
+
|
|
1
57
|
Release 0.19.6 - 2026/03/04
|
|
2
58
|
* out_rdkafka2: Auto-configure SASL PLAIN when username and password are set (#547)
|
|
3
59
|
* out_rdkafka2: Fix NoMethodError during graceful reload (#546)
|
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
|
|
@@ -177,6 +183,10 @@ With the introduction of the rdkafka-ruby based input plugin we hope to support
|
|
|
177
183
|
|
|
178
184
|
See also [rdkafka-ruby](https://github.com/appsignal/rdkafka-ruby) and [librdkafka](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md) for more detailed documentation about Kafka consumer options.
|
|
179
185
|
|
|
186
|
+
`topics` supports POSIX Extended Regular Expression pattern (not Ruby regex syntax) since v0.19.6. If you want to use regex pattern, use `/pattern/` like `/foo.*/`.
|
|
187
|
+
|
|
188
|
+
**Note**: A caret (`^`) is automatically added to the beginning of the pattern.
|
|
189
|
+
|
|
180
190
|
Consuming topic name is used for event tag. So when the target topic name is `app_event`, the tag is `app_event`. If you want to modify tag, use `add_prefix` or `add_suffix` parameter. With `add_prefix kafka`, the tag is `kafka.app_event`.
|
|
181
191
|
|
|
182
192
|
### Output plugin
|
|
@@ -519,6 +529,8 @@ You need to install rdkafka gem.
|
|
|
519
529
|
discard_kafka_delivery_failed (bool) :default => false (No discard)
|
|
520
530
|
discard_kafka_delivery_failed_regex (regexp) :default => nil (No discard)
|
|
521
531
|
use_event_time (bool) :default => false
|
|
532
|
+
# When true, SASL authentication requires an SSL connection
|
|
533
|
+
sasl_over_ssl (bool) :default => true
|
|
522
534
|
|
|
523
535
|
# same with kafka2
|
|
524
536
|
headers (hash) :default => {}
|
|
@@ -565,6 +577,11 @@ You need to install rdkafka gem.
|
|
|
565
577
|
`rdkafka2` supports `discard_kafka_delivery_failed_regex` parameter:
|
|
566
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/`.
|
|
567
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
|
+
|
|
568
585
|
If you use v0.12, use `rdkafka` instead.
|
|
569
586
|
|
|
570
587
|
<match kafka.**>
|
data/ci/prepare-kafka-server.sh
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
|
|
3
|
+
set -x
|
|
4
|
+
|
|
3
5
|
export KAFKA_OPTS=-Dzookeeper.4lw.commands.whitelist=ruok
|
|
4
6
|
/usr/bin/zookeeper-server-start /etc/kafka/zookeeper.properties &
|
|
5
7
|
N_POLLING=30
|
|
@@ -30,4 +32,14 @@ while true ; do
|
|
|
30
32
|
exit 1
|
|
31
33
|
fi
|
|
32
34
|
done
|
|
33
|
-
|
|
35
|
+
# kafka-topics --version fails to connect zookeeper here.
|
|
36
|
+
case "$(java -cp "/usr/share/java/kafka/*" kafka.Kafka --version 2>/dev/null)" in
|
|
37
|
+
7.*)
|
|
38
|
+
/usr/bin/kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
# Deprecated zookeeper
|
|
42
|
+
/usr/bin/kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
|
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
|
|
@@ -76,13 +77,25 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
76
77
|
end
|
|
77
78
|
config_array
|
|
78
79
|
end
|
|
80
|
+
private :_config_to_array
|
|
81
|
+
|
|
82
|
+
def regex_pattern?(topic)
|
|
83
|
+
topic.start_with?('/') && topic.end_with?('/') ? true : false
|
|
84
|
+
end
|
|
85
|
+
private :regex_pattern?
|
|
86
|
+
|
|
87
|
+
def _config_regex_pattern(topic)
|
|
88
|
+
# librdkafka recognizes string as regex pattern if the topic name starts with '^'.
|
|
89
|
+
# https://github.com/confluentinc/librdkafka/blob/570c785e9e35812db8f50254bd2f7e0cf47def39/src/rdkafka.h#L4148
|
|
90
|
+
# https://github.com/confluentinc/librdkafka/blob/e1db7eaa517f0a6438bc846a9c49ede73b9ea211/src/rdkafka_topic.c#L2064
|
|
91
|
+
[topic[1..-2].prepend('^')]
|
|
92
|
+
end
|
|
93
|
+
private :_config_regex_pattern
|
|
79
94
|
|
|
80
95
|
def multi_workers_ready?
|
|
81
96
|
true
|
|
82
97
|
end
|
|
83
98
|
|
|
84
|
-
private :_config_to_array
|
|
85
|
-
|
|
86
99
|
def configure(conf)
|
|
87
100
|
compat_parameters_convert(conf, :parser)
|
|
88
101
|
|
|
@@ -93,7 +106,12 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
93
106
|
log.info "Will watch for topics #{@topics} at brokers " \
|
|
94
107
|
"#{@kafka_configs["bootstrap.servers"]} and '#{@kafka_configs["group.id"]}' group"
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
topics = @topics.strip
|
|
110
|
+
if regex_pattern?(topics)
|
|
111
|
+
@topics = _config_regex_pattern(topics)
|
|
112
|
+
else
|
|
113
|
+
@topics = _config_to_array(topics)
|
|
114
|
+
end
|
|
97
115
|
|
|
98
116
|
parser_conf = conf.elements('parse').first
|
|
99
117
|
unless parser_conf
|
|
@@ -109,6 +127,8 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
109
127
|
if @time_source == :record and @time_format
|
|
110
128
|
@time_parser = Fluent::TimeParser.new(@time_format)
|
|
111
129
|
end
|
|
130
|
+
|
|
131
|
+
@rdkafka_config = build_config
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
def setup_parser(parser_conf)
|
|
@@ -120,8 +140,8 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
120
140
|
Oj.default_options = Fluent::DEFAULT_OJ_OPTIONS
|
|
121
141
|
Proc.new { |msg| Oj.load(msg.payload) }
|
|
122
142
|
rescue LoadError
|
|
123
|
-
require '
|
|
124
|
-
Proc.new { |msg|
|
|
143
|
+
require 'json'
|
|
144
|
+
Proc.new { |msg| JSON.parse(msg.payload, allow_duplicate_key: true) }
|
|
125
145
|
end
|
|
126
146
|
when 'ltsv'
|
|
127
147
|
require 'ltsv'
|
|
@@ -160,8 +180,17 @@ class Fluent::Plugin::RdKafkaGroupInput < Fluent::Plugin::Input
|
|
|
160
180
|
super
|
|
161
181
|
end
|
|
162
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
|
+
|
|
163
192
|
def setup_consumer
|
|
164
|
-
consumer = Rdkafka::Config.new(@
|
|
193
|
+
consumer = Rdkafka::Config.new(@rdkafka_config).consumer
|
|
165
194
|
consumer.subscribe(*@topics)
|
|
166
195
|
consumer
|
|
167
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'
|