BAEL-5170: Expose Kafka Consumer Metrics via Actuator Endpoint (#13723)

Co-authored-by: Tapan Avasthi <tavasthi@Tapans-MacBook-Air.local>
This commit is contained in:
Tapan Avasthi 2023-03-29 04:15:00 +05:30 committed by GitHub
parent 0895faa78c
commit 4050f47183
8 changed files with 92 additions and 59 deletions

View File

@ -23,6 +23,16 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.10.5</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.kafka</groupId> <groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId> <artifactId>spring-kafka</artifactId>

View File

@ -10,6 +10,7 @@ public class LagAnalyzerApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(LagAnalyzerApplication.class, args); SpringApplication.run(LagAnalyzerApplication.class, args);
while (true) ; while (true)
;
} }
} }

View File

@ -1,6 +1,7 @@
package com.baeldung.monitoring.service; package com.baeldung.monitoring.service;
import com.baeldung.monitoring.util.MonitoringUtil; import com.baeldung.monitoring.util.MonitoringUtil;
import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult; import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult;
@ -27,36 +28,38 @@ public class LagAnalyzerService {
private final KafkaConsumer<String, String> consumer; private final KafkaConsumer<String, String> consumer;
@Autowired @Autowired
public LagAnalyzerService( public LagAnalyzerService(@Value("${monitor.kafka.bootstrap.config}") String bootstrapServerConfig) {
@Value("${monitor.kafka.bootstrap.config}") String bootstrapServerConfig) {
adminClient = getAdminClient(bootstrapServerConfig); adminClient = getAdminClient(bootstrapServerConfig);
consumer = getKafkaConsumer(bootstrapServerConfig); consumer = getKafkaConsumer(bootstrapServerConfig);
} }
public Map<TopicPartition, Long> analyzeLag( public Map<TopicPartition, Long> analyzeLag(String groupId)
String groupId) throws ExecutionException, InterruptedException {
throws ExecutionException, InterruptedException {
Map<TopicPartition, Long> consumerGrpOffsets = getConsumerGrpOffsets(groupId); Map<TopicPartition, Long> consumerGrpOffsets = getConsumerGrpOffsets(groupId);
Map<TopicPartition, Long> producerOffsets = getProducerOffsets(consumerGrpOffsets); Map<TopicPartition, Long> producerOffsets = getProducerOffsets(consumerGrpOffsets);
Map<TopicPartition, Long> lags = computeLags(consumerGrpOffsets, producerOffsets); Map<TopicPartition, Long> lags = computeLags(consumerGrpOffsets, producerOffsets);
for (Map.Entry<TopicPartition, Long> lagEntry : lags.entrySet()) { for (Map.Entry<TopicPartition, Long> lagEntry : lags.entrySet()) {
String topic = lagEntry.getKey().topic(); String topic = lagEntry.getKey()
int partition = lagEntry.getKey().partition(); .topic();
int partition = lagEntry.getKey()
.partition();
Long lag = lagEntry.getValue(); Long lag = lagEntry.getValue();
LOGGER.info("Time={} | Lag for topic = {}, partition = {} is {}", LOGGER.info("Time={} | Lag for topic = {}, partition = {}, groupId = {} is {}",
MonitoringUtil.time(), MonitoringUtil.time(),
topic, topic,
partition, partition,
lag); groupId,
lag);
} }
return lags; return lags;
} }
public Map<TopicPartition, Long> getConsumerGrpOffsets(String groupId) public Map<TopicPartition, Long> getConsumerGrpOffsets(String groupId)
throws ExecutionException, InterruptedException { throws ExecutionException, InterruptedException {
ListConsumerGroupOffsetsResult info = adminClient.listConsumerGroupOffsets(groupId); ListConsumerGroupOffsetsResult info = adminClient.listConsumerGroupOffsets(groupId);
Map<TopicPartition, OffsetAndMetadata> metadataMap Map<TopicPartition, OffsetAndMetadata> metadataMap = info
= info.partitionsToOffsetAndMetadata().get(); .partitionsToOffsetAndMetadata()
.get();
Map<TopicPartition, Long> groupOffset = new HashMap<>(); Map<TopicPartition, Long> groupOffset = new HashMap<>();
for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : metadataMap.entrySet()) { for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : metadataMap.entrySet()) {
TopicPartition key = entry.getKey(); TopicPartition key = entry.getKey();
@ -66,8 +69,7 @@ public class LagAnalyzerService {
return groupOffset; return groupOffset;
} }
private Map<TopicPartition, Long> getProducerOffsets( private Map<TopicPartition, Long> getProducerOffsets(Map<TopicPartition, Long> consumerGrpOffset) {
Map<TopicPartition, Long> consumerGrpOffset) {
List<TopicPartition> topicPartitions = new LinkedList<>(); List<TopicPartition> topicPartitions = new LinkedList<>();
for (Map.Entry<TopicPartition, Long> entry : consumerGrpOffset.entrySet()) { for (Map.Entry<TopicPartition, Long> entry : consumerGrpOffset.entrySet()) {
TopicPartition key = entry.getKey(); TopicPartition key = entry.getKey();
@ -77,9 +79,9 @@ public class LagAnalyzerService {
} }
public Map<TopicPartition, Long> computeLags( public Map<TopicPartition, Long> computeLags(
Map<TopicPartition, Long> consumerGrpOffsets, Map<TopicPartition, Long> consumerGrpOffsets,
Map<TopicPartition, Long> producerOffsets) { Map<TopicPartition, Long> producerOffsets) {
Map<TopicPartition, Long> lags = new HashMap<>(); Map<TopicPartition, Long> lags = new HashMap<>();
for (Map.Entry<TopicPartition, Long> entry : consumerGrpOffsets.entrySet()) { for (Map.Entry<TopicPartition, Long> entry : consumerGrpOffsets.entrySet()) {
Long producerOffset = producerOffsets.get(entry.getKey()); Long producerOffset = producerOffsets.get(entry.getKey());
Long consumerOffset = consumerGrpOffsets.get(entry.getKey()); Long consumerOffset = consumerGrpOffsets.get(entry.getKey());
@ -91,15 +93,24 @@ public class LagAnalyzerService {
private AdminClient getAdminClient(String bootstrapServerConfig) { private AdminClient getAdminClient(String bootstrapServerConfig) {
Properties config = new Properties(); Properties config = new Properties();
config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServerConfig); config.put(
AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
bootstrapServerConfig);
return AdminClient.create(config); return AdminClient.create(config);
} }
private KafkaConsumer<String, String> getKafkaConsumer(String bootstrapServerConfig) { private KafkaConsumer<String, String> getKafkaConsumer(
String bootstrapServerConfig) {
Properties properties = new Properties(); Properties properties = new Properties();
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServerConfig); properties.setProperty(
properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); bootstrapServerConfig);
properties.setProperty(
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
properties.setProperty(
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
return new KafkaConsumer<>(properties); return new KafkaConsumer<>(properties);
} }
} }

View File

@ -15,8 +15,8 @@ public class LiveLagAnalyzerService {
@Autowired @Autowired
public LiveLagAnalyzerService( public LiveLagAnalyzerService(
LagAnalyzerService lagAnalyzerService, LagAnalyzerService lagAnalyzerService,
@Value(value = "${monitor.kafka.consumer.groupid}") String groupId) { @Value(value = "${monitor.kafka.consumer.groupid}") String groupId) {
this.lagAnalyzerService = lagAnalyzerService; this.lagAnalyzerService = lagAnalyzerService;
this.groupId = groupId; this.groupId = groupId;
} }

View File

@ -7,10 +7,9 @@ import org.springframework.stereotype.Service;
@Service @Service
public class ConsumerSimulator { public class ConsumerSimulator {
@KafkaListener( @KafkaListener(topics = "${monitor.topic.name}",
topics = "${monitor.topic.name}", containerFactory = "kafkaListenerContainerFactory",
containerFactory = "kafkaListenerContainerFactory", autoStartup = "${monitor.consumer.simulate}")
autoStartup = "${monitor.consumer.simulate}")
public void listenGroup(String message) throws InterruptedException { public void listenGroup(String message) throws InterruptedException {
Thread.sleep(10L); Thread.sleep(10L);
} }

View File

@ -2,53 +2,54 @@ package com.baeldung.monitoring.simulation;
import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory; import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.MicrometerConsumerListener;
import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import io.micrometer.core.instrument.MeterRegistry;
@EnableKafka @EnableKafka
@Configuration @Component
public class KafkaConsumerConfig { public class KafkaConsumerConfig {
@Value(value = "${monitor.kafka.bootstrap.config}") @Value(value = "${monitor.kafka.bootstrap.config}")
private String bootstrapAddress; private String bootstrapAddress;
@Value(value = "${monitor.kafka.consumer.groupid}") @Value(value = "${monitor.kafka.consumer.groupid}")
private String groupId; private String groupId;
@Value(value = "${monitor.kafka.consumer.groupid.simulate}")
private String simulateGroupId;
@Value(value = "${monitor.producer.simulate}")
private boolean enabled;
public ConsumerFactory<String, String> consumerFactory(String groupId) { @Autowired
private MeterRegistry meterRegistry;
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>(); Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
if (enabled) {
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
} else {
props.put(ConsumerConfig.GROUP_ID_CONFIG, simulateGroupId);
}
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 0); props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 0);
return new DefaultKafkaConsumerFactory<>(props); props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
DefaultKafkaConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<>(props);
consumerFactory.addListener(new MicrometerConsumerListener<>(this.meterRegistry));
return consumerFactory;
} }
@Bean @Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() { public ConcurrentKafkaListenerContainerFactory<String, String>
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); kafkaListenerContainerFactory(@Qualifier("consumerFactory") ConsumerFactory<String,
if (enabled) { String> consumerFactory) {
factory.setConsumerFactory(consumerFactory(groupId)); ConcurrentKafkaListenerContainerFactory<String, String> listenerContainerFactory =
} else { new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory(simulateGroupId)); listenerContainerFactory.setConsumerFactory(consumerFactory);
} return listenerContainerFactory;
return factory;
} }
} }

View File

@ -23,10 +23,9 @@ public class ProducerSimulator {
private final boolean enabled; private final boolean enabled;
@Autowired @Autowired
public ProducerSimulator( public ProducerSimulator(KafkaTemplate<String, String> kafkaTemplate,
KafkaTemplate<String, String> kafkaTemplate, @Value(value = "${monitor.topic.name}") String topicName,
@Value(value = "${monitor.topic.name}") String topicName, @Value(value = "${monitor.producer.simulate}") String enabled) {
@Value(value = "${monitor.producer.simulate}") String enabled) {
this.kafkaTemplate = kafkaTemplate; this.kafkaTemplate = kafkaTemplate;
this.topicName = topicName; this.topicName = topicName;
this.enabled = BooleanUtils.toBoolean(enabled); this.enabled = BooleanUtils.toBoolean(enabled);
@ -37,7 +36,9 @@ public class ProducerSimulator {
if (enabled) { if (enabled) {
if (endTime.after(new Date())) { if (endTime.after(new Date())) {
String message = "msg-" + time(); String message = "msg-" + time();
SendResult<String, String> result = kafkaTemplate.send(topicName, message).get(); SendResult<String, String> result = kafkaTemplate
.send(topicName, message)
.get();
} }
} }
} }

View File

@ -1,3 +1,4 @@
server.port=8081
spring.kafka.bootstrap-servers=localhost:9092 spring.kafka.bootstrap-servers=localhost:9092
message.topic.name=baeldung message.topic.name=baeldung
long.message.topic.name=longMessage long.message.topic.name=longMessage
@ -15,3 +16,12 @@ monitor.consumer.simulate=true
monitor.kafka.consumer.groupid.simulate=baeldungGrpSimulate monitor.kafka.consumer.groupid.simulate=baeldungGrpSimulate
test.topic=testtopic1 test.topic=testtopic1
management.endpoints.web.base-path=/actuator
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true
spring.jmx.enabled=false