logstash-integration-kafka 12.1.11-java → 12.2.0-java

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feab8e952a04b460776884674e8fb07912690f6419017191ffd98533a7e94e6f
4
- data.tar.gz: b1b448e15fc162f6e2e63529b1f63db77ec6a780562a824a0ac709e990791a7b
3
+ metadata.gz: 8ce12663a941ab9311aaa94446325a942ee2c8ee8d3f76b3b0a510b2c7729297
4
+ data.tar.gz: 70fa9a7637f015f1f0a53b7273151643bddaf8c22451a24bfa1ff309052ccbb5
5
5
  SHA512:
6
- metadata.gz: 02c08cd82a4db2950196aae82ad01f05ca460dbd99bc32631899549262d9009f33e64ebc42adc564a78cb4d58802aaf83abc3849a53bcfe5ae17f2fe469b3300
7
- data.tar.gz: 3a48b0830cfc0df951f20fe5e6b6544dc75d2e9349840a0f39e82a2cc287750ac1285b8f4bee104e6ac45bbee91460815085455df79647ddf46e7b8f4bfc8d52
6
+ metadata.gz: 1a5dad3768d4295bc13b4739c758efb3f141b009d2342086ef8ee4e130bc943c1c7285fde656624b5aaa1e4c05f5ed92ae6b6386983c34aee4bb018d4f0e7e04
7
+ data.tar.gz: 75b92286c2c12e1d49ec0b86d33a50ede5697e830e09b531bc68c458f0f62a0e686f2143a7e952150ea58eba52e56e4f838f4db36aa488b94bd826f52679692c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 12.2.0
2
+ - Add share group consumer support to Kafka input plugin via `consumer_mode => "share_group"` (Kafka 4.0+ KIP-932) [#252](https://github.com/logstash-plugins/logstash-integration-kafka/pull/252) [#252](https://github.com/logstash-plugins/logstash-integration-kafka/pull/252)
3
+
1
4
  ## 12.1.11
2
5
  - Update jackson dependency to 2.21.7, and require `snappy-java` 1.1.10.10 or later and `guava` 33.7.2-jre or later [#289](https://github.com/logstash-plugins/logstash-integration-kafka/pull/289)
3
6
 
@@ -53,13 +53,48 @@ The Logstash Kafka consumer handles group management and uses the default offset
53
53
  strategy using Kafka topics.
54
54
 
55
55
  Logstash instances by default form a single logical group to subscribe to Kafka topics
56
- Each Logstash Kafka consumer can run multiple threads to increase read throughput. Alternatively,
56
+ Each Logstash Kafka consumer can run multiple threads to increase read throughput. Alternatively,
57
57
  you could run multiple Logstash instances with the same `group_id` to spread the load across
58
58
  physical machines. Messages in a topic will be distributed to all Logstash instances with
59
59
  the same `group_id`.
60
60
 
61
61
  Ideally you should have as many threads as the number of partitions for a perfect balance --
62
- more threads than partitions means that some threads will be idle
62
+ more threads than partitions means that some threads will be idle. This applies to the default
63
+ `consumer_mode => consumer_group`; it does not apply to `consumer_mode => share_group`, which is
64
+ not partition-oriented. See <<plugins-{type}s-{plugin}-consumer_threads>>.
65
+
66
+ [id="plugins-{type}s-{plugin}-share-groups"]
67
+ ==== Kafka Share Groups (queue semantics)
68
+
69
+ Setting <<plugins-{type}s-{plugin}-consumer_mode>> to `share_group` enables Kafka 4.0+ Share
70
+ Groups (KIP-932). Share groups provide *queue semantics*: any consumer in the group can read
71
+ from any partition, and each record is delivered to exactly one consumer. Unlike consumer
72
+ groups, the number of active consumers is no longer bounded by the partition count.
73
+
74
+ Requirements:
75
+
76
+ * Kafka brokers 4.0 or later with `group.share.enable=true`
77
+ * The `group_id` setting identifies the share group name
78
+
79
+ Share group consumers must acknowledge each record.
80
+ After an event is accepted by the pipeline's queue, it is acknowledged as `ACCEPT` to ensure that no other consumer in the share group receives it.
81
+ When an event is either _not_ accepted by the pipeline's queue or an error occurs before it is offered to the queue, the <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>> option controls how the event is acknowledged (`release` for redelivery or `reject` to permanently discard).
82
+
83
+ Example configuration:
84
+
85
+ [source,ruby]
86
+ --
87
+ input {
88
+ kafka {
89
+ bootstrap_servers => "localhost:9092"
90
+ topics => ["my-topic"]
91
+ group_id => "my-share-group"
92
+ consumer_mode => "share_group"
93
+ consumer_threads => 4
94
+ share_group_acknowledgement_on_error => "release"
95
+ }
96
+ }
97
+ --
63
98
 
64
99
  For more information see https://kafka.apache.org/{kafka_client_doc}/documentation.html#theconsumer
65
100
 
@@ -123,6 +158,7 @@ See the https://kafka.apache.org/{kafka_client_doc}/documentation for more detai
123
158
  | <<plugins-{type}s-{plugin}-client_id>> |<<string,string>>|No
124
159
  | <<plugins-{type}s-{plugin}-client_rack>> |<<string,string>>|No
125
160
  | <<plugins-{type}s-{plugin}-connections_max_idle_ms>> |<<number,number>>|No
161
+ | <<plugins-{type}s-{plugin}-consumer_mode>> |<<string,string>>, one of `["consumer_group", "share_group"]`|No
126
162
  | <<plugins-{type}s-{plugin}-consumer_threads>> |<<number,number>>|No
127
163
  | <<plugins-{type}s-{plugin}-decorate_events>> |<<string,string>>|No
128
164
  | <<plugins-{type}s-{plugin}-enable_auto_commit>> |<<boolean,boolean>>|No
@@ -175,6 +211,7 @@ See the https://kafka.apache.org/{kafka_client_doc}/documentation for more detai
175
211
  | <<plugins-{type}s-{plugin}-security_protocol>> |<<string,string>>, one of `["PLAINTEXT", "SSL", "SASL_PLAINTEXT", "SASL_SSL"]`|No
176
212
  | <<plugins-{type}s-{plugin}-send_buffer_bytes>> |<<number,number>>|No
177
213
  | <<plugins-{type}s-{plugin}-session_timeout_ms>> |<<number,number>>|No
214
+ | <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>> |<<string,string>>, one of `["release", "reject"]`|No
178
215
  | <<plugins-{type}s-{plugin}-ssl_endpoint_identification_algorithm>> |<<string,string>>|No
179
216
  | <<plugins-{type}s-{plugin}-ssl_key_password>> |<<password,password>>|No
180
217
  | <<plugins-{type}s-{plugin}-ssl_keystore_location>> |a valid filesystem path|No
@@ -194,13 +231,18 @@ input plugins.
194
231
  &nbsp;
195
232
 
196
233
  [id="plugins-{type}s-{plugin}-auto_commit_interval_ms"]
197
- ===== `auto_commit_interval_ms`
234
+ ===== `auto_commit_interval_ms`
198
235
 
199
236
  * Value type is <<number,number>>
200
237
  * Default value is `5000`.
201
238
 
202
239
  The frequency in milliseconds that the consumer offsets are committed to Kafka.
203
240
 
241
+ [NOTE]
242
+ =====
243
+ This option is supported only with `consumer_mode => consumer_group`.
244
+ =====
245
+
204
246
  [id="plugins-{type}s-{plugin}-auto_offset_reset"]
205
247
  ===== `auto_offset_reset`
206
248
 
@@ -215,6 +257,12 @@ What to do when there is no initial offset in Kafka or if an offset is out of ra
215
257
  * none: throw exception to the consumer if no previous offset is found for the consumer's group
216
258
  * anything else: throw exception to the consumer.
217
259
 
260
+ [NOTE]
261
+ =====
262
+ This option is supported only with `consumer_mode => consumer_group`.
263
+ For `share_group` mode, the equivalent is the broker/group-level `share.auto.offset.reset` setting.
264
+ =====
265
+
218
266
  [id="plugins-{type}s-{plugin}-bootstrap_servers"]
219
267
  ===== `bootstrap_servers`
220
268
 
@@ -285,14 +333,53 @@ https://cwiki.apache.org/confluence/display/KAFKA/KIP-392%3A+Allow+consumers+to+
285
333
 
286
334
  Close idle connections after the number of milliseconds specified by this config.
287
335
 
336
+ [id="plugins-{type}s-{plugin}-consumer_mode"]
337
+ ===== `consumer_mode`
338
+
339
+ * Value can be either of: `consumer_group`, `share_group`
340
+ * Default value is `consumer_group`
341
+
342
+ Sets the consumer mode for this input.
343
+
344
+ `consumer_group` (default) uses the standard Kafka consumer group protocol (`KafkaConsumer`).
345
+ Partitions are assigned exclusively to consumers — each partition is consumed by exactly one
346
+ thread in the group at a time.
347
+
348
+ `share_group` uses the Kafka 4.0+ Share Group protocol (`KafkaShareConsumer`, KIP-932).
349
+ Share groups provide *queue semantics*: records from any partition can be fetched by any
350
+ consumer in the group, and each record is delivered to exactly one consumer. There is no
351
+ partition assignment, so any number of consumers can scale independently without being
352
+ limited by the partition count.
353
+
354
+ To use share groups, your Kafka brokers must be version 4.0 or later and must have
355
+ `group.share.enable=true` set in the broker configuration.
356
+
357
+ [NOTE]
358
+ ====
359
+ Setting any of the following options in the pipeline config raises a configuration error in
360
+ `share_group` mode, since they do not apply to the Share Group protocol: `group_protocol`,
361
+ `group_instance_id`, `partition_assignment_strategy`, `heartbeat_interval_ms`,
362
+ `session_timeout_ms`, `isolation_level`, `enable_auto_commit`, `auto_commit_interval_ms`,
363
+ `auto_offset_reset`, `exclude_internal_topics`, `schema_registry_url`.
364
+
365
+ Some of these have broker/group-level equivalents for share groups that can be configured
366
+ directly on the broker (for example, `auto.offset.reset` → `share.auto.offset.reset`,
367
+ `heartbeat.interval.ms` → `share.heartbeat.interval.ms`,
368
+ `session.timeout.ms` → `share.session.timeout.ms`,
369
+ `isolation.level` → `share.isolation.level`).
370
+ ====
371
+
288
372
  [id="plugins-{type}s-{plugin}-consumer_threads"]
289
- ===== `consumer_threads`
373
+ ===== `consumer_threads`
290
374
 
291
375
  * Value type is <<number,number>>
292
376
  * Default value is `1`
293
377
 
294
- Ideally you should have as many threads as the number of partitions for a perfect
295
- balance — more threads than partitions means that some threads will be idle
378
+ The number of consumer threads this input should spawn for fetching events and pushing them into the pipeline's queue.
379
+
380
+ When configured with `consumer_mode => consumer_group`, each partition in the topic can only be assigned to a single consumer at a time, so if the _total_ number of consumer threads across all consumers in the group exceeds the number of partitions, some of those threads will be idle.
381
+
382
+ This constraint doesn't apply when configured with `consumer_mode => share_group` which is not partition-oriented.
296
383
 
297
384
  [id="plugins-{type}s-{plugin}-decorate_events"]
298
385
  ===== `decorate_events`
@@ -327,7 +414,7 @@ A topic will be auto-created only if this configuration is set to `true` and a
327
414
  otherwise auto-topic creation is not permitted. 
328
415
 
329
416
  [id="plugins-{type}s-{plugin}-enable_auto_commit"]
330
- ===== `enable_auto_commit`
417
+ ===== `enable_auto_commit`
331
418
 
332
419
  * Value type is <<boolean,boolean>>
333
420
  * Default value is `true`
@@ -339,8 +426,14 @@ If true, periodically commit to Kafka the offsets of messages already returned b
339
426
  the consumer. If value is `false` however, the offset is committed every time the
340
427
  consumer writes data fetched from the topic to the in-memory or persistent queue.
341
428
 
429
+ [NOTE]
430
+ =====
431
+ This option is supported only with `consumer_mode => consumer_group`.
432
+ Share groups manage acknowledgement per record rather than via committed offsets — see <<plugins-{type}s-{plugin}-share_group_acknowledgement_on_error>>.
433
+ =====
434
+
342
435
  [id="plugins-{type}s-{plugin}-exclude_internal_topics"]
343
- ===== `exclude_internal_topics`
436
+ ===== `exclude_internal_topics`
344
437
 
345
438
  * Value type is <<string,string>>
346
439
  * There is no default value for this setting.
@@ -348,6 +441,11 @@ consumer writes data fetched from the topic to the in-memory or persistent queue
348
441
  Whether records from internal topics (such as offsets) should be exposed to the consumer.
349
442
  If set to true the only way to receive records from an internal topic is subscribing to it.
350
443
 
444
+ [NOTE]
445
+ =====
446
+ This option is supported only with `consumer_mode => consumer_group`.
447
+ =====
448
+
351
449
  [id="plugins-{type}s-{plugin}-fetch_max_bytes"]
352
450
  ===== `fetch_max_bytes`
353
451
 
@@ -414,6 +512,11 @@ You can set this value to use information such as a hostname, an IP, or anything
414
512
  NOTE: In cases when multiple threads are configured and `consumer_threads` is greater than one, a suffix is appended to
415
513
  the `group_instance_id` to avoid collisions.
416
514
 
515
+ [NOTE]
516
+ =====
517
+ This option is supported only with `consumer_mode => consumer_group`.
518
+ =====
519
+
417
520
  [id="plugins-{type}s-{plugin}-group_protocol"]
418
521
  ===== `group_protocol`
419
522
 
@@ -428,21 +531,31 @@ Specifies the consumer group rebalance protocol used by the Kafka client.
428
531
  When using `consumer`, the following settings **cannot be configured**:
429
532
  `partition_assignment_strategy`, `heartbeat_interval_ms`, and `session_timeout_ms`.
430
533
 
534
+ [NOTE]
535
+ =====
536
+ This option is supported only with `consumer_mode => consumer_group`.
537
+ =====
431
538
 
432
539
  [id="plugins-{type}s-{plugin}-heartbeat_interval_ms"]
433
- ===== `heartbeat_interval_ms`
540
+ ===== `heartbeat_interval_ms`
434
541
 
435
542
  * Value type is <<number,number>>
436
543
  * Default value is `3000` milliseconds (3 seconds).
437
544
 
438
- The expected time between heartbeats to the consumer coordinator. Heartbeats are used to ensure
545
+ The expected time between heartbeats to the consumer coordinator. Heartbeats are used to ensure
439
546
  that the consumer's session stays active and to facilitate rebalancing when new
440
547
  consumers join or leave the group. The value must be set lower than
441
548
  `session.timeout.ms`, but typically should be set no higher than 1/3 of that value.
442
549
  It can be adjusted even lower to control the expected time for normal rebalances.
443
550
 
551
+ [NOTE]
552
+ =====
553
+ This option is supported only with `consumer_mode => consumer_group`.
554
+ For `share_group` mode, the equivalent is the broker/group-level `share.heartbeat.interval.ms` setting.
555
+ =====
556
+
444
557
  [id="plugins-{type}s-{plugin}-isolation_level"]
445
- ===== `isolation_level`
558
+ ===== `isolation_level`
446
559
 
447
560
  * Value type is <<string,string>>
448
561
  * Default value is `"read_uncommitted"`
@@ -452,6 +565,12 @@ transactional messages which have been committed. If set to `read_uncommitted` (
452
565
  return all messages, even transactional messages which have been aborted. Non-transactional messages will be returned
453
566
  unconditionally in either mode.
454
567
 
568
+ [NOTE]
569
+ =====
570
+ This option is supported only with `consumer_mode => consumer_group`.
571
+ For `share_group` mode, the equivalent is the broker/group-level `share.isolation.level` setting.
572
+ =====
573
+
455
574
  [id="plugins-{type}s-{plugin}-jaas_path"]
456
575
  ===== `jaas_path`
457
576
 
@@ -548,6 +667,12 @@ partition ownership amongst consumer instances, supported options are:
548
667
  These map to Kafka's corresponding https://kafka.apache.org/{kafka_client_doc}/javadoc/org/apache/kafka/clients/consumer/ConsumerPartitionAssignor.html[`ConsumerPartitionAssignor`]
549
668
  implementations.
550
669
 
670
+ [NOTE]
671
+ =====
672
+ This option is supported only with `consumer_mode => consumer_group`.
673
+ Share groups do not assign partitions exclusively to consumers, so partition assignment strategies do not apply.
674
+ =====
675
+
551
676
  [id="plugins-{type}s-{plugin}-poll_timeout_ms"]
552
677
  ===== `poll_timeout_ms`
553
678
 
@@ -796,6 +921,11 @@ The schemas must follow a naming convention with the pattern <topic name>-value.
796
921
  Use either the Schema Registry config option or the
797
922
  <<plugins-{type}s-{plugin}-value_deserializer_class>> config option, but not both.
798
923
 
924
+ [NOTE]
925
+ =====
926
+ Schema Registry options (including `schema_registry_key`, `schema_registry_secret`, `schema_registry_proxy`, `schema_registry_validation`, and `schema_registry_ssl_*`) are supported only with `consumer_mode => consumer_group`.
927
+ =====
928
+
799
929
  [id="plugins-{type}s-{plugin}-schema_registry_validation"]
800
930
  ===== `schema_registry_validation`
801
931
 
@@ -825,7 +955,7 @@ Security protocol to use, which can be either of PLAINTEXT,SSL,SASL_PLAINTEXT,SA
825
955
  The size of the TCP send buffer (SO_SNDBUF) to use when sending data
826
956
 
827
957
  [id="plugins-{type}s-{plugin}-session_timeout_ms"]
828
- ===== `session_timeout_ms`
958
+ ===== `session_timeout_ms`
829
959
 
830
960
  * Value type is <<number,number>>
831
961
  * Default value is `10000` milliseconds (10 seconds).
@@ -833,6 +963,28 @@ The size of the TCP send buffer (SO_SNDBUF) to use when sending data
833
963
  The timeout after which, if the `poll_timeout_ms` is not invoked, the consumer is marked dead
834
964
  and a rebalance operation is triggered for the group identified by `group_id`
835
965
 
966
+ [NOTE]
967
+ =====
968
+ This option is supported only with `consumer_mode => consumer_group`.
969
+ For `share_group` mode, the equivalent is the broker/group-level `share.session.timeout.ms` setting.
970
+ =====
971
+
972
+ [id="plugins-{type}s-{plugin}-share_group_acknowledgement_on_error"]
973
+ ===== `share_group_acknowledgement_on_error`
974
+
975
+ * Value can be either of: `release`, `reject`
976
+ * Default value is `release`
977
+
978
+ Controls how a record is acknowledged when the event is either not accepted by the pipeline's queue or an error occurs before it is offered.
979
+
980
+ * `release` (default): The record is released back to the share group for redelivery to any available consumer. This strategy favors consistency over availability, which can allow a single malformed event to be retried indefinitely.
981
+ * `reject`: The record is permanently discarded and will not be redelivered. This strategy favors availability over consistency and risks data loss.
982
+
983
+ [NOTE]
984
+ =====
985
+ This option is supported only with `consumer_mode => share_group`.
986
+ =====
987
+
836
988
  [id="plugins-{type}s-{plugin}-ssl_endpoint_identification_algorithm"]
837
989
  ===== `ssl_endpoint_identification_algorithm`
838
990
 
@@ -45,8 +45,10 @@ require 'logstash/plugin_mixins/deprecation_logger_support'
45
45
  # physical machines. Messages in a topic will be distributed to all Logstash instances with
46
46
  # the same `group_id`.
47
47
  #
48
- # Ideally you should have as many threads as the number of partitions for a perfect balance --
49
- # more threads than partitions means that some threads will be idle
48
+ # In `consumer_group` mode, you should ideally have as many threads as the number of partitions
49
+ # for a perfect balance -- more threads than partitions means that some threads will be idle.
50
+ # This does not apply in `share_group` mode, where any thread can fetch from any partition, so
51
+ # the number of threads is not bounded by the partition count.
50
52
  #
51
53
  # For more information see http://kafka.apache.org/documentation.html#theconsumer
52
54
  #
@@ -100,8 +102,10 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
100
102
  # is to be able to track the source of requests beyond just ip/port by allowing
101
103
  # a logical application name to be included.
102
104
  config :client_id, :validate => :string, :default => "logstash"
103
- # Ideally you should have as many threads as the number of partitions for a perfect
104
- # balance — more threads than partitions means that some threads will be idle
105
+ # In `consumer_group` mode, you should ideally have as many threads as the number of partitions
106
+ # for a perfect balance — more threads than partitions means that some threads will be idle.
107
+ # This does not apply in `share_group` mode, where any thread can fetch from any partition, so
108
+ # the number of threads is not bounded by the partition count.
105
109
  config :consumer_threads, :validate => :number, :default => 1
106
110
  # If true, periodically commit to Kafka the offsets of messages already returned by the consumer.
107
111
  # This committed offset will be used when the process fails as the position from
@@ -281,6 +285,33 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
281
285
  # otherwise auto-topic creation is not permitted.
282
286
  config :auto_create_topics, :validate => :boolean, :default => true
283
287
 
288
+ # Sets the consumer mode for this input.
289
+ #
290
+ # * `consumer_group` (default): Standard Kafka consumer group mode. Partitions are assigned
291
+ # exclusively to consumers; each partition is consumed by exactly one thread in the group.
292
+ # This mode uses `KafkaConsumer` internally.
293
+ #
294
+ # * `share_group`: Kafka 4.0+ Share Group mode (KIP-932). Provides queue semantics where
295
+ # records from any partition can be delivered to any consumer in the share group, and each
296
+ # record is delivered to exactly one consumer. Consumers must acknowledge each record.
297
+ # This mode uses `KafkaShareConsumer` internally and requires Kafka brokers 4.0 or later
298
+ # with `group.share.enable=true` on the broker.
299
+ #
300
+ # The following options are not supported in `share_group` mode and raise a configuration
301
+ # error if set in the pipeline config: `group_protocol`, `group_instance_id`,
302
+ # `partition_assignment_strategy`, `heartbeat_interval_ms`, `session_timeout_ms`,
303
+ # `isolation_level`, `enable_auto_commit`, `auto_commit_interval_ms`, `auto_offset_reset`,
304
+ # `exclude_internal_topics`, `schema_registry_url`.
305
+ config :consumer_mode, :validate => ["consumer_group", "share_group"], :default => "consumer_group"
306
+
307
+ # Controls how records are acknowledged when `consumer_mode` is set to `share_group`.
308
+ # Successfully processed records are always acknowledged as ACCEPT.
309
+ # This setting controls what happens when an error occurs during record processing:
310
+ #
311
+ # * `release` (default): Re-queue the record for redelivery to any consumer in the share group.
312
+ # * `reject`: Permanently discard the record (it will not be redelivered).
313
+ config :share_group_acknowledgement_on_error, :validate => ["release", "reject"], :default => "release"
314
+
284
315
  config :decorate_events, :validate => %w(none basic extended false true), :default => "none"
285
316
 
286
317
  attr_reader :metadata_mode
@@ -294,15 +325,45 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
294
325
  super(params)
295
326
  end
296
327
 
328
+ SHARE_GROUP_INCOMPATIBLE_OPTIONS = %w[
329
+ group_protocol
330
+ group_instance_id
331
+ partition_assignment_strategy
332
+ heartbeat_interval_ms
333
+ session_timeout_ms
334
+ isolation_level
335
+ enable_auto_commit
336
+ auto_commit_interval_ms
337
+ auto_offset_reset
338
+ exclude_internal_topics
339
+ schema_registry_url
340
+ ].freeze
341
+
342
+ CONSUMER_GROUP_ONLY_OPTIONS = %w[
343
+ share_group_acknowledgement_on_error
344
+ ].freeze
345
+
297
346
  public
298
347
  def register
299
348
  @runner_threads = []
300
349
  @metadata_mode = extract_metadata_level(@decorate_events)
301
350
  reassign_dns_lookup
302
351
  @pattern ||= java.util.regex.Pattern.compile(@topics_pattern) unless @topics_pattern.nil?
303
- check_schema_registry_parameters
304
352
 
305
- set_group_protocol!
353
+ if @consumer_mode == "share_group"
354
+ validate_share_group_config!
355
+ ack_type_class = org.apache.kafka.clients.consumer.AcknowledgeType
356
+ @share_group_ack_error_type =
357
+ if @share_group_acknowledgement_on_error == "reject"
358
+ ack_type_class::REJECT
359
+ else
360
+ ack_type_class::RELEASE
361
+ end
362
+ else
363
+ validate_consumer_group_config!
364
+ check_schema_registry_parameters
365
+ set_group_protocol!
366
+ end
306
367
  end
307
368
 
308
369
  METADATA_NONE = Set[].freeze
@@ -329,20 +390,35 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
329
390
 
330
391
  public
331
392
  def run(logstash_queue)
332
- @runner_consumers = consumer_threads.times.map do |i|
333
- thread_group_instance_id = consumer_threads > 1 && group_instance_id ? "#{group_instance_id}-#{i}" : group_instance_id
334
- subscribe(create_consumer("#{client_id}-#{i}", thread_group_instance_id))
393
+ if @consumer_mode == "share_group"
394
+ @runner_consumers = consumer_threads.times.map do |i|
395
+ subscribe(create_share_consumer("#{client_id}-#{i}"))
396
+ end
397
+ @runner_threads = @runner_consumers.map.with_index do |consumer, i|
398
+ share_group_thread_runner(logstash_queue, consumer, "kafka-input-worker-#{client_id}-#{i}")
399
+ end
400
+ else
401
+ @runner_consumers = consumer_threads.times.map do |i|
402
+ thread_group_instance_id = consumer_threads > 1 && group_instance_id ? "#{group_instance_id}-#{i}" : group_instance_id
403
+ subscribe(create_consumer("#{client_id}-#{i}", thread_group_instance_id))
404
+ end
405
+ @runner_threads = @runner_consumers.map.with_index { |consumer, i| thread_runner(logstash_queue, consumer,
406
+ "kafka-input-worker-#{client_id}-#{i}") }
335
407
  end
336
- @runner_threads = @runner_consumers.map.with_index { |consumer, i| thread_runner(logstash_queue, consumer,
337
- "kafka-input-worker-#{client_id}-#{i}") }
338
408
  @runner_threads.each(&:start)
339
409
  @runner_threads.each(&:join)
340
410
  end # def run
341
411
 
342
412
  public
343
413
  def stop
344
- # if we have consumers, wake them up to unblock our runner threads
345
- @runner_consumers && @runner_consumers.each(&:wakeup)
414
+ # wake up consumers to unblock the poll loop in runner threads
415
+ @runner_consumers && @runner_consumers.each do |consumer|
416
+ begin
417
+ consumer.wakeup
418
+ rescue => e
419
+ logger.debug("Error waking up consumer on stop", :error => e.message)
420
+ end
421
+ end
346
422
  end
347
423
 
348
424
  public
@@ -440,46 +516,63 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
440
516
  end
441
517
 
442
518
  private
519
+ def build_common_props(client_id)
520
+ props = java.util.Properties.new
521
+ kafka = org.apache.kafka.clients.consumer.ConsumerConfig
522
+
523
+ props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers)
524
+ props.put(kafka::CLIENT_DNS_LOOKUP_CONFIG, client_dns_lookup)
525
+ props.put(kafka::CLIENT_ID_CONFIG, client_id)
526
+ props.put(kafka::CONNECTIONS_MAX_IDLE_MS_CONFIG, connections_max_idle_ms.to_s) unless connections_max_idle_ms.nil?
527
+ props.put(kafka::FETCH_MAX_BYTES_CONFIG, fetch_max_bytes.to_s) unless fetch_max_bytes.nil?
528
+ props.put(kafka::FETCH_MAX_WAIT_MS_CONFIG, fetch_max_wait_ms.to_s) unless fetch_max_wait_ms.nil?
529
+ props.put(kafka::FETCH_MIN_BYTES_CONFIG, fetch_min_bytes.to_s) unless fetch_min_bytes.nil?
530
+ props.put(kafka::GROUP_ID_CONFIG, group_id)
531
+ props.put(kafka::KEY_DESERIALIZER_CLASS_CONFIG, key_deserializer_class)
532
+ props.put(kafka::MAX_PARTITION_FETCH_BYTES_CONFIG, max_partition_fetch_bytes.to_s) unless max_partition_fetch_bytes.nil?
533
+ props.put(kafka::MAX_POLL_RECORDS_CONFIG, max_poll_records.to_s) unless max_poll_records.nil?
534
+ props.put(kafka::MAX_POLL_INTERVAL_MS_CONFIG, max_poll_interval_ms.to_s) unless max_poll_interval_ms.nil?
535
+ props.put(kafka::METADATA_MAX_AGE_CONFIG, metadata_max_age_ms.to_s) unless metadata_max_age_ms.nil?
536
+ props.put(kafka::RECEIVE_BUFFER_CONFIG, receive_buffer_bytes.to_s) unless receive_buffer_bytes.nil?
537
+ props.put(kafka::RECONNECT_BACKOFF_MS_CONFIG, reconnect_backoff_ms.to_s) unless reconnect_backoff_ms.nil?
538
+ props.put(kafka::RECONNECT_BACKOFF_MAX_MS_CONFIG, reconnect_backoff_max_ms.to_s) unless reconnect_backoff_max_ms.nil?
539
+ props.put(kafka::REQUEST_TIMEOUT_MS_CONFIG, request_timeout_ms.to_s) unless request_timeout_ms.nil?
540
+ props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s) unless retry_backoff_ms.nil?
541
+ props.put(kafka::SEND_BUFFER_CONFIG, send_buffer_bytes.to_s) unless send_buffer_bytes.nil?
542
+ props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, value_deserializer_class)
543
+ props.put(kafka::ALLOW_AUTO_CREATE_TOPICS_CONFIG, auto_create_topics) unless auto_create_topics.nil?
544
+ props.put(kafka::CHECK_CRCS_CONFIG, check_crcs.to_s) unless check_crcs.nil?
545
+ props.put(kafka::CLIENT_RACK_CONFIG, client_rack) unless client_rack.nil?
546
+
547
+ props.put("security.protocol", security_protocol) unless security_protocol.nil?
548
+ if security_protocol == "SSL"
549
+ set_trustore_keystore_config(props)
550
+ elsif security_protocol == "SASL_PLAINTEXT"
551
+ set_sasl_config(props)
552
+ elsif security_protocol == "SASL_SSL"
553
+ set_trustore_keystore_config(props)
554
+ set_sasl_config(props)
555
+ end
556
+
557
+ props
558
+ end
559
+
443
560
  def create_consumer(client_id, group_instance_id)
