-
Notifications
You must be signed in to change notification settings - Fork 40.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Kafka Config for consumer/producer by topic Enhancements #39066
Comments
Thanks for the suggestions.
Boot's Given the above, I'm not sure that Boot is the right place to encode knowledge about the relationship between the WDYT, @artembilan?
It's Spring Kafka's We could also look at providing dedicated configuration properties for this in Boot that we'd map to
That's an interesting idea. It certainly feels preferable to configuring class names in YAML or properties which is always error prone.
I'm not sure about this one as I think it could get rather complicated. You'd need to somehow configure a delegate everywhere that the error handling deserializer may be used. With some of the other improvements discussed above, I suspect there may be less of a need for this as it would be easier to perform the error-handling decoration in your own code. |
OK. I see what is going on. That What I would suggest is like a top-level end-user
Although from here it is not clear how to distinguish between This way you would not need that I also wonder if this YAML feature would make it a bit easier for you for time being:
CC @sobychacko |
+1 to the idea from @artembilan where users provide the deserializers as beans, and then Spring Boot injects them into |
Hi, Can you show what the In my case I have many topics, some topics use AVRO values and some use JSON values, so I use the confluent KafkaAvroDeserializer for one and the spring JsonDeserializer for the other, however, since each topic maps to a different resulting class I don't think it works. Edit:I had a go at implementing this, I set the avro deserializer as the bytopic default and ended up having to use custom deserializers for each JSON topic that just extends the spring JsonDeserializer with a concrete type, I was glad to see that the Kafkalistener can just accept +1 to getting |
I raised another issue above after forgetting about this one apologies... I've had a bit more time to think on this and I think it would be more useful for the config to live under spring.kafka.topics:
create-topics: true # a way to disable automatic topic creation [boolean optional default=false]
partitions: 2 # defines the number of partitions to create topics with [numeric optional default=1]
replicas: 2 # defines the number of partitions to create topics with [numeric optional default=1]
compact: true # defines compact cleanup policy to create topics with [boolean optional default=false]
topic-properties: # allows `org.apache.kafka.clients.producer.ProducerConfig` values to be used for topic creation [map optional default={}]
retention.ms: 720000
by-topic-config:
sensor-events: # alias/user friendly name for topic
name: ...-sensor-events # full topic name to consume from [string required]
enabled: true # provides a way to stop consuming from this topic in config [boolean optional default=true]
key-serializer: org.apache.kafka.common.serialization.ByteArraySerializer # configures the by-topic-serializer
value-serializer: io.confluent.kafka.serializers.KafkaAvroSerializer # configures the by-topic-serializer
key-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer # configures the by-topic-deserializer
value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer # configures the by-topic-deserializer
partitions: 3 # overwrite for default above
replicas: 3 # overwrite for default above
compact: false # overwrite for default above
topic-properties: # overwrite for default above
retention.ms: 360000
...-events:
name: ...
... WIP Implementation:import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.header.Header;
import org.apache.kafka.common.header.internals.RecordHeader;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.kafka.DefaultKafkaConsumerFactoryCustomizer;
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaAdmin;
import org.springframework.kafka.core.KafkaTemplate;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@AutoConfiguration
@AutoConfigureAfter(KafkaAutoConfiguration.class)
public class KafkaTopicAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.kafka.topics")
public TopicsConfig topicsConfig() {
return new TopicsConfig();
}
@Bean
public String[] topics(final TopicsConfig topicsConfig) {
return topicsConfig.getByTopicConfig().values().stream()
.filter(TopicConfig::enabled)
.map(TopicConfig::name)
.toArray(String[]::new);
}
@Bean
public String[] realtimeTopics(final TopicsConfig topicsConfig) {
return topicsConfig.getByTopicConfig().entrySet().stream()
.filter(entry -> !entry.getValue().compact() && entry.getValue().enabled())
.map(Map.Entry::getKey)
.toArray(String[]::new);
}
@Bean
public String[] compactTopics(final TopicsConfig topicsConfig) {
return topicsConfig.getByTopicConfig().entrySet().stream()
.filter(entry -> entry.getValue().compact() && entry.getValue().enabled())
.map(Map.Entry::getKey)
.toArray(String[]::new);
}
@Bean
@ConditionalOnProperty("spring.kafka.topics.compact-topic-group-id")
public Header compactTopicRecordHeader(final TopicsConfig topicsConfig) {
return new RecordHeader("compactTopicGroupId", topicsConfig.getCompactTopicGroupId().getBytes(StandardCharsets.UTF_8));
}
@Bean
@ConditionalOnProperty(value = "spring.kafka.topics.create-topics", havingValue = "true")
public KafkaAdmin.NewTopics newTopics(final TopicsConfig topicsConfig) {
final var newTopics = topicsConfig.getByTopicConfig().values().stream()
.filter(TopicConfig::enabled)
.map(topicConfig -> {
final var topicBuilder = TopicBuilder.name(topicConfig.name())
.partitions(Optional.ofNullable(topicConfig.partitions())
.orElse(topicsConfig.getPartitions()))
.replicas(Optional.ofNullable(topicConfig.replicas())
.orElse(topicsConfig.getReplicas()))
.configs(Optional.ofNullable(topicConfig.topicProperties())
.map(topicProperties -> Stream.of(topicProperties, topicsConfig.getTopicProperties())
.filter(Objects::nonNull)
.map(Map::entrySet)
.flatMap(Collection::stream)
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey,
Map.Entry::getValue,
(topicProperty, defaultTopicProperty) -> topicProperty)))
.orElse(topicsConfig.getTopicProperties()));
if (topicConfig.compact()) {
topicBuilder.compact();
}
return topicBuilder.build();
})
.toArray(NewTopic[]::new);
return new KafkaAdmin.NewTopics(newTopics);
}
@Bean
public Map<String, KafkaTemplate<Object, Object>> byTopicKafkaTemplate(final TopicsConfig topicsConfig,
final KafkaProperties kafkaProperties) {
return topicsConfig.getByTopicConfig().entrySet().stream()
.filter(entry -> entry.getValue().enabled())
.filter(entry -> entry.getValue().keySerializer() != null || entry.getValue().valueSerializer() != null)
.map(topicConfig -> {
var producerConfig = new HashMap<>(kafkaProperties.buildProducerProperties(null));
if (topicConfig.getValue().keySerializer() != null) {
producerConfig.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, topicConfig.getValue().keySerializer());
}
if (topicConfig.getValue().valueDeserializer() != null) {
producerConfig.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, topicConfig.getValue().valueDeserializer());
}
final var producerFactory = new DefaultKafkaProducerFactory<>(producerConfig);
final var kafkaTemplate = new KafkaTemplate<>(producerFactory);
kafkaTemplate.setDefaultTopic(topicConfig.getValue().name());
return Map.entry(topicConfig.getKey(), kafkaTemplate);
})
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey,
Map.Entry::getValue));
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public DefaultKafkaConsumerFactoryCustomizer kafkaConsumerFactoryCustomizer(final TopicsConfig topicsConfig) {
final var byTopicKeyDeserializerConfig = topicsConfig.getByTopicConfig().values().stream()
.filter(TopicConfig::enabled)
.filter(topicConfig -> topicConfig.keyDeserializer() != null)
.map(topicConfig -> toByTopicPattern(topicConfig.name(), topicConfig.keyDeserializer()))
.collect(Collectors.joining(","));
final var byTopicValueDeserializerConfig = topicsConfig.getByTopicConfig().values().stream()
.filter(TopicConfig::enabled)
.filter(topicConfig -> topicConfig.valueDeserializer() != null)
.map(topicConfig -> toByTopicPattern(topicConfig.name(), topicConfig.valueDeserializer()))
.collect(Collectors.joining(","));
return consumerFactory -> consumerFactory.updateConfigs(Map.of(
"spring.kafka.key.serialization.bytopic.config", byTopicKeyDeserializerConfig,
"spring.kafka.value.serialization.bytopic.config", byTopicValueDeserializerConfig,
"spring.kafka.filtering.by.headers", "compactTopicGroupId:" + topicsConfig.getCompactTopicGroupId()));
}
private static String toByTopicPattern(String topic, String deserializer) {
return topic + ":" + deserializer;
}
} Further to this I'm trying to figure out how do deal with async retries and DLT's, in my scenario as I imagine is often the case topic's are preferably centrally managed somewhere rather than generated by code so I'm hoping to only need 1 retry / dlt topic per app (still useful to autogenerate for local and integration testing), at the moment I'm exploring a way to use the generated One of the difficulties with the retries and DLT is that since I'm using avro with schema registry the serializer needs to be called with the source topic when producing, hoping I'll post my final solution when I get there but would appreciate some input/thoughts on this. For me config driven isn't just removing boilerplate but I'd like to be able to split up a k8s deployment of an app that consumes from many topics where some topics are more critical and could do with having a separate auto-scaling group, additionally I'm hoping to be able use config maps to quickly pause a consumer by setting enabled to false in the by-topic-config. |
Working implementation https://gist.github.com/StephenFlavin/f4c7dc7758a09e84fb7185ac2d44bcf8 My aspiration here is that all that's required by a dev to set up after the configuration is this 1 class @RetryableTopic
@KafkaListener
public void listen(ConsumerRecord<?, ?> record) {
System.out.println("Received: " + record);
}
@DltHandler
public void dlt(ConsumerRecord<?, ?> record) {
System.out.println("DLT Received: " + record);
} but maybe I'm taking it too far to be built into spring's auto config |
We recently inherited a Porject with a lot of Kafka. Standing on the shoulder of the Spring Giants and with the recent updates we removed as much boilerplate as possible and tried to solve whats possible via configuration.
Delegating Serializer and Deserializer
Delegating Serializer and Deserializer
It took us quite a while to figure why our config was not picked up.
Obiously a beginners fail, solution is here
If you use
spring.kafka.value.serialization.bytopic.config
(kafka property) you must setvalue-deserializer
toorg.springframework.kafka.support.serializer.DelegatingByTopicDeserializer
A Working Config:
Any chance that spring boot can warn about this or make those propeties its own so they can be autoconfigured?
On the same Topic the value of the
spring.kafka.value.serialization.bytopic.config
is a comma separted list of"topicregex:some.com.package.hard.to.read.and.maintain.if.there.is.more.than.one.serializer"
this list becomes hard to read/maintain.
Beeing able to provide this list as "list" or even as map via yaml would be nice.
To add some typesafety a Bean of ConsumerTopicToDeserializer or something similiar which autoconfiguration picks up to do it right and save us fools some time :-)
we used the customizer to add it before we found the solution up top
ErrorHandlingDeserializer
Maybe you see some things which can be adressed in the Documentation and/or autoconfig.
Thanks for the great work!
The text was updated successfully, but these errors were encountered: