Skip to content

Commit 18f3756

Browse files
filebeat kafka input: add group_instance_id for static group membership (#51772) (#51886)
* filebeat kafka input: add group_instance_id for static group membership The Kafka input now has a `group_instance_id` option wiring it to Consumer.Group.InstanceId (KIP-345): a member that rejoins within `session_timeout` keeps its partitions instead of rebalancing. Static membership needs Kafka `version` >= 2.3.0, so the input fails fast with a clear error when it is set on the default 2.1.0. Unset by default; existing configs are unaffected. Assisted-By: Claude Code (cherry picked from commit 6811ebc) Co-authored-by: Anderson Queiroz <anderson.queiroz@elastic.co>
1 parent dfd5ed5 commit 18f3756

7 files changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: enhancement
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Add `group_instance_id` (KIP-345 static group membership) to the Filebeat Kafka input.
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
description: >
19+
The Filebeat Kafka input now exposes `group_instance_id`, wired to Sarama's
20+
`Consumer.Group.InstanceId`. Setting a stable, unique id per consumer instance
21+
enables Kafka static group membership (KIP-345): a member that restarts and
22+
rejoins within `session_timeout` is recognized as the same member, avoiding the
23+
rebalance storms that dynamic membership causes during rolling restarts of
24+
multi-replica deployments. Requires `version` >= 2.3.0. Unset by default, so
25+
existing behavior is unchanged.
26+
27+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
28+
component: "filebeat"
29+
30+
# PR URL; optional; the PR number that added the changeset.
31+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
32+
# pr: https://github.com/elastic/beats/pull/0000
33+
34+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
35+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
36+
issue: https://github.com/elastic/beats/issues/51768

docs/reference/filebeat/filebeat-input-kafka.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,25 @@ stack: ga 9.3.8+, ga 9.4.4+, ga 9.5
128128
How often the consumer sends heartbeats to the broker. This must be lower than `session_timeout`, and is typically set to no more than a third of that value. Default is 3s.
129129

130130

131+
### `group_instance_id` [_group_instance_id]
132+
```{applies_to}
133+
stack: ga 9.3+
134+
```
135+
136+
A stable identifier that enables Kafka static group membership ([KIP-345](https://cwiki.apache.org/confluence/display/KAFKA/KIP-345%3A+Introduce+static+membership+protocol+to+reduce+consumer+rebalances)). When set, a consumer that restarts and rejoins the group within [`session_timeout`](#_session_timeout) is recognized as the same member and keeps its partition assignment, avoiding the rebalances that a rolling restart of multiple instances would otherwise trigger. Requires `version` to be at least `2.3.0`.
137+
138+
Each consumer instance sharing a `group_id` must use a **unique** `group_instance_id`. Two live members with the same value cause the broker to fence one of them. Set `group_instance_id` to a value that is unique to each consumer instance and stable across restarts. The option is unset by default, in which case the consumer uses dynamic membership.
139+
140+
```yaml
141+
- type: kafka
142+
hosts: ["kafka-broker:9092"]
143+
topics: ["my-topic"]
144+
group_id: "filebeat"
145+
version: "2.3.0"
146+
group_instance_id: "<unique-instance-id>"
147+
```
148+
149+
131150
### `max_wait_time` [_max_wait_time]
132151

133152
How long to wait for the minimum number of input bytes while reading. Default is 250ms.

filebeat/_meta/config/filebeat.inputs.reference.yml.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ filebeat.inputs:
703703
# How often the consumer sends heartbeats. Must be lower than session_timeout.
704704
#heartbeat_interval: 3s
705705
706+
# Static group membership id (KIP-345). Set a unique, stable value per instance
707+
# so a restart does not trigger a rebalance. Requires version >= 2.3.0.
708+
#group_instance_id: "<unique-instance-id>"
709+
706710
# How long to wait for the minimum number of input bytes while reading.
707711
#max_wait_time: 250ms
708712

filebeat/filebeat.reference.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,10 @@ filebeat.inputs:
11221122
# How often the consumer sends heartbeats. Must be lower than session_timeout.
11231123
#heartbeat_interval: 3s
11241124

1125+
# Static group membership id (KIP-345). Set a unique, stable value per instance
1126+
# so a restart does not trigger a rebalance. Requires version >= 2.3.0.
1127+
#group_instance_id: "<unique-instance-id>"
1128+
11251129
# How long to wait for the minimum number of input bytes while reading.
11261130
#max_wait_time: 250ms
11271131

filebeat/input/kafka/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type kafkaInputConfig struct {
3838
Hosts []string `config:"hosts" validate:"required"`
3939
Topics []string `config:"topics" validate:"required"`
4040
GroupID string `config:"group_id" validate:"required"`
41+
GroupInstanceID string `config:"group_instance_id"`
4142
ClientID string `config:"client_id"`
4243
Version kafka.Version `config:"version"`
4344
InitialOffset initialOffset `config:"initial_offset"`
@@ -180,6 +181,13 @@ func newSaramaConfig(config kafkaInputConfig, logger *logp.Logger) (*sarama.Conf
180181
k.Consumer.Group.Session.Timeout = config.SessionTimeout
181182
k.Consumer.Group.Heartbeat.Interval = config.HeartbeatInterval
182183

184+
if config.GroupInstanceID != "" {
185+
if !version.IsAtLeast(sarama.V2_3_0_0) {
186+
return nil, fmt.Errorf("group_instance_id requires 'version' >= 2.3.0 for static group membership (KIP-345); configured version is %q", config.Version)
187+
}
188+
k.Consumer.Group.InstanceId = config.GroupInstanceID
189+
}
190+
183191
k.Net.DialTimeout = config.Timeout
184192
k.Net.ReadTimeout = config.Timeout
185193
k.Net.WriteTimeout = config.Timeout

filebeat/input/kafka/config_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package kafka
1919

2020
import (
21+
"strings"
2122
"testing"
2223
"time"
2324

@@ -39,6 +40,8 @@ func TestNewSaramaConfigDefaults(t *testing.T) {
3940
assert.Equal(t, 30*time.Second, saramaConfig.Net.DialTimeout)
4041
assert.Equal(t, 30*time.Second, saramaConfig.Net.ReadTimeout)
4142
assert.Equal(t, 30*time.Second, saramaConfig.Net.WriteTimeout)
43+
assert.Empty(t, saramaConfig.Consumer.Group.InstanceId,
44+
"group_instance_id must be unset by default so consumers keep dynamic membership")
4245
}
4346

4447
// TestNewSaramaConfigTimeoutOverrides verifies that the session_timeout,
@@ -62,3 +65,57 @@ func TestNewSaramaConfigTimeoutOverrides(t *testing.T) {
6265
assert.Equal(t, 60*time.Second, saramaConfig.Net.WriteTimeout)
6366
assert.Equal(t, 15*time.Second, saramaConfig.Net.KeepAlive)
6467
}
68+
69+
// TestNewSaramaConfigGroupInstanceID verifies that group_instance_id is
70+
// propagated to sarama's Consumer.Group.InstanceId, enabling Kafka static
71+
// group membership (KIP-345), when a compatible protocol version is set.
72+
func TestNewSaramaConfigGroupInstanceID(t *testing.T) {
73+
config := defaultConfig()
74+
config.Version = "2.3.0"
75+
config.GroupInstanceID = "filebeat-pod-1"
76+
77+
saramaConfig, err := newSaramaConfig(config, logp.NewNopLogger())
78+
require.NoError(t, err)
79+
80+
assert.Equal(t, "filebeat-pod-1", saramaConfig.Consumer.Group.InstanceId,
81+
"group_instance_id must be propagated to sarama's Consumer.Group.InstanceId")
82+
}
83+
84+
// TestNewSaramaConfigGroupInstanceIDRequiresVersion verifies that setting
85+
// group_instance_id with a protocol version below 2.3.0 (including the 2.1.0
86+
// default) fails early with a clear, Filebeat-oriented error rather than
87+
// sarama's opaque "need Version >= 2.3" message.
88+
func TestNewSaramaConfigGroupInstanceIDRequiresVersion(t *testing.T) {
89+
config := defaultConfig() // Version defaults to 2.1.0
90+
config.GroupInstanceID = "filebeat-pod-1"
91+
92+
_, err := newSaramaConfig(config, logp.NewNopLogger())
93+
require.Error(t, err, "group_instance_id below version 2.3.0 must be rejected")
94+
assert.ErrorContains(t, err, "group_instance_id requires 'version' >= 2.3.0",
95+
"error must carry the stable, searchable message naming the option and required version")
96+
}
97+
98+
// TestNewSaramaConfigGroupInstanceIDInvalid verifies that malformed
99+
// group_instance_id values are rejected by sarama's own validation (length,
100+
// reserved names, and the allowed character set) even when the version is
101+
// compatible.
102+
func TestNewSaramaConfigGroupInstanceIDInvalid(t *testing.T) {
103+
tests := map[string]string{
104+
"dot": ".",
105+
"dot-dot": "..",
106+
"illegal char": "has space",
107+
"too long (250)": strings.Repeat("a", 250),
108+
}
109+
110+
for name, id := range tests {
111+
t.Run(name, func(t *testing.T) {
112+
config := defaultConfig()
113+
config.Version = "2.3.0"
114+
config.GroupInstanceID = id
115+
116+
_, err := newSaramaConfig(config, logp.NewNopLogger())
117+
assert.Error(t, err,
118+
"invalid group_instance_id %q must be rejected", id)
119+
})
120+
}
121+
}

x-pack/filebeat/filebeat.reference.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2822,6 +2822,10 @@ filebeat.inputs:
28222822
# How often the consumer sends heartbeats. Must be lower than session_timeout.
28232823
#heartbeat_interval: 3s
28242824

2825+
# Static group membership id (KIP-345). Set a unique, stable value per instance
2826+
# so a restart does not trigger a rebalance. Requires version >= 2.3.0.
2827+
#group_instance_id: "<unique-instance-id>"
2828+
28252829
# How long to wait for the minimum number of input bytes while reading.
28262830
#max_wait_time: 250ms
28272831

0 commit comments

Comments
 (0)