444
561
  begin
445
- props = java.util.Properties.new
562
+ props = build_common_props(client_id)
446
563
  kafka = org.apache.kafka.clients.consumer.ConsumerConfig
447
564
 
448
565
  props.put(kafka::AUTO_COMMIT_INTERVAL_MS_CONFIG, auto_commit_interval_ms.to_s) unless auto_commit_interval_ms.nil?
449
566
  props.put(kafka::AUTO_OFFSET_RESET_CONFIG, auto_offset_reset) unless auto_offset_reset.nil?
450
- props.put(kafka::ALLOW_AUTO_CREATE_TOPICS_CONFIG, auto_create_topics) unless auto_create_topics.nil?
451
- props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers)
452
- props.put(kafka::CHECK_CRCS_CONFIG, check_crcs.to_s) unless check_crcs.nil?
453
- props.put(kafka::CLIENT_DNS_LOOKUP_CONFIG, client_dns_lookup)
454
- props.put(kafka::CLIENT_ID_CONFIG, client_id)
455
- props.put(kafka::CONNECTIONS_MAX_IDLE_MS_CONFIG, connections_max_idle_ms.to_s) unless connections_max_idle_ms.nil?
456
567
  props.put(kafka::ENABLE_AUTO_COMMIT_CONFIG, enable_auto_commit.to_s)
