Skip to content

Commit 2cbd09e

Browse files
authored
Implement regex for consumer_groups (#14382)
* Implement regex for consumer_groups * Add validated files * Match original PR * Fix tab
1 parent 90da5f6 commit 2cbd09e

File tree

8 files changed

+374
-50
lines changed

8 files changed

+374
-50
lines changed

kafka_consumer/assets/configuration/spec.yaml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,28 @@ files:
6262
- name: consumer_groups
6363
description: |
6464
Each level is optional. Any empty values are fetched from the Kafka cluster.
65-
You can have empty partitions (example: <CONSUMER_NAME_2>), topics (example: <CONSUMER_NAME_3>),
66-
and even consumer_groups. If you omit consumer_groups, you must set `monitor_unlisted_consumer_groups` to true.
65+
You can have empty partitions (example: <CONSUMER_NAME_2>), topics
66+
(example: <CONSUMER_NAME_3>),
67+
and even consumer_groups. If you do not specify `consumer_groups`,
68+
you must set `monitor_unlisted_consumer_groups` to true.
69+
70+
If both `consumer_groups` and `monitor_unlisted_consumer_groups` are used,
71+
then `consumer_groups` is ignored.
72+
value:
73+
type: object
74+
example:
75+
<CONSUMER_NAME_1>:
76+
<TOPIC_NAME_1>: [0, 1, 4, 12]
77+
<CONSUMER_NAME_2>:
78+
<TOPIC_NAME_2>: []
79+
<CONSUMER_NAME_3>: {}
80+
- name: consumer_groups_regex
81+
description: |
82+
Set consumer groups and topics as regex strings.
83+
If both `consumer_groups_regex` and `consumer_groups` are filled out,
84+
both configuration options will be used.
85+
If `monitor_unlisted_consumer_groups` is enabled, then
86+
`consumer_groups_regex` is ignored.
6787
value:
6888
type: object
6989
example:
@@ -74,8 +94,8 @@ files:
7494
<CONSUMER_NAME_3>: {}
7595
- name: monitor_unlisted_consumer_groups
7696
description: |
77-
Setting monitor_unlisted_consumer_groups to `true` tells the check to discover all consumer groups
78-
and fetch all their known offsets. If this is not set to true, you must specify consumer_groups.
97+
Setting `monitor_unlisted_consumer_groups` to `true` tells the check to discover all consumer groups
98+
and fetch all their known offsets. If this is not set to true, you must specify `consumer_groups`.
7999
80100
WARNING: This feature requires that your Kafka brokers be version >= 0.10.2. It is impossible to
81101
support this feature on older brokers because they do not provide a way to determine the mapping

kafka_consumer/datadog_checks/kafka_consumer/client.py

Lines changed: 92 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -120,30 +120,7 @@ def get_consumer_offsets(self):
120120
# {(consumer_group, topic, partition): offset}
121121
consumer_offsets = {}
122122

123-
if self.config._monitor_unlisted_consumer_groups:
124-
# Get all consumer groups
125-
consumer_groups = []
126-
consumer_groups_future = self.kafka_client.list_consumer_groups()
127-
self.log.debug('MONITOR UNLISTED CG FUTURES: %s', consumer_groups_future)
128-
try:
129-
list_consumer_groups_result = consumer_groups_future.result()
130-
self.log.debug('MONITOR UNLISTED FUTURES RESULT: %s', list_consumer_groups_result)
131-
132-
consumer_groups.extend(
133-
valid_consumer_group.group_id for valid_consumer_group in list_consumer_groups_result.valid
134-
)
135-
except Exception as e:
136-
self.log.error("Failed to collect consumer offsets %s", e)
137-
138-
elif self.config._consumer_groups:
139-
self._validate_consumer_groups()
140-
consumer_groups = self.config._consumer_groups
141-
142-
else:
143-
raise ConfigurationError(
144-
"Cannot fetch consumer offsets because no consumer_groups are specified and "
145-
"monitor_unlisted_consumer_groups is %s." % self.config._monitor_unlisted_consumer_groups
146-
)
123+
consumer_groups = self._get_consumer_groups()
147124

148125
for future in self._get_consumer_offset_futures(consumer_groups):
149126
try:
@@ -178,6 +155,31 @@ def get_consumer_offsets(self):
178155

179156
return consumer_offsets
180157

158+
def _get_consumer_groups(self):
159+
if self.config._monitor_unlisted_consumer_groups or self.config._consumer_groups_regex:
160+
# Get all consumer groups
161+
consumer_groups = []
162+
consumer_groups_future = self.kafka_client.list_consumer_groups()
163+
self.log.debug('MONITOR UNLISTED CG FUTURES: %s', consumer_groups_future)
164+
try:
165+
list_consumer_groups_result = consumer_groups_future.result()
166+
self.log.debug('MONITOR UNLISTED FUTURES RESULT: %s', list_consumer_groups_result)
167+
168+
consumer_groups.extend(
169+
valid_consumer_group.group_id for valid_consumer_group in list_consumer_groups_result.valid
170+
)
171+
except Exception as e:
172+
self.log.error("Failed to collect consumer groups: %s", e)
173+
return consumer_groups
174+
elif self.config._consumer_groups:
175+
self._validate_consumer_groups()
176+
return self.config._consumer_groups
177+
else:
178+
raise ConfigurationError(
179+
"Cannot fetch consumer offsets because no consumer_groups are specified and "
180+
"monitor_unlisted_consumer_groups is %s." % self.config._monitor_unlisted_consumer_groups
181+
)
182+
181183
def _get_consumer_offset_futures(self, consumer_groups):
182184
topics = self.kafka_client.list_topics(timeout=self.config._request_timeout)
183185
# {(consumer_group, topic, partition): offset}
@@ -221,24 +223,77 @@ def _get_topic_partitions(self, topics, consumer_group):
221223

222224
partitions = list(topics.topics[topic].partitions.keys())
223225

224-
for partition in partitions:
225-
# Get all topic-partition combinations allowed based on config
226-
# if topics is None => collect all topics and partitions for the consumer group
227-
# if partitions is None => collect all partitions from the consumer group's topic
228-
if not self.config._monitor_unlisted_consumer_groups and self.config._consumer_groups.get(
229-
consumer_group
226+
if self.config._monitor_unlisted_consumer_groups:
227+
for partition in partitions:
228+
topic_partition = TopicPartition(topic, partition)
229+
self.log.debug("TOPIC PARTITION: %s", topic_partition)
230+
yield topic_partition
231+
232+
elif self.config._consumer_groups_regex:
233+
for filtered_topic_partition in self._get_regex_filtered_topic_partitions(
234+
consumer_group, topic, partitions
230235
):
231-
if (
232-
self.config._consumer_groups[consumer_group]
233-
and topic not in self.config._consumer_groups[consumer_group]
234-
):
236+
topic_partition = TopicPartition(filtered_topic_partition[0], filtered_topic_partition[1])
237+
self.log.debug("TOPIC PARTITION: %s", topic_partition)
238+
yield topic_partition
239+
240+
if self.config._consumer_groups:
241+
for partition in partitions:
242+
# Get all topic-partition combinations allowed based on config
243+
# if topics is None => collect all topics and partitions for the consumer group
244+
# if partitions is None => collect all partitions from the consumer group's topic
245+
if self.config._consumer_groups.get(consumer_group):
246+
if (
247+
self.config._consumer_groups[consumer_group]
248+
and topic not in self.config._consumer_groups[consumer_group]
249+
):
250+
self.log.debug(
251+
"Partition %s skipped because the topic %s is not in the consumer_group.",
252+
partition,
253+
topic,
254+
)
255+
continue
256+
if (
257+
self.config._consumer_groups[consumer_group].get(topic)
258+
and partition not in self.config._consumer_groups[consumer_group][topic]
259+
):
260+
self.log.debug(
261+
"Partition %s skipped because it is not defined in the consumer group for the topic %s",
262+
partition,
263+
topic,
264+
)
265+
continue
266+
267+
topic_partition = TopicPartition(topic, partition)
268+
self.log.debug("TOPIC PARTITION: %s", topic_partition)
269+
yield topic_partition
270+
271+
def _get_regex_filtered_topic_partitions(self, consumer_group, topic, partitions):
272+
for partition in partitions:
273+
# Do a regex filtering here for consumer groups
274+
for consumer_group_compiled_regex in self.config._consumer_groups_compiled_regex:
275+
if not consumer_group_compiled_regex.match(consumer_group):
276+
return
277+
278+
consumer_group_topics_regex = self.config._consumer_groups_compiled_regex.get(
279+
consumer_group_compiled_regex
280+
)
281+
282+
# If topics is empty, return all combinations of topic and partition
283+
if not consumer_group_topics_regex:
284+
yield (topic, partition)
285+
286+
# Do a regex filtering here for topics
287+
for topic_regex in consumer_group_topics_regex:
288+
if not topic_regex.match(topic):
235289
self.log.debug(
236290
"Partition %s skipped because the topic %s is not in the consumer_group.", partition, topic
237291
)
238292
continue
293+
239294
if (
240-
self.config._consumer_groups[consumer_group].get(topic)
241-
and partition not in self.config._consumer_groups[consumer_group][topic]
295+
consumer_group_topics_regex.get(topic_regex)
296+
and partition not in consumer_group_topics_regex[topic_regex]
242297
):
243298
self.log.debug(
244299
"Partition %s skipped because it is not defined in the consumer group for the topic %s",
@@ -247,6 +302,4 @@ def _get_topic_partitions(self, topics, consumer_group):
247302
)
248303
continue
249304

250-
self.log.debug("TOPIC PARTITION: %s", TopicPartition(topic, partition))
251-
252-
yield TopicPartition(topic, partition)
305+
yield (topic, partition)

kafka_consumer/datadog_checks/kafka_consumer/config.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
import os
5+
import re
56

67
from datadog_checks.base import ConfigurationError, is_affirmative
78
from datadog_checks.kafka_consumer.constants import CONTEXT_UPPER_BOUND, DEFAULT_KAFKA_TIMEOUT
89

910

1011
class KafkaConfig:
11-
def __init__(self, init_config, instance) -> None:
12+
def __init__(self, init_config, instance, log) -> None:
1213
self.instance = instance
1314
self.init_config = init_config
1415
self._context_limit = int(init_config.get('max_partition_contexts', CONTEXT_UPPER_BOUND))
16+
self.log = log
1517
self._custom_tags = instance.get('tags', [])
1618
self._monitor_unlisted_consumer_groups = is_affirmative(instance.get('monitor_unlisted_consumer_groups', False))
1719
self._monitor_all_broker_highwatermarks = is_affirmative(
1820
instance.get('monitor_all_broker_highwatermarks', False)
1921
)
2022
self._consumer_groups = instance.get('consumer_groups', {})
23+
self._consumer_groups_regex = instance.get('consumer_groups_regex', {})
2124

22-
self._kafka_connect_str = instance.get('kafka_connect_str')
25+
self._consumer_groups_compiled_regex = self._compile_regex(self._consumer_groups_regex)
2326

27+
self._kafka_connect_str = instance.get('kafka_connect_str')
2428
self._kafka_version = instance.get('kafka_client_api_version')
2529
if isinstance(self._kafka_version, str):
2630
self._kafka_version = tuple(map(int, self._kafka_version.split(".")))
@@ -72,3 +76,31 @@ def validate_config(self):
7276

7377
elif self._sasl_oauth_token_provider.get("client_secret") is None:
7478
raise ConfigurationError("The `client_secret` setting of `auth_token` reader is required")
79+
80+
# If `monitor_unlisted_consumer_groups` is set to true and
81+
# using `consumer_groups`, we prioritize `monitor_unlisted_consumer_groups`
82+
if self._monitor_unlisted_consumer_groups and (self._consumer_groups or self._consumer_groups_regex):
83+
self.log.warning(
84+
"Using both monitor_unlisted_consumer_groups and consumer_groups or consumer_groups_regex, "
85+
"so all consumer groups will be collected."
86+
)
87+
88+
if self._consumer_groups and self._consumer_groups_regex:
89+
self.log.warning("Using consumer_groups and consumer_groups_regex, will combine the two config options.")
90+
91+
def _compile_regex(self, consumer_groups_regex):
92+
patterns = {}
93+
94+
for consumer_group_regex in consumer_groups_regex:
95+
consumer_group_pattern = re.compile(consumer_group_regex)
96+
patterns[consumer_group_pattern] = {}
97+
98+
topics_regex = consumer_groups_regex.get(consumer_group_regex)
99+
100+
for topic_regex in topics_regex:
101+
topic_pattern = re.compile(topic_regex)
102+
103+
partitions = self._consumer_groups_regex[consumer_group_regex][topic_regex]
104+
patterns[consumer_group_pattern].update({topic_pattern: partitions})
105+
106+
return patterns

kafka_consumer/datadog_checks/kafka_consumer/config_models/defaults.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def instance_consumer_groups(field, value):
2222
return get_default_field_value(field, value)
2323

2424

25+
def instance_consumer_groups_regex(field, value):
26+
return get_default_field_value(field, value)
27+
28+
2529
def instance_disable_generic_tags(field, value):
2630
return False
2731

kafka_consumer/datadog_checks/kafka_consumer/config_models/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Config:
4141
allow_mutation = False
4242

4343
consumer_groups: Optional[Mapping[str, Any]]
44+
consumer_groups_regex: Optional[Mapping[str, Any]]
4445
disable_generic_tags: Optional[bool]
4546
empty_default_hostname: Optional[bool]
4647
kafka_client_api_version: Optional[str]

kafka_consumer/datadog_checks/kafka_consumer/data/conf.yaml.example

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,13 @@ instances:
5656

5757
## @param consumer_groups - mapping - optional
5858
## Each level is optional. Any empty values are fetched from the Kafka cluster.
59-
## You can have empty partitions (example: <CONSUMER_NAME_2>), topics (example: <CONSUMER_NAME_3>),
60-
## and even consumer_groups. If you omit consumer_groups, you must set `monitor_unlisted_consumer_groups` to true.
59+
## You can have empty partitions (example: <CONSUMER_NAME_2>), topics
60+
## (example: <CONSUMER_NAME_3>),
61+
## and even consumer_groups. If you do not specify `consumer_groups`,
62+
## you must set `monitor_unlisted_consumer_groups` to true.
63+
##
64+
## If both `consumer_groups` and `monitor_unlisted_consumer_groups` are used,
65+
## then `consumer_groups` is ignored.
6166
#
6267
# consumer_groups:
6368
# <CONSUMER_NAME_1>:
@@ -70,9 +75,27 @@ instances:
7075
# <TOPIC_NAME_2>: []
7176
# <CONSUMER_NAME_3>: {}
7277

78+
## @param consumer_groups_regex - mapping - optional
79+
## Set consumer groups and topics as regex strings.
80+
## If both `consumer_groups_regex` and `consumer_groups` are filled out,
81+
## both configuration options will be used.
82+
## If `monitor_unlisted_consumer_groups` is enabled, then
83+
## `consumer_groups_regex` is ignored.
84+
#
85+
# consumer_groups_regex:
86+
# <CONSUMER_NAME_1>:
87+
# <TOPIC_NAME_1>:
88+
# - 0
89+
# - 1
90+
# - 4
91+
# - 12
92+
# <CONSUMER_NAME_2>:
93+
# <TOPIC_NAME_2>: []
94+
# <CONSUMER_NAME_3>: {}
95+
7396
## @param monitor_unlisted_consumer_groups - boolean - optional - default: false
74-
## Setting monitor_unlisted_consumer_groups to `true` tells the check to discover all consumer groups
75-
## and fetch all their known offsets. If this is not set to true, you must specify consumer_groups.
97+
## Setting `monitor_unlisted_consumer_groups` to `true` tells the check to discover all consumer groups
98+
## and fetch all their known offsets. If this is not set to true, you must specify `consumer_groups`.
7699
##
77100
## WARNING: This feature requires that your Kafka brokers be version >= 0.10.2. It is impossible to
78101
## support this feature on older brokers because they do not provide a way to determine the mapping

kafka_consumer/datadog_checks/kafka_consumer/kafka_consumer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class KafkaCheck(AgentCheck):
2323

2424
def __init__(self, name, init_config, instances):
2525
super(KafkaCheck, self).__init__(name, init_config, instances)
26-
self.config = KafkaConfig(self.init_config, self.instance)
26+
self.config = KafkaConfig(self.init_config, self.instance, self.log)
2727
self._context_limit = self.config._context_limit
2828
self.client = KafkaClient(self.config, self.get_tls_context(), self.log)
2929
self.check_initializations.append(self.config.validate_config)

0 commit comments

Comments
 (0)