457
568
  props.put(kafka::EXCLUDE_INTERNAL_TOPICS_CONFIG, exclude_internal_topics) unless exclude_internal_topics.nil?
458
- props.put(kafka::FETCH_MAX_BYTES_CONFIG, fetch_max_bytes.to_s) unless fetch_max_bytes.nil?
459
- props.put(kafka::FETCH_MAX_WAIT_MS_CONFIG, fetch_max_wait_ms.to_s) unless fetch_max_wait_ms.nil?
460
- props.put(kafka::FETCH_MIN_BYTES_CONFIG, fetch_min_bytes.to_s) unless fetch_min_bytes.nil?
461
- props.put(kafka::GROUP_ID_CONFIG, group_id)
462
569
  props.put(kafka::GROUP_INSTANCE_ID_CONFIG, group_instance_id) unless group_instance_id.nil?
463
570
  props.put(kafka::GROUP_PROTOCOL_CONFIG, group_protocol)
464
571
  props.put(kafka::HEARTBEAT_INTERVAL_MS_CONFIG, heartbeat_interval_ms.to_s) unless heartbeat_interval_ms.nil?
465
572
  props.put(kafka::ISOLATION_LEVEL_CONFIG, isolation_level)
466
- props.put(kafka::KEY_DESERIALIZER_CLASS_CONFIG, key_deserializer_class)
467
- props.put(kafka::MAX_PARTITION_FETCH_BYTES_CONFIG, max_partition_fetch_bytes.to_s) unless max_partition_fetch_bytes.nil?
468
- props.put(kafka::MAX_POLL_RECORDS_CONFIG, max_poll_records.to_s) unless max_poll_records.nil?
469
- props.put(kafka::MAX_POLL_INTERVAL_MS_CONFIG, max_poll_interval_ms.to_s) unless max_poll_interval_ms.nil?
470
- props.put(kafka::METADATA_MAX_AGE_CONFIG, metadata_max_age_ms.to_s) unless metadata_max_age_ms.nil?
471
573
  props.put(kafka::PARTITION_ASSIGNMENT_STRATEGY_CONFIG, partition_assignment_strategy_class) unless partition_assignment_strategy.nil?
472
- props.put(kafka::RECEIVE_BUFFER_CONFIG, receive_buffer_bytes.to_s) unless receive_buffer_bytes.nil?
473
- props.put(kafka::RECONNECT_BACKOFF_MS_CONFIG, reconnect_backoff_ms.to_s) unless reconnect_backoff_ms.nil?
474
- props.put(kafka::RECONNECT_BACKOFF_MAX_MS_CONFIG, reconnect_backoff_max_ms.to_s) unless reconnect_backoff_max_ms.nil?
475
- props.put(kafka::REQUEST_TIMEOUT_MS_CONFIG, request_timeout_ms.to_s) unless request_timeout_ms.nil?
476
- props.put(kafka::RETRY_BACKOFF_MS_CONFIG, retry_backoff_ms.to_s) unless retry_backoff_ms.nil?
477
- props.put(kafka::SEND_BUFFER_CONFIG, send_buffer_bytes.to_s) unless send_buffer_bytes.nil?
478
574
  props.put(kafka::SESSION_TIMEOUT_MS_CONFIG, session_timeout_ms.to_s) unless session_timeout_ms.nil?
479
- props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, value_deserializer_class)
480
- props.put(kafka::CLIENT_RACK_CONFIG, client_rack) unless client_rack.nil?
481
575
 
482
- props.put("security.protocol", security_protocol) unless security_protocol.nil?
483
576
  if schema_registry_url
484
577
  props.put(kafka::VALUE_DESERIALIZER_CLASS_CONFIG, Java::io.confluent.kafka.serializers.KafkaAvroDeserializer.java_class)
485
578
  serdes_config = Java::io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig
@@ -495,14 +588,6 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
495
588
  props.put(serdes_config::BASIC_AUTH_CREDENTIALS_SOURCE, 'URL')
496
589
  end
497
590
  end
498
- if security_protocol == "SSL"
499
- set_trustore_keystore_config(props)
500
- elsif security_protocol == "SASL_PLAINTEXT"
501
- set_sasl_config(props)
502
- elsif security_protocol == "SASL_SSL"
503
- set_trustore_keystore_config(props)
504
- set_sasl_config(props)
505
- end
506
591
  if schema_registry_ssl_truststore_location
507
592
  props.put('schema.registry.ssl.truststore.location', schema_registry_ssl_truststore_location)
508
593
  props.put('schema.registry.ssl.truststore.password', schema_registry_ssl_truststore_password.value)
@@ -565,4 +650,100 @@ class LogStash::Inputs::Kafka < LogStash::Inputs::Base
565
650
  !header.nil? && !header.value.nil? && !header.key.nil?
566
651
  end
567
652
 
653
+ def validate_share_group_config!
654
+ incompatible = original_params.keys & SHARE_GROUP_INCOMPATIBLE_OPTIONS
655
+ unless incompatible.empty?
656
+ raise LogStash::ConfigurationError,
657
+ "The following options are not compatible with share_group consumer_mode and must be removed: #{incompatible.join(', ')}"
658
+ end
659
+ end
660
+
661
+ def validate_consumer_group_config!
662
+ incompatible = original_params.keys & CONSUMER_GROUP_ONLY_OPTIONS
663
+ unless incompatible.empty?
664
+ raise LogStash::ConfigurationError,
665
+ "The following options require consumer_mode => 'share_group' and cannot be used with consumer_group mode: #{incompatible.join(', ')}"
666
+ end
667
+ end
668
+
669
+ def create_share_consumer(client_id)
670
+ begin
671
+ props = build_common_props(client_id)
672
+
673
+ # Share consumer defaults to implicit acknowledgement, which disallows explicit
674
+ # acknowledge() calls. The plugin acknowledges each record after processing, so
675
+ # explicit mode is required.
676
+ props.put("share.acknowledgement.mode", "explicit")
677
+
678
+ org.apache.kafka.clients.consumer.KafkaShareConsumer.new(props)
679
+ rescue => e
680
+ logger.error("Unable to create Kafka share consumer from given configuration",
681
+ :kafka_error_message => e,
682
+ :cause => e.respond_to?(:getCause) ? e.getCause() : nil)
683
+ raise e
684
+ end
685
+ end
686
+
687
+ def share_group_thread_runner(logstash_queue, consumer, name)
688
+ java.lang.Thread.new do
689
+ LogStash::Util::set_thread_name(name)
690
+ begin
691
+ codec_instance = @codec.clone
692
+ ack_accept = org.apache.kafka.clients.consumer.AcknowledgeType::ACCEPT
693
+ ack_release = org.apache.kafka.clients.consumer.AcknowledgeType::RELEASE
694
+ until stop?
695
+ records = do_poll(consumer)
696
+ unless records.empty?
697
+ records.each do |record|
698
+ if stop?
699
+ consumer.acknowledge(record, ack_release)
700
+ else
701
+ begin
702
+ handle_record(record, codec_instance, logstash_queue)
703
+ consumer.acknowledge(record, ack_accept)
704
+ rescue => e
705
+ logger.error("Error processing record in share group mode, acknowledging with #{@share_group_acknowledgement_on_error}",
706
+ :kafka_error_message => e,
707
+ :cause => e.respond_to?(:getCause) ? e.getCause() : nil)
708
+ consumer.acknowledge(record, @share_group_ack_error_type)
709
+ end
710
+ end
711
+ end
712
+ commit_share_acknowledgements(consumer)
713
+ end
714
+ end
715
+ ensure
716
+ consumer.close
717
+ end
718
+ end
719
+ end
720
+
721
+ def commit_share_acknowledgements(consumer)
722
+ begin
723
+ log_rejected_acknowledgements(consumer.commitSync)
724
+ rescue org.apache.kafka.common.errors.WakeupException => e
725
+ logger.debug("Wake up from share consumer commitSync", :kafka_error_message => e)
726
+ raise e unless stop?
727
+ rescue StandardError => e
728
+ logger.error("Unable to commit share group acknowledgements",
729
+ :kafka_error_message => e,
730
+ :cause => e.respond_to?(:getCause) ? e.getCause() : nil)
731
+ end
732
+ end
733
+
734
+ # Unlike `KafkaConsumer#commitSync`, which returns void and raises on failure,
735
+ # `ShareConsumer#commitSync` reports per-partition acknowledgement failures through its
736
+ # return value and only raises for unrecoverable errors.
737
+ def log_rejected_acknowledgements(results)
738
+ return if results.nil?
739
+
740
+ results.each do |topic_id_partition, maybe_error|
741
+ next unless maybe_error && maybe_error.isPresent
742
+ logger.warn("Share group acknowledgements were rejected, the affected records will be redelivered",
743
+ :topic => topic_id_partition.topic,
744
+ :partition => topic_id_partition.partition,
745
+ :kafka_error_message => maybe_error.get)
746
+ end
747
+ end
748
+
568
749
  end #class LogStash::Inputs::Kafka
@@ -14,7 +14,7 @@ require_jar('com.fasterxml.jackson.core', 'jackson-core', '2.21.7')
14
14
  require_jar('org.apache.kafka', 'kafka-clients', '4.2.0')
15
15
  require_jar('org.apache.httpcomponents.client5', 'httpclient5', '5.6.5')
16
16
  require_jar('com.github.luben', 'zstd-jni', '1.5.7-24')
17
- require_jar('at.yawk.lz4', 'lz4-java', '1.12.0')
17
+ require_jar('at.yawk.lz4', 'lz4-java', '1.12.1')
18
18
  require_jar('org.xerial.snappy', 'snappy-java', '1.1.10.12')
19
19
  require_jar('com.google.guava', 'guava', '33.7.2-jre')
20
20
  require_jar('com.fasterxml.jackson.core', 'jackson-annotations', '2.21')
@@ -130,6 +130,97 @@ describe "inputs/kafka", :integration => true do
130
130
  end
131
131
  end
132
132
 
133
+ context "#share-group" do
134
+ # Self-contained: the topic and the group's share.auto.offset.reset are set up here rather
135
+ # than in kafka_test_setup.sh, so these specs run against any broker with share groups
136
+ # available. Both names are unique per example, which keeps share-group state (which is
137
+ # persistent per group/topic) from leaking between examples or between runs.
138
+ let(:run_id) { rand(36**8).to_s(36) }
139
+ let(:share_group_topic) { "logstash_integration_share_group_topic_#{run_id}" }
140
+ let(:share_group_id) { "logstash_integration_share_group_#{run_id}" }
141
+ let(:share_group_partitions) { 3 }
142
+ let(:share_group_msg_count) { 30 }
143
+ let(:share_group_config) do
144
+ { 'topics' => [share_group_topic], 'group_id' => share_group_id,
145
+ 'consumer_mode' => 'share_group', 'consumer_threads' => 3,
146
+ 'codec' => 'plain', 'bootstrap_servers' => 'localhost:9092' }
147
+ end
148
+ let(:produced_messages) { share_group_msg_count.times.map { |i| "share-msg-#{run_id}-#{i}" } }
149
+
150
+ def with_admin_client
151
+ props = java.util.Properties.new
152
+ props.put(org.apache.kafka.clients.admin.AdminClientConfig::BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
153
+ admin = org.apache.kafka.clients.admin.AdminClient.create(props)
154
+ begin
155
+ yield admin
156
+ ensure
157
+ admin.close
158
+ end
159
+ end
160
+
161
+ # Share groups default to share.auto.offset.reset=latest. Pinning it to earliest lets the
162
+ # specs produce up front instead of racing the group to a stable epoch before producing.
163
+ def set_share_group_offset_reset(admin, group_id, value)
164
+ resource = org.apache.kafka.common.config.ConfigResource.new(
165
+ org.apache.kafka.common.config.ConfigResource::Type::GROUP, group_id)
166
+ ops = java.util.ArrayList.new
167
+ ops.add(org.apache.kafka.clients.admin.AlterConfigOp.new(
168
+ org.apache.kafka.clients.admin.ConfigEntry.new('share.auto.offset.reset', value),
169
+ org.apache.kafka.clients.admin.AlterConfigOp::OpType::SET))
170
+ changes = java.util.HashMap.new
171
+ changes.put(resource, ops)
172
+ admin.incrementalAlterConfigs(changes).all.get
173
+ end
174
+
175
+ before do
176
+ with_admin_client do |admin|
177
+ admin.createTopics([org.apache.kafka.clients.admin.NewTopic.new(
178
+ share_group_topic, share_group_partitions, 1.to_java(:short))]).all.get
179
+ set_share_group_offset_reset(admin, share_group_id, 'earliest')
180
+ end
181
+
182
+ # One producer for the whole batch; send_message opens and closes one per record.
183
+ props = java.util.Properties.new
184
+ kafka = org.apache.kafka.clients.producer.ProducerConfig
185
+ props.put(kafka::BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
186
+ props.put(kafka::KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
187
+ props.put(kafka::VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer")
188
+ producer = org.apache.kafka.clients.producer.KafkaProducer.new(props)
189
+ begin
190
+ produced_messages.each do |payload|
191
+ producer.send(org.apache.kafka.clients.producer.ProducerRecord.new(share_group_topic, payload))
192
+ end
193
+ producer.flush
194
+ ensure
195
+ producer.close
196
+ end
197
+ end
198
+
199
+ after do
200
+ with_admin_client { |admin| admin.deleteTopics([share_group_topic]).all.get } rescue nil
201
+ end
202
+
203
+ def collect_share_group_messages(config)
204
+ consume_messages(config, timeout: false, event_count: share_group_msg_count) do |queue, _input|
205
+ # Not `eq`: share groups are at-least-once, so assert on the distinct payloads
206
+ # rather than failing the wait outright if a record gets redelivered.
207
+ wait(timeout_seconds).for { queue.length }.to be >= share_group_msg_count
208
+ queue.length.times.map { queue.pop.get("message") }
209
+ end
210
+ end
211
+
212
+ it "delivers every produced record across the consumer threads" do
213
+ expect(collect_share_group_messages(share_group_config).uniq).to match_array(produced_messages)
214
+ end
215
+
216
+ it "is not bounded by the partition count" do
217
+ # More threads than partitions. In consumer_group mode the surplus threads would
218
+ # necessarily sit idle; a share group has no exclusive partition assignment.
219
+ config = share_group_config.merge('consumer_threads' => share_group_partitions + 2)
220
+ expect(collect_share_group_messages(config).uniq).to match_array(produced_messages)
221
+ end
222
+ end
223
+
133
224
  context "#kafka-decorate" do
134
225
  it "should show the right topic and group name in decorated kafka section" do
135
226
  start = LogStash::Timestamp.now.time.to_i
@@ -84,6 +84,11 @@ describe LogStash::Inputs::Kafka do
84
84
  expect { subject.register }.to_not raise_error
85
85
  end
86
86
 
87
+ it "raises ConfigurationError when share_group_acknowledgement_on_error is set in consumer_group mode" do
88
+ config['share_group_acknowledgement_on_error'] = 'reject'
89
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /share_group_acknowledgement_on_error/)
90
+ end
91
+
87
92
  context "when the deprecated `default` is specified" do
88
93
  let(:config) { common_config.merge('client_dns_lookup' => 'default') }
89
94
 
@@ -495,4 +500,219 @@ describe LogStash::Inputs::Kafka do
495
500
  expect( subject.enable_auto_commit ).to be true
496
501
  end
497
502
  end
503
+
504
+ context 'share group mode' do
505
+ let(:share_consumer_double) { double('kafka-share-consumer') }
506
+ let(:config) { common_config.merge('consumer_mode' => 'share_group') }
507
+
508
+ before do
509
+ allow(share_consumer_double).to receive(:subscribe)
510
+ allow(share_consumer_double).to receive(:wakeup)
511
+ allow(share_consumer_double).to receive(:close)
512
+ end
513
+
514
+ describe '#register' do
515
+ it "registers without error" do
516
+ expect { subject.register }.not_to raise_error
517
+ end
518
+
519
+ it "raises ConfigurationError when group_instance_id is set" do
520
+ config['group_instance_id'] = 'my-instance'
521
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /group_instance_id/)
522
+ end
523
+
524
+ it "raises ConfigurationError when partition_assignment_strategy is set" do
525
+ config['partition_assignment_strategy'] = 'sticky'
526
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /partition_assignment_strategy/)
527
+ end
528
+
529
+ it "raises ConfigurationError when schema_registry_url is set" do
530
+ config['schema_registry_url'] = 'http://localhost:8081'
531
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /schema_registry_url/)
532
+ end
533
+
534
+ %w[
535
+ enable_auto_commit
536
+ auto_commit_interval_ms
537
+ heartbeat_interval_ms
538
+ session_timeout_ms
539
+ isolation_level
540
+ auto_offset_reset
541
+ exclude_internal_topics
542
+ group_protocol
543
+ ].each do |opt|
544
+ it "raises ConfigurationError when #{opt} is set in the pipeline config" do
545
+ config[opt] = case opt
546
+ when 'enable_auto_commit', 'exclude_internal_topics' then 'true'
547
+ when 'auto_commit_interval_ms', 'heartbeat_interval_ms', 'session_timeout_ms' then '5000'
548
+ when 'isolation_level' then 'read_committed'
549
+ when 'auto_offset_reset' then 'earliest'
550
+ when 'group_protocol' then 'consumer'
551
+ end
552
+ expect { subject.register }.to raise_error(LogStash::ConfigurationError, /#{opt}/)
553
+ end
554
+ end
555
+
556
+ it "accepts check_crcs in share_group mode (supported by KafkaShareConsumer)" do
557
+ config['check_crcs'] = true
558
+ expect { subject.register }.not_to raise_error
559
+ end
560
+
561
+ it "accepts auto_create_topics in share_group mode (supported by KafkaShareConsumer)" do
562
+ config['auto_create_topics'] = true
563
+ expect { subject.register }.not_to raise_error
564
+ end
565
+ end
566
+
567
+ describe '#create_share_consumer' do
568
+ it "creates a KafkaShareConsumer with correct bootstrap_servers and group_id" do
569
+ expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
570
+ to receive(:new).with(hash_including(
571
+ 'bootstrap.servers' => 'localhost:9092',
572
+ 'group.id' => 'logstash'
573
+ )).and_return(share_consumer_double)
574
+
575
+ expect(subject.send(:create_share_consumer, 'test-share-client-0')).to be share_consumer_double
576
+ end
577
+
578
+ it "does not set enable.auto.commit, isolation.level or auto.offset.reset" do
579
+ expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
580
+ to receive(:new) do |props|
581
+ expect(props).not_to have_key('enable.auto.commit')
582
+ expect(props).not_to have_key('isolation.level')
583
+ expect(props).not_to have_key('auto.offset.reset')
584
+ share_consumer_double
585
+ end
586
+
587
+ subject.send(:create_share_consumer, 'test-share-client-0')
588
+ end
589
+
590
+ it "sets share.acknowledgement.mode to explicit" do
591
+ expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
592
+ to receive(:new).with(hash_including('share.acknowledgement.mode' => 'explicit')).
593
+ and_return(share_consumer_double)
594
+
595
+ subject.send(:create_share_consumer, 'test-share-client-0')
596
+ end
597
+
598
+ context 'with client_rack' do
599
+ let(:config) { super().merge('client_rack' => 'US-EAST-1') }
600
+
601
+ it "passes client.rack to the share consumer" do
602
+ expect(org.apache.kafka.clients.consumer.KafkaShareConsumer).
603
+ to receive(:new).with(hash_including('client.rack' => 'US-EAST-1')).
604
+ and_return(share_consumer_double)
605
+
606
+ subject.send(:create_share_consumer, 'test-share-client-0')
607
+ end
608
+ end
609
+ end
610
+
611
+ describe '#running in share_group mode' do
612
+ let(:q) { Queue.new }
613
+ let(:payload) do
614
+ 5.times.map { org.apache.kafka.clients.consumer.ConsumerRecord.new("logstash", 0, 0, "key", "value") }
615
+ end
616
+
617
+ before do
618
+ subject.register
619
+ expect(subject).to receive(:create_share_consumer).once.and_return(share_consumer_double)
620
+ polled = false
621
+ allow(share_consumer_double).to receive(:poll) do
622
+ if polled
623
+ []
624
+ else
625
+ polled = true
626
+ payload
627
+ end
628
+ end
629
+ allow(share_consumer_double).to receive(:acknowledge)
630
+ allow(share_consumer_double).to receive(:commitSync)
631
+ end
632
+
633
+ it "processes events and acknowledges each record" do
634
+ expect(share_consumer_double).to receive(:acknowledge).exactly(5).times
635
+ expect(share_consumer_double).to receive(:commitSync).at_least(:once)
636
+
637
+ t = Thread.new { sleep(1); subject.do_stop }
638
+ subject.run(q)
639
+ t.join
640
+
641
+ expect(q.size).to eq(5)
642
+ end
643
+
644
+ it "acknowledges with ACCEPT on successful record processing" do
645
+ ack_accept = org.apache.kafka.clients.consumer.AcknowledgeType::ACCEPT
646
+ expect(share_consumer_double).to receive(:acknowledge).with(anything, ack_accept).exactly(5).times
647
+ allow(share_consumer_double).to receive(:commitSync)
648
+
649
+ t = Thread.new { sleep(1); subject.do_stop }
650
+ subject.run(q)
651
+ t.join
652
+ end
653
+ end
654
+
655
+ describe '#commit_share_acknowledgements' do
656
+ # `ShareConsumer#commitSync` reports per-partition acknowledgement failures in its
657
+ # return value instead of raising, so they are only visible if the plugin inspects it.
658
+ let(:topic_id_partition) do
659
+ org.apache.kafka.common.TopicIdPartition.new(
660
+ org.apache.kafka.common.Uuid.randomUuid, 3, "logstash")
661
+ end
662
+ let(:rejected) do
663
+ java.util.Optional.of(
664
+ org.apache.kafka.common.errors.InvalidRecordStateException.new("acquisition lock expired"))
665
+ end
666
+
667
+ before { subject.register }
668
+
669
+ it "warns about acknowledgements the broker rejected" do
670
+ allow(share_consumer_double).to receive(:commitSync).and_return({ topic_id_partition => rejected })
671
+
672
+ expect(subject.logger).to receive(:warn).with(
673
+ /rejected/, hash_including(:topic => "logstash", :partition => 3))
674
+
675
+ subject.send(:commit_share_acknowledgements, share_consumer_double)
676
+ end
677
+
678
+ it "stays quiet when every acknowledgement succeeded" do
679
+ allow(share_consumer_double).to receive(:commitSync).
680
+ and_return({ topic_id_partition => java.util.Optional.empty })
681
+
682
+ expect(subject.logger).to_not receive(:warn)
683
+
684
+ subject.send(:commit_share_acknowledgements, share_consumer_double)
685
+ end
686
+
687
+ it "tolerates the empty map returned when there was nothing to acknowledge" do
688
+ allow(share_consumer_double).to receive(:commitSync).and_return(java.util.Collections.emptyMap)
689
+
690
+ expect(subject.logger).to_not receive(:warn)
691
+
692
+ expect { subject.send(:commit_share_acknowledgements, share_consumer_double) }.to_not raise_error
693
+ end
694
+ end
695
+
696
+ describe 'acknowledgement_on_error' do
697
+ before { subject.register }
698
+
699
+ context 'with release strategy (default)' do
700
+ it "uses RELEASE ack type on error" do
701
+ expect(subject.instance_variable_get(:@share_group_ack_error_type)).to eq(
702
+ org.apache.kafka.clients.consumer.AcknowledgeType::RELEASE
703
+ )
704
+ end
705
+ end
706
+
707
+ context 'with reject strategy' do
708
+ let(:config) { super().merge('share_group_acknowledgement_on_error' => 'reject') }
709
+
710
+ it "uses REJECT ack type on error" do
711
+ expect(subject.instance_variable_get(:@share_group_ack_error_type)).to eq(
712
+ org.apache.kafka.clients.consumer.AcknowledgeType::REJECT
713
+ )
714
+ end
715
+ end
716
+ end
717
+ end
498
718
  end
data/version CHANGED
@@ -1 +1 @@
1
- 12.1.11
1
+ 12.2.0
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-integration-kafka
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.1.11
4
+ version: 12.2.0
5
5
  platform: java
6
6
  authors:
7
7
  - Elastic
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: logstash-core-plugin-api
@@ -244,7 +244,7 @@ files:
244
244
  - spec/unit/inputs/avro_schema_fixture_payment.asvc
245
245
  - spec/unit/inputs/kafka_spec.rb
246
246
  - spec/unit/outputs/kafka_spec.rb
247
- - vendor/jar-dependencies/at/yawk/lz4/lz4-java/1.12.0/lz4-java-1.12.0.jar
247
+ - vendor/jar-dependencies/at/yawk/lz4/lz4-java/1.12.1/lz4-java-1.12.1.jar
248
248
  - vendor/jar-dependencies/com/eclipsesource/minimal-json/minimal-json/0.9.5/minimal-json-0.9.5.jar
249
249
  - vendor/jar-dependencies/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar
250
250
  - vendor/jar-dependencies/com/fasterxml/jackson/core/jackson-core/2.21.7/jackson-core-2.21.7.jar
@@ -303,7 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
303
303
  - !ruby/object:Gem::Version
304
304
  version: '0'
305
305
  requirements: []
306
- rubygems_version: 3.6.3
306
+ rubygems_version: 3.7.2
307
307
  specification_version: 4
308
308
  summary: Integration with Kafka - input and output plugins
309
309
  test_files: